Project

General

Profile

Web tech » History » Version 29

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