Project

General

Profile

Web tech » History » Version 17

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