Project

General

Profile

Web tech » History » Version 28

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