Project

General

Profile

Web tech » History » Version 18

jun chen, 02/17/2025 06:25 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
### button
27
28
29 18 jun chen
{{collapse(show code...)
30
``` 
31 17 jun chen
// 设置图表的尺寸和边距
32
const margin = {top: 20, right: 30, bottom: 30, left: 40},
33
      width = 960 - margin.left - margin.right,
34
      height = 500 - margin.top - margin.bottom;
35
36
// 设置SVG的尺寸
37
const svg = d3.select("#chart").append("svg")
38
    .attr("width", width + margin.left + margin.right)
39
    .attr("height", height + margin.top + margin.bottom)
40
  .append("g")
41
    .attr("transform", `translate(${margin.left},${margin.top})`);
42
43
// 设置比例尺
44
const x = d3.scaleLinear().range([0, width]);
45
const y = d3.scaleLinear().range([height, 0]);
46
47
// 定义折线生成器
48
const line = d3.line()
49
    .x(d => x(d.index)) // 使用第一列(index)作为X轴
50
    .y(d => y(d.observation)); // 使用观测值作为Y轴
51
52
// 初始化Y轴数据为第二列(observation1)
53
let currentY = "observation1";
54
55
// 读取CSV文件
56
d3.csv("data.csv").then(data => {
57 1 jun chen
    // 转换数据类型
58 17 jun chen
    data.forEach(d => {
59 18 jun chen
        d.index = + parseInt(d.index); // 第一列转换为数值
60
        d.observation1 = + parseFloat(d.observation1); // 第二列转换为数值
61
        d.observation2 = + parseFloat(d.observation2); // 第三列转换为数值
62 17 jun chen
    });
63
64
    // 设置比例尺的域
65
    x.domain(d3.extent(data, d => d.index)); // X轴范围为第一列的最小值和最大值
66
    y.domain([0, d3.max(data, d => Math.max(d.observation1, d.observation2))]); // Y轴范围为0到观测值的最大值
67
68
    // 绘制初始折线图(使用observation1)
69
    drawChart(data);
70
71
    // 添加按钮点击事件
72
    d3.select("#toggleButton").on("click", function() {
73
        // 切换Y轴数据
74
        currentY = currentY === "observation1" ? "observation2" : "observation1";
75
        // 更新按钮文本
76
        d3.select(this).text(currentY === "observation1" ? "Switch to Observation 2" : "Switch to Observation 1");
77
        // 重新绘制折线图
78
        drawChart(data);
79
    });
80
});
81
82
// 绘制折线图的函数
83
function drawChart(data) {
84
    // 移除旧的折线和点
85
    svg.selectAll(".line").remove();
86
    svg.selectAll(".dot").remove();
87
88
    // 更新折线生成器的Y值
89
    line.y(d => y(d[currentY]));
90
91
    // 添加折线
92
    svg.append("path")
93
        .datum(data)
94
        .attr("class", `line ${currentY === "observation1" ? "line1" : "line2"}`)
95
        .attr("d", line);
96
97
    // 添加点
98
    svg.selectAll(".dot")
99
        .data(data)
100
      .enter().append("circle")
101
        .attr("class", "dot")
102
        .attr("cx", d => x(d.index)) // 使用第一列(index)作为X轴
103
        .attr("cy", d => y(d[currentY])) // 使用当前观测值作为Y轴
104
        .attr("r", 5)
105
        .on("mouseover", function(event, d) {
106
            tooltip.transition()
107
                .duration(200)
108
                .style("opacity", .9);
109
            tooltip.html(`Index: ${d.index}<br>Observation: ${d[currentY]}<br>Label: ${d.label}`)
110
                .style("left", (event.pageX + 5) + "px")
111
                .style("top", (event.pageY - 28) + "px");
112
        })
113
        .on("mouseout", function(d) {
114
            tooltip.transition()
115
                .duration(500)
116
                .style("opacity", 0);
117
        });
118
119
    // 添加X轴
120
    svg.select(".x-axis").remove(); // 移除旧的X轴
121
    svg.append("g")
122
        .attr("class", "x-axis")
123
        .attr("transform", `translate(0,${height})`)
124
        .call(d3.axisBottom(x));
125
126
    // 添加Y轴
127
    svg.select(".y-axis").remove(); // 移除旧的Y轴
128
    svg.append("g")
129
        .attr("class", "y-axis")
130
        .call(d3.axisLeft(y));
131
}
132
133 1 jun chen
// 添加提示工具
134 17 jun chen
const tooltip = d3.select("body").append("div")
135
    .attr("class", "tooltip")
136
    .style("opacity", 0);
137 18 jun chen
``` 
138 17 jun chen
139
140 16 jun chen
### js d3 读入 csv 并绘制折线交互
141 15 jun chen
142 16 jun chen
{{collapse(show code...)
143 15 jun chen
``` 
144
145
// 设置图表的尺寸和边距
146
const margin = {top: 20, right: 30, bottom: 30, left: 40},
147
      width = 960 - margin.left - margin.right,
148
      height = 500 - margin.top - margin.bottom;
149
150
// 设置SVG的尺寸
151
const svg = d3.select("#chart").append("svg")
152
    .attr("width", width + margin.left + margin.right)
153
    .attr("height", height + margin.top + margin.bottom)
154
  .append("g")
155
    .attr("transform", `translate(${margin.left},${margin.top})`);
156
157
// 设置比例尺
158
const x = d3.scaleLinear().range([0, width]);
159
const y = d3.scaleLinear().range([height, 0]);
160
161
// 定义折线生成器
162
const line = d3.line()
163
    .x((d, i) => x(i))
164
    .y(d => y(d.observation));
165
166
// 读取CSV文件
167
d3.csv("data.csv").then(data => {
168
    // 转换数据类型
169
    data.forEach((d, i) => {
170
        d.observation = +d.observation;
171
        d.index = i;
172
    });
173
174
    // 设置比例尺的域
175
    x.domain([0, data.length - 1]);
176
    y.domain([0, d3.max(data, d => d.observation)]);
177
178
    // 添加折线
179
    svg.append("path")
180
        .datum(data)
181
        .attr("class", "line")
182
        .attr("d", line);
183
184
    // 添加点
185
    svg.selectAll(".dot")
186
        .data(data)
187
      .enter().append("circle")
188
        .attr("class", "dot")
189
        .attr("cx", (d, i) => x(i))
190
        .attr("cy", d => y(d.observation))
191
        .attr("r", 5)
192
        .on("mouseover", function(event, d) {
193
            tooltip.transition()
194
                .duration(200)
195
                .style("opacity", .9);
196
            tooltip.html(`Observation: ${d.observation}<br>Label: ${d.label}`)
197
                .style("left", (event.pageX + 5) + "px")
198
                .style("top", (event.pageY - 28) + "px");
199
        })
200
        .on("mouseout", function(d) {
201
            tooltip.transition()
202
                .duration(500)
203
                .style("opacity", 0);
204
        });
205
206
    // 添加X轴
207
    svg.append("g")
208
        .attr("transform", `translate(0,${height})`)
209
        .call(d3.axisBottom(x));
210
211
    // 添加Y轴
212
    svg.append("g")
213
        .call(d3.axisLeft(y));
214
});
215
216
// 添加提示工具
217
const tooltip = d3.select("body").append("div")
218
    .attr("class", "tooltip")
219
    .style("opacity", 0);
220
```
221
222
``` 
223
<!DOCTYPE html>
224
<html lang="en">
225
<head>
226
    <meta charset="UTF-8">
227
    <title>D3.js Line Chart</title>
228
    <script src="https://d3js.org/d3.v7.min.js"></script>
229
    <style>
230
        .line {
231
            fill: none;
232
            stroke: steelblue;
233
            stroke-width: 2px;
234
        }
235
        .dot {
236
            fill: steelblue;
237
            stroke: #fff;
238
        }
239
        .tooltip {
240
            position: absolute;
241
            text-align: center;
242
            width: 120px;
243
            height: auto;
244
            padding: 5px;
245
            font: 12px sans-serif;
246
            background: lightsteelblue;
247
            border: 0px;
248
            border-radius: 8px;
249
            pointer-events: none;
250
        }
251
    </style>
252
</head>
253
<body>
254
    <div id="chart"></div>
255
    <script src="script.js"></script>
256 1 jun chen
</body>
257 15 jun chen
</html>
258
259
```
260 16 jun chen
}}
261 15 jun chen
262 5 jun chen
### js d3 内嵌数据显示折线
263 4 jun chen
264 6 jun chen
{{collapse(show code...)
265
266 1 jun chen
``` 
267
<!DOCTYPE html>
268
<html lang="en">
269
<head>
270
    <meta charset="UTF-8">
271
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
272
    <title>分段填充折线图</title>
273
    <script src="https://d3js.org/d3.v7.min.js"></script>
274
    <style>
275
        .tooltip {
276
            position: absolute;
277
            background-color: rgba(255, 255, 255, 0.9);
278
            border: 1px solid #ccc;
279
            padding: 5px;
280
            font-size: 12px;
281
            pointer-events: none;
282
            opacity: 0;
283
        }
284
        .line {
285
            fill: none;
286
            stroke: black;
287
            stroke-width: 2;
288
        }
289
    </style>
290
</head>
291
<body>
292
    <div id="chart"></div>
293
    <div class="tooltip" id="tooltip"></div>
294
295
    <script>
296
        // 示例数据
297
        const data = [
298
            { time: "2025-01-01", value: 10, description: "说明1" },
299
            { time: "2025-01-02", value: 15, description: "说明2" },
300
            { time: "2025-01-03", value: 20, description: "说明3" },
301
            { time: "2025-01-04", value: 25, description: "说明4" },
302
            { time: "2025-01-05", value: 30, description: "说明5" },
303
            { time: "2025-01-06", value: 35, description: "说明6" },
304
            { time: "2025-01-07", value: 40, description: "说明7" },
305
            { time: "2025-01-08", value: 45, description: "说明8" },
306
            { time: "2025-01-09", value: 50, description: "说明9" },
307
            { time: "2025-01-10", value: 55, description: "说明10" },
308
            { time: "2025-01-11", value: 60, description: "说明11" },
309
            { time: "2025-01-12", value: 65, description: "说明12" },
310
            { time: "2025-01-13", value: 70, description: "说明13" },
311
            { time: "2025-01-14", value: 75, description: "说明14" },
312
            { time: "2025-01-15", value: 80, description: "说明15" },
313
            { time: "2025-01-16", value: 85, description: "说明16" },
314
            { time: "2025-01-17", value: 90, description: "说明17" },
315
            { time: "2025-01-18", value: 95, description: "说明18" },
316
            { time: "2025-01-19", value: 100, description: "说明19" },
317
            { time: "2025-01-20", value: 105, description: "说明20" }
318
        ];
319
320
        // 设置图表尺寸
321
        const margin = { top: 20, right: 30, bottom: 30, left: 40 };
322
        const width = 800 - margin.left - margin.right;
323
        const height = 400 - margin.top - margin.bottom;
324
325
        // 创建 SVG 容器
326
        const svg = d3.select("#chart")
327
            .append("svg")
328
            .attr("width", width + margin.left + margin.right)
329
            .attr("height", height + margin.top + margin.bottom)
330
            .append("g")
331
            .attr("transform", `translate(${margin.left},${margin.top})`);
332
333
        // 解析时间格式
334
        const parseTime = d3.timeParse("%Y-%m-%d");
335
336
        // 格式化数据
337
        data.forEach(d => {
338
            d.time = parseTime(d.time);
339
            d.value = +d.value;
340
        });
341
342
        // 设置比例尺
343
        const x = d3.scaleTime()
344
            .domain(d3.extent(data, d => d.time))
345
            .range([0, width]);
346
347
        const y = d3.scaleLinear()
348
            .domain([0, d3.max(data, d => d.value)])
349
            .range([height, 0]);
350
351
        // 添加 X 轴
352
        svg.append("g")
353
            .attr("transform", `translate(0,${height})`)
354
            .call(d3.axisBottom(x));
355
356
        // 添加 Y 轴
357
        svg.append("g")
358
            .call(d3.axisLeft(y));
359
360
        // 创建折线生成器
361
        const line = d3.line()
362
            .x(d => x(d.time))
363
            .y(d => y(d.value));
364
365
        // 绘制折线
366
        svg.append("path")
367
            .datum(data)
368
            .attr("class", "line")
369
            .attr("d", line);
370
371
        // 分段填充颜色
372
        const first10 = data.slice(0, 10);
373
        const last10 = data.slice(-10);
374
        const middle = data.slice(10, -10);
375
376
        // 填充前十个时间段的绿色区域
377
        svg.append("path")
378
            .datum(first10)
379
            .attr("fill", "green")
380
            .attr("opacity", 0.3)
381
            .attr("d", d3.area()
382
                .x(d => x(d.time))
383
                .y0(height)
384
                .y1(d => y(d.value))
385
            );
386
387
        // 填充中间时间段的蓝色区域
388
        svg.append("path")
389
            .datum(middle)
390
            .attr("fill", "blue")
391
            .attr("opacity", 0.3)
392
            .attr("d", d3.area()
393
                .x(d => x(d.time))
394
                .y0(height)
395
                .y1(d => y(d.value))
396
            );
397
398
        // 填充后十个时间段的红色区域
399
        svg.append("path")
400
            .datum(last10)
401
            .attr("fill", "red")
402
            .attr("opacity", 0.3)
403
            .attr("d", d3.area()
404
                .x(d => x(d.time))
405
                .y0(height)
406
                .y1(d => y(d.value))
407
            );
408
409
        // 添加悬停交互
410
        const tooltip = d3.select("#tooltip");
411
412
        svg.selectAll(".dot")
413
            .data(data)
414
            .enter()
415
            .append("circle")
416
            .attr("class", "dot")
417
            .attr("cx", d => x(d.time))
418
            .attr("cy", d => y(d.value))
419
            .attr("r", 5)
420
            .attr("fill", "steelblue")
421
            .on("mouseover", (event, d) => {
422
                tooltip.style("opacity", 1)
423
                    .html(`时间: ${d3.timeFormat("%Y-%m-%d")(d.time)}<br>数值: ${d.value}<br>说明: ${d.description}`)
424
                    .style("left", `${event.pageX + 5}px`)
425
                    .style("top", `${event.pageY - 20}px`);
426
            })
427
            .on("mouseout", () => {
428
                tooltip.style("opacity", 0);
429
            });
430
    </script>
431
</body>
432
</html>
433
```
434 6 jun chen
435
}}
436 1 jun chen
437 13 jun chen
### JavaScript + Plotly(纯前端实现)
438 1 jun chen
439 8 jun chen
{{collapse(show code...)
440
441 1 jun chen
```html
442
<!DOCTYPE html>
443
<html>
444
<head>
445
    <title>交互式图表</title>
446
    <!-- 引入 Plotly.js -->
447
    <script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script>
448
</head>
449
<body>
450
    <div id="chart"></div>
451
452
    <script>
453
        // 读取CSV文件(假设文件名为 data.csv)
454
        fetch('data.csv')
455
            .then(response => response.text())
456
            .then(csvText => {
457
                // 解析CSV数据
458
                const rows = csvText.split('\n');
459
                const x = [], y = [];
460
                rows.forEach((row, index) => {
461
                    if (index === 0) return; // 跳过标题行
462
                    const [xVal, yVal] = row.split(',');
463
                    x.push(parseFloat(xVal));
464
                    y.push(parseFloat(yVal));
465
                });
466
467
                // 绘制图表
468
                Plotly.newPlot('chart', [{
469
                    x: x,
470
                    y: y,
471
                    type: 'scatter',
472
                    mode: 'lines+markers',
473
                    marker: { color: 'blue' },
474
                    line: { shape: 'spline' }
475
                }], {
476
                    title: '交互式数据图表',
477
                    xaxis: { title: 'X轴' },
478
                    yaxis: { title: 'Y轴' },
479
                    hovermode: 'closest'
480
                });
481
            });
482
    </script>
483
</body>
484
</html>
485
```
486 8 jun chen
}}
487 1 jun chen
488
#### 使用步骤:
489
1. 将CSV文件命名为 `data.csv`,格式如下:
490
   ```csv
491
   x,y
492
   1,5
493
   2,3
494
   3,7
495
   4,2
496
   5,8
497
   ```
498
2. 将HTML文件和 `data.csv` 放在同一目录下,用浏览器打开HTML文件。
499
3. 效果:支持**缩放、悬停显示数值、拖拽平移**等交互。
500
501
---
502
503 14 jun chen
### Python + Plotly(生成独立HTML文件)
504 1 jun chen
#### 特点:适合Python用户,自动化生成图表文件。
505
506 9 jun chen
{{collapse(show code...)
507 1 jun chen
```python
508
import pandas as pd
509
import plotly.express as px
510
511
# 1. 读取CSV文件
512
df = pd.read_csv("data.csv")
513
514
# 2. 创建交互式图表
515
fig = px.line(
516
    df, x='x', y='y',
517
    title='Python生成的交互式图表',
518
    markers=True,  # 显示数据点
519
    line_shape='spline'  # 平滑曲线
520
)
521
522
# 3. 自定义悬停效果和样式
523
fig.update_traces(
524
    hoverinfo='x+y',  # 悬停显示x和y值
525
    line=dict(width=2, color='royalblue'),
526
    marker=dict(size=8, color='firebrick')
527
)
528
529
# 4. 保存为HTML文件
530
fig.write_html("interactive_chart.html")
531
```
532
}}
533
534
#### 使用步骤:
535
1. 安装依赖:
536
   ```bash
537
   pip install pandas plotly
538
   ```
539
2. 运行代码后,生成 `interactive_chart.html`,用浏览器打开即可看到图表。
540
541
### **进阶方案(可选)**
542
1. **动态数据加载**(JavaScript):
543 11 jun chen
544
{{collapse(View details...)
545 1 jun chen
   ```html
546
   <input type="file" id="csvFile" accept=".csv">
547
   <div id="chart"></div>
548
   <script>
549
     document.getElementById('csvFile').addEventListener('change', function(e) {
550
       const file = e.target.files[0];
551
       const reader = new FileReader();
552
       reader.onload = function(e) {
553
         // 解析并绘制图表(代码同方法一)
554
       };
555
       reader.readAsText(file);
556
     });
557
   </script>
558
   ```
559 11 jun chen
}}
560 1 jun chen
   - 用户可上传任意CSV文件,实时生成图表。
561
562 11 jun chen
563 1 jun chen
2. **添加控件**(Python + Dash):
564 11 jun chen
{{collapse(View details...)
565 1 jun chen
   ```python
566
   from dash import Dash, dcc, html
567
   import pandas as pd
568
   import plotly.express as px
569
570
   app = Dash(__name__)
571
   df = pd.read_csv("data.csv")
572
573
   app.layout = html.Div([
574
       dcc.Graph(
575
           id='live-chart',
576
           figure=px.scatter(df, x='x', y='y', title='Dash动态图表')
577
       ),
578
       html.Button('更新数据', id='update-button')
579
   ])
580
581
   if __name__ == '__main__':
582
       app.run_server(debug=True)
583
   ```
584 11 jun chen
}}
585
586 1 jun chen
   - 运行后访问 `http://localhost:8050`,支持动态交互和按钮触发操作。
587
588
---