Project

General

Profile

Web tech » History » Version 20

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