Project

General

Profile

Web tech » History » Version 22

jun chen, 02/19/2025 11:47 AM

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