Project

General

Profile

Web tech » History » Version 25

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