Project

General

Profile

Web tech » History » Version 21

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