Project

General

Profile

Web tech » History » Version 23

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