Project

General

Profile

Web tech » History » Version 19

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