Project

General

Profile

Web tech » History » Version 26

jun chen, 03/13/2025 09:12 PM

1 1 jun chen
# Web tech
2
3 3 jun chen
{{toc}}
4
5 1 jun chen
6 4 jun chen
## How to visualize data:
7
8
D3 ref: https://observablehq.com/@d3/gallery  ,  https://johan.github.io/d3/ex/
9
Plotly ref: https://plotly.com/javascript/
10
11 12 jun chen
---
12
13
### **交互功能对比**
14
| **功能**               | **JavaScript/Plotly** | **Python/Plotly** |   **JavaScript/d3**  |
15
|------------------------|-----------------------|-------------------|----------------------|
16
| 缩放/平移              | ✔️                    | ✔️               | ✔️                  |
17
| 悬停显示数值           | ✔️                    | ✔️               | ✔️                  |
18
| 数据点高亮             | ✔️                    | ✔️               | ✔️                  |
19
| 导出为图片(PNG/JPEG) | ✔️                    | ✔️               |✔️                  | 
20
| 动态更新数据           | ✔️(需额外代码)      | ❌               | ✔️                  |
21
| 旧firefox支持          | ❌(globalthis)      | ?                | ✔️                  |
22
23
---
24
25 26 jun chen
### drc 网页显示
26
27
、、、
28
29
<!DOCTYPE html>
30
<html>
31
<head>
32
    <style>
33
        .category { margin: 20px 0; }
34
        .thumbnails {
35
            display: grid;
36
            grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
37
            gap: 8px;
38
        }
39
        .thumbnail {
40
            width: 120px;
41
            height: 90px;
42
            object-fit: cover;
43
            cursor: pointer;
44
            transition: transform 0.2s;
45
        }
46
        .thumbnail:hover { transform: scale(1.05); }
47
        /* 模态框样式 */
48
        .modal {
49
            position: fixed;
50
            top: 0;
51
            left: 0;
52
            width: 100vw;
53
            height: 100vh;
54
            background: rgba(0,0,0,0.8);
55
            display: none;
56
            justify-content: center;
57
            align-items: center;
58
        }
59
        .modal-img {
60
            max-width: 80%;
61
            max-height: 80%;
62
        }
63
        .close-btn {
64
            position: absolute;
65
            top: 20px;
66
            right: 20px;
67
            color: white;
68
            font-size: 30px;
69
            cursor: pointer;
70
        }
71
        .nav-btn {
72
            position: absolute;
73
            top: 50%;
74
            transform: translateY(-50%);
75
            color: white;
76
            font-size: 30px;
77
            cursor: pointer;
78
        }
79
        .prev { left: 20px; }
80
        .next { right: 20px; }
81
    </style>
82
</head>
83
<body>
84
    <div id="gallery"></div>
85
</body>
86
</html>
87
、、、
88
89
```
90
91
// 模拟数据结构
92
const categories = [
93
    {
94
        name: "自然",
95
        photos: ["nature/1.jpg", "nature/2.jpg", /*...*/]
96
    },
97
    // 其他四个分类...
98
];
99
100
// 创建照片墙
101
const gallery = d3.select("#gallery");
102
103
// 创建分类容器
104
const categoryDivs = gallery.selectAll(".category")
105
    .data(categories)
106
    .join("div")
107
    .attr("class", "category");
108
109
// 添加分类标题
110
categoryDivs.append("h2")
111
    .text(d => d.name);
112
113
// 创建缩略图网格
114
categoryDivs.each(function(category) {
115
    const container = d3.select(this);
116
    
117
    container.append("div")
118
        .attr("class", "thumbnails")
119
        .selectAll(".thumbnail")
120
        .data(category.photos)
121
        .join("img")
122
        .attr("class", "thumbnail")
123
        .attr("src", d => d) // 根据实际路径调整
124
        .on("click", function(event, imgPath) {
125
            showModal(category.photos, imgPath);
126
        });
127
});
128
129
// 创建模态框
130
const modal = d3.select("body")
131
    .append("div")
132
    .attr("class", "modal");
133
134
modal.append("span")
135
    .attr("class", "close-btn")
136
    .html("&times;")
137
    .on("click", hideModal);
138
139
const img = modal.append("img")
140
    .attr("class", "modal-img");
141
142
modal.append("div")
143
    .attr("class", "nav-btn prev")
144
    .html("&#10094;")
145
    .on("click", () => navigate(-1));
146
147
modal.append("div")
148
    .attr("class", "nav-btn next")
149
    .html("&#10095;")
150
    .on("click", () => navigate(1));
151
152
let currentImages = [];
153
let currentIndex = 0;
154
155
// 显示模态框
156
function showModal(images, selectedImage) {
157
    currentImages = images;
158
    currentIndex = images.indexOf(selectedImage);
159
    img.attr("src", selectedImage);
160
    modal.style("display", "flex");
161
}
162
163
// 隐藏模态框
164
function hideModal() {
165
    modal.style("display", "none");
166
}
167
168
// 图片导航
169
function navigate(direction) {
170
    currentIndex = (currentIndex + direction + currentImages.length) % currentImages.length;
171
    img.attr("src", currentImages[currentIndex]);
172
}
173
174
// 点击外部关闭
175
window.onclick = function(event) {
176
    if (event.target === modal.node()) {
177
        hideModal();
178
    }
179
}
180
181
// 键盘导航
182
document.addEventListener("keydown", (e) => {
183
    if (modal.style("display") === "flex") {
184
        if (e.key === "ArrowLeft") navigate(-1);
185
        if (e.key === "ArrowRight") navigate(1);
186
        if (e.key === "Escape") hideModal();
187
    }
188
});
189
```
190
191 17 jun chen
192 25 jun chen
### 实现在X轴下方添加可缩放和拖动的滑块条:
193
194 24 jun chen
{{collapse(show code...)
195 1 jun chen
196 24 jun chen
1. 调整边距和创建滑块容器
197 22 jun chen
在原有SVG中增加底部边距,并为滑块条创建新的容器。
198
199
```javascript
200
// 修改边距,底部增加空间给滑块条
201
const margin = { top: 50, right: 50, bottom: 100, left: 50 };
202
203
// 创建主图表容器
204
const svg = d3.select("#chart").append("svg")
205
    .attr("width", width + margin.left + margin.right)
206
    .attr("height", height + margin.top + margin.bottom)
207
  .append("g")
208
    .attr("transform", `translate(${margin.left},${margin.top})`);
209
210
// 创建滑块条的容器(位于主图表下方)
211
const slider = d3.select("svg")
212
  .append("g")
213
    .attr("class", "slider")
214
    .attr("transform", `translate(${margin.left},${height + margin.top + 20})`);
215 1 jun chen
```
216 22 jun chen
217 24 jun chen
2. 初始化滑块比例尺和轴
218 22 jun chen
使用主图表的原始数据范围定义滑块的比例尺。
219
220
```javascript
221
// 主图表的比例尺(可缩放)
222
let x = d3.scaleLinear().range([0, width]);
223
224
// 滑块的比例尺(固定为完整数据范围)
225
let xSlider = d3.scaleLinear().range([0, width]);
226
227
// 滑块的X轴
228
const xAxisSlider = d3.axisBottom(xSlider)
229
  .tickSize(0)
230 1 jun chen
  .tickFormat("");
231 22 jun chen
```
232
233 24 jun chen
3. 绘制滑块条的背景和轴
234 22 jun chen
在滑块区域绘制简化的折线图和轴。
235
236
```javascript
237
// 在数据加载后初始化滑块
238
d3.csv("data.csv").then(data => {
239
  // ...原有数据处理...
240
241
  // 初始化滑块比例尺
242
  xSlider.domain(d3.extent(data, d => d.index));
243
  
244
  // 绘制滑块背景折线
245
  slider.append("path")
246
    .datum(data)
247
    .attr("class", "slider-line")
248
    .attr("d", d3.line()
249
      .x(d => xSlider(d.index))
250
      .y(0)); // 简化高度
251
252
  // 添加滑块轴
253
  slider.append("g")
254
    .attr("class", "x-axis-slider")
255 1 jun chen
    .call(xAxisSlider);
256 22 jun chen
});
257
```
258
259 24 jun chen
4. 添加刷子并处理事件
260 22 jun chen
使用`d3.brushX`创建交互刷子。
261
262
```javascript
263
// 创建刷子
264
const brush = d3.brushX()
265
  .extent([[0, -10], [width, 10]]) // 设置刷子区域高度
266
  .on("brush", brushing)
267
  .on("end", brushEnded);
268
269
// 应用刷子到滑块
270
slider.append("g")
271
  .attr("class", "brush")
272
  .call(brush);
273
274
// 刷子拖动时的处理函数
275
function brushing(event) {
276
  if (!event.selection) return;
277
  const [x0, x1] = event.selection;
278
  x.domain([xSlider.invert(x0), xSlider.invert(x1)]);
279
  updateMainChart(); // 更新主图表
280
}
281
282
// 刷子结束时的处理(双击重置)
283
function brushEnded(event) {
284
  if (!event.selection) {
285
    x.domain(xSlider.domain()); // 重置为完整范围
286 1 jun chen
    updateMainChart();
287 22 jun chen
  }
288
}
289
```
290
291 24 jun chen
5. 封装主图表更新函数
292 22 jun chen
将主图表的绘制逻辑封装成可重用的函数。
293
294
```javascript
295
function updateMainChart() {
296
  // 更新X轴
297
  svg.select(".x-axis").call(d3.axisBottom(x));
298
  
299
  // 更新折线
300
  svg.selectAll(".line")
301
    .attr("d", d3.line()
302
      .x(d => x(d.index))
303
      .y(d => y(d[currentY])));
304
  
305
  // 更新点位置
306
  svg.selectAll(".dot")
307
    .attr("cx", d => x(d.index));
308 1 jun chen
  
309 22 jun chen
  // 更新背景区域
310
  drawBackgroundRegions(ranges);
311
}
312
```
313
314 24 jun chen
6. CSS样式调整
315 22 jun chen
添加滑块条样式:
316
317
```css
318
.slider-line {
319
  fill: none;
320
  stroke: #666;
321
  stroke-width: 1px;
322
}
323
324
.brush .selection {
325
  fill: steelblue;
326
  fill-opacity: 0.3;
327
  stroke: none;
328 1 jun chen
}
329 22 jun chen
330
.brush .handle {
331
  fill: steelblue;
332
}
333
```
334
335 24 jun chen
7. 完整修改后的代码结构
336 22 jun chen
整合上述修改后的核心代码:
337
338
```javascript
339
// 设置边距和尺寸
340
const margin = { top: 50, right: 50, bottom: 100, left: 50 };
341
const width = 960 - margin.left - margin.right;
342
const height = 500 - margin.top - margin.bottom;
343
344
// 创建SVG容器
345
const svg = d3.select("#chart").append("svg")
346
    .attr("width", width + margin.left + margin.right)
347
    .attr("height", height + margin.top + margin.bottom + 50); // 增加底部空间
348
349
// 主图表容器
350
const main = svg.append("g")
351
    .attr("transform", `translate(${margin.left},${margin.top})`);
352
353
// 滑块容器
354
const slider = svg.append("g")
355
    .attr("class", "slider")
356
    .attr("transform", `translate(${margin.left},${height + margin.top + 30})`);
357
358
// 初始化比例尺
359
const x = d3.scaleLinear().range([0, width]);
360
const xSlider = d3.scaleLinear().range([0, width]);
361
const y = d3.scaleLinear().range([height, 0]);
362
363
// 加载数据
364
d3.csv("data.csv").then(data => {
365
  // 数据处理...
366
  data.forEach(d => {
367
    d.index = +d.index;
368
    d.observation1 = +d.observation1;
369
    d.observation2 = +d.observation2;
370
  });
371
372
  // 设置比例尺
373
  x.domain(d3.extent(data, d => d.index));
374
  xSlider.domain(x.domain());
375
  y.domain([0, d3.max(data, d => Math.max(d.observation1, d.observation2))]);
376
377
  // 绘制主图表
378
  drawMainChart(data);
379
  
380
  // 绘制滑块背景
381
  slider.append("path")
382
    .datum(data)
383
    .attr("class", "slider-line")
384
    .attr("d", d3.line()
385
      .x(d => xSlider(d.index))
386
      .y(0));
387
388
  // 添加刷子
389
  const brush = d3.brushX()
390
    .extent([[0, -10], [width, 10]])
391
    .on("brush", brushing)
392
    .on("end", brushEnded);
393
394
  slider.append("g")
395
    .attr("class", "brush")
396
    .call(brush);
397
});
398
399
function drawMainChart(data) {
400
  // 绘制折线、点、轴等...
401
  // 使用x比例尺进行所有定位
402
}
403
404
function updateMainChart() {
405
  // 更新所有依赖x比例尺的元素
406
  main.select(".x-axis").call(d3.axisBottom(x));
407
  main.selectAll(".line").attr("d", ...);
408
  main.selectAll(".dot").attr("cx", ...);
409
  // 更新背景区域...
410
}
411
412
// 刷子事件处理函数
413
function brushing(event) {
414
  if (!event.selection) return;
415
  const [x0, x1] = event.selection.map(xSlider.invert);
416
  x.domain([x0, x1]);
417
  updateMainChart();
418
}
419
420 1 jun chen
function brushEnded(event) {
421 22 jun chen
  if (!event.selection) {
422
    x.domain(xSlider.domain());
423
    updateMainChart();
424
  }
425
}
426
```
427 24 jun chen
}}
428 22 jun chen
429 20 jun chen
### 交互,圆圈,准心
430 1 jun chen
431 20 jun chen
{{collapse(show code...)
432
433 19 jun chen
```
434
// 设置图表的尺寸和边距
435
const margin = {top: 50, right: 50, bottom: 50, left: 50},
436
      width = 960 - margin.left - margin.right,
437
      height = 500 - margin.top - margin.bottom;
438
439
// 设置SVG的尺寸
440
const svg = d3.select("#chart").append("svg")
441
    .attr("width", width + margin.left + margin.right)
442
    .attr("height", height + margin.top + margin.bottom)
443
  .append("g")
444
    .attr("transform", `translate(${margin.left},${margin.top})`);
445
446
// 设置比例尺
447
const x = d3.scaleLinear().range([0, width]);
448
const y = d3.scaleLinear().range([height, 0]);
449
450
// 定义折线生成器
451
const line = d3.line()
452
    .x(d => x(d.index)) // 使用第一列(index)作为X轴
453
    .y(d => y(d.observation)); // 使用观测值作为Y轴
454
455
// 初始化Y轴数据为第二列(observation1)
456
let currentY = "observation1";
457
458
// 创建高亮圆圈
459
const highlightCircle = svg.append("circle")
460
    .attr("class", "highlight-circle")
461
    .attr("r", 7) // 圆圈的半径
462
    .style("fill", "none")
463
    .style("stroke", "red")
464
    .style("stroke-width", 2)
465
    .style("opacity", 0); // 初始不可见
466
467
// 创建准心线(水平线和垂直线)
468
const horizontalLine = svg.append("line")
469
    .attr("class", "crosshair-line")
470
    .style("stroke", "red")
471
    .style("stroke-width", 1)
472
    .style("stroke-dasharray", "3,3") // 虚线样式
473
    .style("opacity", 0); // 初始不可见
474
475
const verticalLine = svg.append("line")
476
    .attr("class", "crosshair-line")
477
    .style("stroke", "red")
478
    .style("stroke-width", 1)
479
    .style("stroke-dasharray", "3,3") // 虚线样式
480
    .style("opacity", 0); // 初始不可见
481
482
// 解析 label 中的 begin 和 end 标记
483
function parseLabelRanges(data) {
484
    const ranges = [];
485
    let beginIndex = null;
486
487
    data.forEach((d, i) => {
488
        if (d.label.startsWith("begin")) {
489
            beginIndex = d.index; // 记录 begin 的 index
490
        } else if (d.label.startsWith("end") && beginIndex !== null) {
491
            ranges.push({ begin: beginIndex, end: d.index }); // 记录 begin 和 end 的范围
492
            beginIndex = null; // 重置 beginIndex
493
        }
494
    });
495
496
    return ranges;
497
}
498
499
// 绘制绿色背景区域
500
function drawBackgroundRegions(ranges) {
501
    // 移除旧的背景区域
502
    svg.selectAll(".background-region").remove();
503
504
    // 绘制新的背景区域
505
    ranges.forEach(range => {
506
        svg.append("rect")
507
            .attr("class", "background-region")
508
            .attr("x", x(range.begin)) // 起始位置
509
            .attr("width", x(range.end) - x(range.begin)) // 宽度
510
            .attr("y", 0) // 从顶部开始
511
            .attr("height", height) // 覆盖整个图表高度
512
            .style("fill", "green")
513
            .style("opacity", 0.2); // 设置透明度
514
    });
515
}
516
517
// 读取CSV文件
518
d3.csv("data.csv").then(data => {
519
    // 转换数据类型
520
    data.forEach(d => {
521
        d.index = +d.index; // 第一列转换为数值
522
        d.observation1 = +d.observation1; // 第二列转换为数值
523
        d.observation2 = +d.observation2; // 第三列转换为数值
524
    });
525
526
    // 设置比例尺的域
527
    x.domain(d3.extent(data, d => d.index)); // X轴范围为第一列的最小值和最大值
528
    y.domain([0, d3.max(data, d => Math.max(d.observation1, d.observation2))]; // Y轴范围为0到观测值的最大值
529
530
    // 解析 label 中的 begin 和 end 标记
531
    const ranges = parseLabelRanges(data);
532
533
    // 绘制初始折线图(使用observation1)
534
    drawChart(data, ranges);
535
536
    // 添加按钮点击事件
537
    d3.select("#toggleButton").on("click", function() {
538
        // 切换Y轴数据
539
        currentY = currentY === "observation1" ? "observation2" : "observation1";
540
        // 更新按钮文本
541
        d3.select(this).text(currentY === "observation1" ? "Switch to Observation 2" : "Switch to Observation 1");
542
        // 重新绘制折线图
543
        drawChart(data, ranges);
544
    });
545
});
546
547
// 绘制折线图的函数
548
function drawChart(data, ranges) {
549
    // 移除旧的折线和点
550
    svg.selectAll(".line").remove();
551
    svg.selectAll(".dot").remove();
552
553
    // 更新折线生成器的Y值
554
    line.y(d => y(d[currentY]));
555
556
    // 添加折线
557
    svg.append("path")
558
        .datum(data)
559
        .attr("class", `line ${currentY === "observation1" ? "line1" : "line2"}`)
560
        .attr("d", line);
561
562
    // 添加点
563
    svg.selectAll(".dot")
564
        .data(data)
565
      .enter().append("circle")
566
        .attr("class", "dot")
567
        .attr("cx", d => x(d.index)) // 使用第一列(index)作为X轴
568
        .attr("cy", d => y(d[currentY])) // 使用当前观测值作为Y轴
569
        .attr("r", 5)
570
        .on("mouseover", function(event, d) {
571
            // 显示高亮圆圈
572
            highlightCircle
573
                .attr("cx", x(d.index))
574
                .attr("cy", y(d[currentY]))
575
                .style("opacity", 1);
576
577
            // 显示准心线
578
            horizontalLine
579
                .attr("x1", 0)
580
                .attr("x2", width)
581
                .attr("y1", y(d[currentY]))
582
                .attr("y2", y(d[currentY]))
583
                .style("opacity", 1);
584
585
            verticalLine
586
                .attr("x1", x(d.index))
587
                .attr("x2", x(d.index))
588
                .attr("y1", 0)
589
                .attr("y2", height)
590
                .style("opacity", 1);
591
592
            // 显示工具提示
593
            tooltip.transition()
594
                .duration(200)
595
                .style("opacity", .9);
596
            tooltip.html(`
597
                <div><strong>Index:</strong> ${d.index}</div>
598
                <div><strong>Observation:</strong> ${d[currentY]}</div>
599
                <div><strong>Label:</strong> ${d.label}</div>
600
            `)
601
            .style("left", (event.pageX + 5) + "px")
602
            .style("top", (event.pageY - 28) + "px");
603
        })
604
        .on("mouseout", function(d) {
605
            // 隐藏高亮圆圈和准心线
606
            highlightCircle.style("opacity", 0);
607
            horizontalLine.style("opacity", 0);
608
            verticalLine.style("opacity", 0);
609
610
            // 隐藏工具提示
611
            tooltip.transition()
612
                .duration(500)
613
                .style("opacity", 0);
614
        });
615
616
    // 绘制绿色背景区域
617
    drawBackgroundRegions(ranges);
618
619
    // 添加X轴
620
    svg.select(".x-axis").remove(); // 移除旧的X轴
621
    svg.append("g")
622
        .attr("class", "x-axis")
623
        .attr("transform", `translate(0,${height})`)
624
        .call(d3.axisBottom(x));
625
626
    // 添加Y轴
627
    svg.select(".y-axis").remove(); // 移除旧的Y轴
628
    svg.append("g")
629
        .attr("class", "y-axis")
630
        .call(d3.axisLeft(y));
631
632
    // 添加X轴标签
633
    svg.select(".x-axis-label").remove(); // 移除旧的X轴标签
634
    svg.append("text")
635
        .attr("class", "x-axis-label")
636
        .attr("x", width / 2)
637
        .attr("y", height + margin.bottom - 10)
638
        .style("text-anchor", "middle")
639
        .text("时间");
640
641
    // 添加Y轴标签
642
    svg.select(".y-axis-label").remove(); // 移除旧的Y轴标签
643
    svg.append("text")
644
        .attr("class", "y-axis-label")
645
        .attr("transform", "rotate(-90)")
646
        .attr("x", -height / 2)
647
        .attr("y", -margin.left + 20)
648
        .style("text-anchor", "middle")
649
        .text("观测值");
650
}
651
652
// 添加提示工具
653
const tooltip = d3.select("body").append("div")
654
    .attr("class", "tooltip")
655
    .style("opacity", 0)
656
    .style("font-size", "14px") // 放大字体
657
    .style("text-align", "left"); // 左对齐
658
659 1 jun chen
```
660 20 jun chen
}}
661 17 jun chen
662 20 jun chen
### button 切换图表
663 17 jun chen
664 18 jun chen
665
{{collapse(show code...)
666 17 jun chen
``` 
667
// 设置图表的尺寸和边距
668
const margin = {top: 20, right: 30, bottom: 30, left: 40},
669
      width = 960 - margin.left - margin.right,
670
      height = 500 - margin.top - margin.bottom;
671
672
// 设置SVG的尺寸
673
const svg = d3.select("#chart").append("svg")
674
    .attr("width", width + margin.left + margin.right)
675
    .attr("height", height + margin.top + margin.bottom)
676
  .append("g")
677
    .attr("transform", `translate(${margin.left},${margin.top})`);
678
679
// 设置比例尺
680
const x = d3.scaleLinear().range([0, width]);
681
const y = d3.scaleLinear().range([height, 0]);
682
683
// 定义折线生成器
684
const line = d3.line()
685
    .x(d => x(d.index)) // 使用第一列(index)作为X轴
686
    .y(d => y(d.observation)); // 使用观测值作为Y轴
687
688
// 初始化Y轴数据为第二列(observation1)
689
let currentY = "observation1";
690
691
// 读取CSV文件
692 1 jun chen
d3.csv("data.csv").then(data => {
693 17 jun chen
    // 转换数据类型
694 18 jun chen
    data.forEach(d => {
695
        d.index = + parseInt(d.index); // 第一列转换为数值
696
        d.observation1 = + parseFloat(d.observation1); // 第二列转换为数值
697 17 jun chen
        d.observation2 = + parseFloat(d.observation2); // 第三列转换为数值
698
    });
699
700
    // 设置比例尺的域
701
    x.domain(d3.extent(data, d => d.index)); // X轴范围为第一列的最小值和最大值
702
    y.domain([0, d3.max(data, d => Math.max(d.observation1, d.observation2))]); // Y轴范围为0到观测值的最大值
703
704
    // 绘制初始折线图(使用observation1)
705
    drawChart(data);
706
707
    // 添加按钮点击事件
708
    d3.select("#toggleButton").on("click", function() {
709
        // 切换Y轴数据
710
        currentY = currentY === "observation1" ? "observation2" : "observation1";
711
        // 更新按钮文本
712
        d3.select(this).text(currentY === "observation1" ? "Switch to Observation 2" : "Switch to Observation 1");
713
        // 重新绘制折线图
714
        drawChart(data);
715
    });
716
});
717
718
// 绘制折线图的函数
719
function drawChart(data) {
720
    // 移除旧的折线和点
721
    svg.selectAll(".line").remove();
722
    svg.selectAll(".dot").remove();
723
724
    // 更新折线生成器的Y值
725
    line.y(d => y(d[currentY]));
726
727
    // 添加折线
728
    svg.append("path")
729
        .datum(data)
730
        .attr("class", `line ${currentY === "observation1" ? "line1" : "line2"}`)
731
        .attr("d", line);
732
733
    // 添加点
734
    svg.selectAll(".dot")
735
        .data(data)
736
      .enter().append("circle")
737
        .attr("class", "dot")
738
        .attr("cx", d => x(d.index)) // 使用第一列(index)作为X轴
739
        .attr("cy", d => y(d[currentY])) // 使用当前观测值作为Y轴
740
        .attr("r", 5)
741
        .on("mouseover", function(event, d) {
742
            tooltip.transition()
743
                .duration(200)
744
                .style("opacity", .9);
745
            tooltip.html(`Index: ${d.index}<br>Observation: ${d[currentY]}<br>Label: ${d.label}`)
746
                .style("left", (event.pageX + 5) + "px")
747
                .style("top", (event.pageY - 28) + "px");
748
        })
749
        .on("mouseout", function(d) {
750
            tooltip.transition()
751
                .duration(500)
752
                .style("opacity", 0);
753
        });
754
755
    // 添加X轴
756
    svg.select(".x-axis").remove(); // 移除旧的X轴
757
    svg.append("g")
758
        .attr("class", "x-axis")
759
        .attr("transform", `translate(0,${height})`)
760
        .call(d3.axisBottom(x));
761
762
    // 添加Y轴
763
    svg.select(".y-axis").remove(); // 移除旧的Y轴
764
    svg.append("g")
765
        .attr("class", "y-axis")
766
        .call(d3.axisLeft(y));
767
}
768 1 jun chen
769 17 jun chen
// 添加提示工具
770
const tooltip = d3.select("body").append("div")
771
    .attr("class", "tooltip")
772 18 jun chen
    .style("opacity", 0);
773 17 jun chen
``` 
774
775 16 jun chen
776 15 jun chen
### js d3 读入 csv 并绘制折线交互
777 16 jun chen
778 15 jun chen
{{collapse(show code...)
779
``` 
780
781
// 设置图表的尺寸和边距
782
const margin = {top: 20, right: 30, bottom: 30, left: 40},
783
      width = 960 - margin.left - margin.right,
784
      height = 500 - margin.top - margin.bottom;
785
786
// 设置SVG的尺寸
787
const svg = d3.select("#chart").append("svg")
788
    .attr("width", width + margin.left + margin.right)
789
    .attr("height", height + margin.top + margin.bottom)
790
  .append("g")
791
    .attr("transform", `translate(${margin.left},${margin.top})`);
792
793
// 设置比例尺
794
const x = d3.scaleLinear().range([0, width]);
795
const y = d3.scaleLinear().range([height, 0]);
796
797
// 定义折线生成器
798
const line = d3.line()
799
    .x((d, i) => x(i))
800
    .y(d => y(d.observation));
801
802
// 读取CSV文件
803
d3.csv("data.csv").then(data => {
804
    // 转换数据类型
805
    data.forEach((d, i) => {
806
        d.observation = +d.observation;
807
        d.index = i;
808
    });
809
810
    // 设置比例尺的域
811
    x.domain([0, data.length - 1]);
812
    y.domain([0, d3.max(data, d => d.observation)]);
813
814
    // 添加折线
815
    svg.append("path")
816
        .datum(data)
817
        .attr("class", "line")
818
        .attr("d", line);
819
820
    // 添加点
821
    svg.selectAll(".dot")
822
        .data(data)
823
      .enter().append("circle")
824
        .attr("class", "dot")
825
        .attr("cx", (d, i) => x(i))
826
        .attr("cy", d => y(d.observation))
827
        .attr("r", 5)
828
        .on("mouseover", function(event, d) {
829
            tooltip.transition()
830
                .duration(200)
831
                .style("opacity", .9);
832
            tooltip.html(`Observation: ${d.observation}<br>Label: ${d.label}`)
833
                .style("left", (event.pageX + 5) + "px")
834
                .style("top", (event.pageY - 28) + "px");
835
        })
836
        .on("mouseout", function(d) {
837
            tooltip.transition()
838
                .duration(500)
839
                .style("opacity", 0);
840
        });
841
842
    // 添加X轴
843
    svg.append("g")
844
        .attr("transform", `translate(0,${height})`)
845
        .call(d3.axisBottom(x));
846
847
    // 添加Y轴
848
    svg.append("g")
849
        .call(d3.axisLeft(y));
850
});
851
852
// 添加提示工具
853
const tooltip = d3.select("body").append("div")
854
    .attr("class", "tooltip")
855
    .style("opacity", 0);
856
```
857
858
``` 
859
<!DOCTYPE html>
860
<html lang="en">
861
<head>
862
    <meta charset="UTF-8">
863
    <title>D3.js Line Chart</title>
864
    <script src="https://d3js.org/d3.v7.min.js"></script>
865
    <style>
866
        .line {
867
            fill: none;
868
            stroke: steelblue;
869
            stroke-width: 2px;
870
        }
871
        .dot {
872
            fill: steelblue;
873
            stroke: #fff;
874
        }
875
        .tooltip {
876
            position: absolute;
877
            text-align: center;
878
            width: 120px;
879
            height: auto;
880
            padding: 5px;
881
            font: 12px sans-serif;
882
            background: lightsteelblue;
883
            border: 0px;
884
            border-radius: 8px;
885
            pointer-events: none;
886
        }
887
    </style>
888
</head>
889
<body>
890
    <div id="chart"></div>
891 1 jun chen
    <script src="script.js"></script>
892 15 jun chen
</body>
893
</html>
894
895 16 jun chen
```
896 15 jun chen
}}
897 5 jun chen
898 4 jun chen
### js d3 内嵌数据显示折线
899 6 jun chen
900
{{collapse(show code...)
901 1 jun chen
902
``` 
903
<!DOCTYPE html>
904
<html lang="en">
905
<head>
906
    <meta charset="UTF-8">
907
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
908
    <title>分段填充折线图</title>
909
    <script src="https://d3js.org/d3.v7.min.js"></script>
910
    <style>
911
        .tooltip {
912
            position: absolute;
913
            background-color: rgba(255, 255, 255, 0.9);
914
            border: 1px solid #ccc;
915
            padding: 5px;
916
            font-size: 12px;
917
            pointer-events: none;
918
            opacity: 0;
919
        }
920
        .line {
921
            fill: none;
922
            stroke: black;
923
            stroke-width: 2;
924
        }
925
    </style>
926
</head>
927
<body>
928
    <div id="chart"></div>
929
    <div class="tooltip" id="tooltip"></div>
930
931
    <script>
932
        // 示例数据
933
        const data = [
934
            { time: "2025-01-01", value: 10, description: "说明1" },
935
            { time: "2025-01-02", value: 15, description: "说明2" },
936
            { time: "2025-01-03", value: 20, description: "说明3" },
937
            { time: "2025-01-04", value: 25, description: "说明4" },
938
            { time: "2025-01-05", value: 30, description: "说明5" },
939
            { time: "2025-01-06", value: 35, description: "说明6" },
940
            { time: "2025-01-07", value: 40, description: "说明7" },
941
            { time: "2025-01-08", value: 45, description: "说明8" },
942
            { time: "2025-01-09", value: 50, description: "说明9" },
943
            { time: "2025-01-10", value: 55, description: "说明10" },
944
            { time: "2025-01-11", value: 60, description: "说明11" },
945
            { time: "2025-01-12", value: 65, description: "说明12" },
946
            { time: "2025-01-13", value: 70, description: "说明13" },
947
            { time: "2025-01-14", value: 75, description: "说明14" },
948
            { time: "2025-01-15", value: 80, description: "说明15" },
949
            { time: "2025-01-16", value: 85, description: "说明16" },
950
            { time: "2025-01-17", value: 90, description: "说明17" },
951
            { time: "2025-01-18", value: 95, description: "说明18" },
952
            { time: "2025-01-19", value: 100, description: "说明19" },
953
            { time: "2025-01-20", value: 105, description: "说明20" }
954
        ];
955
956
        // 设置图表尺寸
957
        const margin = { top: 20, right: 30, bottom: 30, left: 40 };
958
        const width = 800 - margin.left - margin.right;
959
        const height = 400 - margin.top - margin.bottom;
960
961
        // 创建 SVG 容器
962
        const svg = d3.select("#chart")
963
            .append("svg")
964
            .attr("width", width + margin.left + margin.right)
965
            .attr("height", height + margin.top + margin.bottom)
966
            .append("g")
967
            .attr("transform", `translate(${margin.left},${margin.top})`);
968
969
        // 解析时间格式
970
        const parseTime = d3.timeParse("%Y-%m-%d");
971
972
        // 格式化数据
973
        data.forEach(d => {
974
            d.time = parseTime(d.time);
975
            d.value = +d.value;
976
        });
977
978
        // 设置比例尺
979
        const x = d3.scaleTime()
980
            .domain(d3.extent(data, d => d.time))
981
            .range([0, width]);
982
983
        const y = d3.scaleLinear()
984
            .domain([0, d3.max(data, d => d.value)])
985
            .range([height, 0]);
986
987
        // 添加 X 轴
988
        svg.append("g")
989
            .attr("transform", `translate(0,${height})`)
990
            .call(d3.axisBottom(x));
991
992
        // 添加 Y 轴
993
        svg.append("g")
994
            .call(d3.axisLeft(y));
995
996
        // 创建折线生成器
997
        const line = d3.line()
998
            .x(d => x(d.time))
999
            .y(d => y(d.value));
1000
1001
        // 绘制折线
1002
        svg.append("path")
1003
            .datum(data)
1004
            .attr("class", "line")
1005
            .attr("d", line);
1006
1007
        // 分段填充颜色
1008
        const first10 = data.slice(0, 10);
1009
        const last10 = data.slice(-10);
1010
        const middle = data.slice(10, -10);
1011
1012
        // 填充前十个时间段的绿色区域
1013
        svg.append("path")
1014
            .datum(first10)
1015
            .attr("fill", "green")
1016
            .attr("opacity", 0.3)
1017
            .attr("d", d3.area()
1018
                .x(d => x(d.time))
1019
                .y0(height)
1020
                .y1(d => y(d.value))
1021
            );
1022
1023
        // 填充中间时间段的蓝色区域
1024
        svg.append("path")
1025
            .datum(middle)
1026
            .attr("fill", "blue")
1027
            .attr("opacity", 0.3)
1028
            .attr("d", d3.area()
1029
                .x(d => x(d.time))
1030
                .y0(height)
1031
                .y1(d => y(d.value))
1032
            );
1033
1034
        // 填充后十个时间段的红色区域
1035
        svg.append("path")
1036
            .datum(last10)
1037
            .attr("fill", "red")
1038
            .attr("opacity", 0.3)
1039
            .attr("d", d3.area()
1040
                .x(d => x(d.time))
1041
                .y0(height)
1042
                .y1(d => y(d.value))
1043
            );
1044
1045
        // 添加悬停交互
1046
        const tooltip = d3.select("#tooltip");
1047
1048
        svg.selectAll(".dot")
1049
            .data(data)
1050
            .enter()
1051
            .append("circle")
1052
            .attr("class", "dot")
1053
            .attr("cx", d => x(d.time))
1054
            .attr("cy", d => y(d.value))
1055
            .attr("r", 5)
1056
            .attr("fill", "steelblue")
1057
            .on("mouseover", (event, d) => {
1058
                tooltip.style("opacity", 1)
1059
                    .html(`时间: ${d3.timeFormat("%Y-%m-%d")(d.time)}<br>数值: ${d.value}<br>说明: ${d.description}`)
1060
                    .style("left", `${event.pageX + 5}px`)
1061
                    .style("top", `${event.pageY - 20}px`);
1062
            })
1063
            .on("mouseout", () => {
1064
                tooltip.style("opacity", 0);
1065
            });
1066
    </script>
1067
</body>
1068
</html>
1069 6 jun chen
```
1070
1071 1 jun chen
}}
1072 13 jun chen
1073 1 jun chen
### JavaScript + Plotly(纯前端实现)
1074 8 jun chen
1075
{{collapse(show code...)
1076 1 jun chen
1077
```html
1078
<!DOCTYPE html>
1079
<html>
1080
<head>
1081
    <title>交互式图表</title>
1082
    <!-- 引入 Plotly.js -->
1083
    <script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script>
1084
</head>
1085
<body>
1086
    <div id="chart"></div>
1087
1088
    <script>
1089
        // 读取CSV文件(假设文件名为 data.csv)
1090
        fetch('data.csv')
1091
            .then(response => response.text())
1092
            .then(csvText => {
1093
                // 解析CSV数据
1094
                const rows = csvText.split('\n');
1095
                const x = [], y = [];
1096
                rows.forEach((row, index) => {
1097
                    if (index === 0) return; // 跳过标题行
1098
                    const [xVal, yVal] = row.split(',');
1099
                    x.push(parseFloat(xVal));
1100
                    y.push(parseFloat(yVal));
1101
                });
1102
1103
                // 绘制图表
1104
                Plotly.newPlot('chart', [{
1105
                    x: x,
1106
                    y: y,
1107
                    type: 'scatter',
1108
                    mode: 'lines+markers',
1109
                    marker: { color: 'blue' },
1110
                    line: { shape: 'spline' }
1111
                }], {
1112
                    title: '交互式数据图表',
1113
                    xaxis: { title: 'X轴' },
1114
                    yaxis: { title: 'Y轴' },
1115
                    hovermode: 'closest'
1116
                });
1117
            });
1118
    </script>
1119
</body>
1120
</html>
1121 8 jun chen
```
1122 1 jun chen
}}
1123
1124
#### 使用步骤:
1125
1. 将CSV文件命名为 `data.csv`,格式如下:
1126
   ```csv
1127
   x,y
1128
   1,5
1129
   2,3
1130
   3,7
1131
   4,2
1132
   5,8
1133
   ```
1134
2. 将HTML文件和 `data.csv` 放在同一目录下,用浏览器打开HTML文件。
1135
3. 效果:支持**缩放、悬停显示数值、拖拽平移**等交互。
1136
1137
---
1138 14 jun chen
1139 1 jun chen
### Python + Plotly(生成独立HTML文件)
1140
#### 特点:适合Python用户,自动化生成图表文件。
1141 9 jun chen
1142 1 jun chen
{{collapse(show code...)
1143
```python
1144
import pandas as pd
1145
import plotly.express as px
1146
1147
# 1. 读取CSV文件
1148
df = pd.read_csv("data.csv")
1149
1150
# 2. 创建交互式图表
1151
fig = px.line(
1152
    df, x='x', y='y',
1153
    title='Python生成的交互式图表',
1154
    markers=True,  # 显示数据点
1155
    line_shape='spline'  # 平滑曲线
1156
)
1157
1158
# 3. 自定义悬停效果和样式
1159
fig.update_traces(
1160
    hoverinfo='x+y',  # 悬停显示x和y值
1161
    line=dict(width=2, color='royalblue'),
1162
    marker=dict(size=8, color='firebrick')
1163
)
1164
1165
# 4. 保存为HTML文件
1166
fig.write_html("interactive_chart.html")
1167
```
1168
}}
1169
1170
#### 使用步骤:
1171
1. 安装依赖:
1172
   ```bash
1173
   pip install pandas plotly
1174
   ```
1175
2. 运行代码后,生成 `interactive_chart.html`,用浏览器打开即可看到图表。
1176
1177
### **进阶方案(可选)**
1178 11 jun chen
1. **动态数据加载**(JavaScript):
1179
1180 1 jun chen
{{collapse(View details...)
1181
   ```html
1182
   <input type="file" id="csvFile" accept=".csv">
1183
   <div id="chart"></div>
1184
   <script>
1185
     document.getElementById('csvFile').addEventListener('change', function(e) {
1186
       const file = e.target.files[0];
1187
       const reader = new FileReader();
1188
       reader.onload = function(e) {
1189
         // 解析并绘制图表(代码同方法一)
1190
       };
1191
       reader.readAsText(file);
1192
     });
1193
   </script>
1194 11 jun chen
   ```
1195 1 jun chen
}}
1196
   - 用户可上传任意CSV文件,实时生成图表。
1197 11 jun chen
1198 1 jun chen
1199 11 jun chen
2. **添加控件**(Python + Dash):
1200 1 jun chen
{{collapse(View details...)
1201
   ```python
1202
   from dash import Dash, dcc, html
1203
   import pandas as pd
1204
   import plotly.express as px
1205
1206
   app = Dash(__name__)
1207
   df = pd.read_csv("data.csv")
1208
1209
   app.layout = html.Div([
1210
       dcc.Graph(
1211
           id='live-chart',
1212
           figure=px.scatter(df, x='x', y='y', title='Dash动态图表')
1213
       ),
1214
       html.Button('更新数据', id='update-button')
1215
   ])
1216
1217
   if __name__ == '__main__':
1218
       app.run_server(debug=True)
1219 11 jun chen
   ```
1220
}}
1221 1 jun chen
1222
   - 运行后访问 `http://localhost:8050`,支持动态交互和按钮触发操作。
1223
1224
---