Project

General

Profile

Web tech » History » Version 24

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