datly 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1566 +0,0 @@
1
- // d3-selection
2
- import { select, selectAll } from "d3-selection";
3
- import {
4
- scaleLinear,
5
- scaleBand,
6
- scalePoint,
7
- scaleOrdinal,
8
- scaleSequential,
9
- } from "d3-scale";
10
- import {
11
- extent,
12
- max,
13
- min,
14
- sum,
15
- range,
16
- mean,
17
- deviation,
18
- histogram,
19
- quantile,
20
- } from "d3-array";
21
- import { axisBottom, axisLeft } from "d3-axis";
22
- import {
23
- schemeCategory10,
24
- interpolateRdYlBu,
25
- interpolateViridis,
26
- } from "d3-scale-chromatic";
27
- import { line, area, curveBasis, pie, arc } from "d3-shape";
28
-
29
- const d3 = {
30
- select,
31
- selectAll,
32
- scaleLinear,
33
- scaleBand,
34
- scalePoint,
35
- scaleOrdinal,
36
- scaleSequential,
37
- extent,
38
- max,
39
- min,
40
- sum,
41
- deviation,
42
- mean,
43
- quantile,
44
- histogram,
45
- range,
46
- axisBottom,
47
- axisLeft,
48
- line,
49
- area,
50
- curveBasis,
51
- pie,
52
- arc,
53
- schemeCategory10,
54
- interpolateViridis,
55
- interpolateRdYlBu,
56
- };
57
-
58
- class DataViz {
59
- constructor(containerId = "dataviz-container") {
60
- this.containerId = containerId;
61
- this.defaultWidth = 800;
62
- this.defaultHeight = 600;
63
- this.defaultMargin = { top: 40, right: 40, bottom: 60, left: 60 };
64
- this.colors = d3.schemeCategory10;
65
- }
66
-
67
- /**
68
- * Cria ou atualiza um container para visualização
69
- * @param {string} containerId - ID do elemento container
70
- * @param {number} width - Largura do SVG
71
- * @param {number} height - Altura do SVG
72
- */
73
- createContainer(
74
- containerId,
75
- width = this.defaultWidth,
76
- height = this.defaultHeight
77
- ) {
78
- const targetId = containerId || this.containerId;
79
- let container = d3.select(`#${targetId}`);
80
-
81
- if (container.empty()) {
82
- container = d3
83
- .select("body")
84
- .append("div")
85
- .attr("id", targetId)
86
- .style("margin", "20px");
87
- }
88
-
89
- container.selectAll("*").remove();
90
-
91
- const svg = container
92
- .append("svg")
93
- .attr("width", width)
94
- .attr("height", height)
95
- .style("background", "#fff")
96
- .style("border", "1px solid #ddd")
97
- .style("border-radius", "8px");
98
-
99
- return { container, svg };
100
- }
101
-
102
- // ============================================
103
- // HISTOGRAMA
104
- // ============================================
105
- histogram(data, options = {}) {
106
- const {
107
- title = "Histogram",
108
- xlabel = "Value",
109
- ylabel = "Frequency",
110
- bins = 30,
111
- color = "#4299e1",
112
- width = this.defaultWidth,
113
- height = this.defaultHeight,
114
- containerId = null,
115
- } = options;
116
-
117
- const { svg } = this.createContainer(containerId, width, height);
118
- const margin = this.defaultMargin;
119
- const innerWidth = width - margin.left - margin.right;
120
- const innerHeight = height - margin.top - margin.bottom;
121
-
122
- const g = svg
123
- .append("g")
124
- .attr("transform", `translate(${margin.left},${margin.top})`);
125
-
126
- const x = d3.scaleLinear().domain(d3.extent(data)).range([0, innerWidth]);
127
-
128
- const histogram = d3
129
- .histogram()
130
- .domain(x.domain())
131
- .thresholds(x.ticks(bins));
132
-
133
- const histData = histogram(data);
134
-
135
- const y = d3
136
- .scaleLinear()
137
- .domain([0, d3.max(histData, (d) => d.length)])
138
- .range([innerHeight, 0]);
139
-
140
- g.selectAll("rect")
141
- .data(histData)
142
- .join("rect")
143
- .attr("x", (d) => x(d.x0) + 1)
144
- .attr("width", (d) => Math.max(0, x(d.x1) - x(d.x0) - 2))
145
- .attr("y", (d) => y(d.length))
146
- .attr("height", (d) => innerHeight - y(d.length))
147
- .attr("fill", color)
148
- .attr("opacity", 0.8)
149
- .on("mouseover", function () {
150
- d3.select(this).attr("opacity", 1);
151
- })
152
- .on("mouseout", function () {
153
- d3.select(this).attr("opacity", 0.8);
154
- });
155
-
156
- g.append("g")
157
- .attr("transform", `translate(0,${innerHeight})`)
158
- .call(d3.axisBottom(x))
159
- .append("text")
160
- .attr("x", innerWidth / 2)
161
- .attr("y", 40)
162
- .attr("fill", "#000")
163
- .attr("text-anchor", "middle")
164
- .text(xlabel);
165
-
166
- g.append("g")
167
- .call(d3.axisLeft(y))
168
- .append("text")
169
- .attr("transform", "rotate(-90)")
170
- .attr("x", -innerHeight / 2)
171
- .attr("y", -40)
172
- .attr("fill", "#000")
173
- .attr("text-anchor", "middle")
174
- .text(ylabel);
175
-
176
- svg
177
- .append("text")
178
- .attr("x", width / 2)
179
- .attr("y", 20)
180
- .attr("text-anchor", "middle")
181
- .style("font-size", "16px")
182
- .style("font-weight", "bold")
183
- .text(title);
184
-
185
- return this;
186
- }
187
-
188
- // ============================================
189
- // BOX PLOT
190
- // ============================================
191
- boxplot(data, options = {}) {
192
- const {
193
- title = "Box Plot",
194
- xlabel = "Category",
195
- ylabel = "Value",
196
- labels = null,
197
- color = "#4299e1",
198
- width = this.defaultWidth,
199
- height = this.defaultHeight,
200
- containerId = null,
201
- } = options;
202
-
203
- const { svg } = this.createContainer(containerId, width, height);
204
- const margin = this.defaultMargin;
205
- const innerWidth = width - margin.left - margin.right;
206
- const innerHeight = height - margin.top - margin.bottom;
207
-
208
- const g = svg
209
- .append("g")
210
- .attr("transform", `translate(${margin.left},${margin.top})`);
211
-
212
- const datasets = Array.isArray(data[0]) ? data : [data];
213
- const categoryLabels = labels || datasets.map((_, i) => `Group ${i + 1}`);
214
-
215
- const boxData = datasets.map((dataset, i) => {
216
- const sorted = [...dataset].sort((a, b) => a - b);
217
- const q1 = d3.quantile(sorted, 0.25);
218
- const median = d3.quantile(sorted, 0.5);
219
- const q3 = d3.quantile(sorted, 0.75);
220
- const iqr = q3 - q1;
221
- const min = Math.max(d3.min(sorted), q1 - 1.5 * iqr);
222
- const max = Math.min(d3.max(sorted), q3 + 1.5 * iqr);
223
- const outliers = sorted.filter((d) => d < min || d > max);
224
-
225
- return { label: categoryLabels[i], q1, median, q3, min, max, outliers };
226
- });
227
-
228
- const x = d3
229
- .scaleBand()
230
- .domain(categoryLabels)
231
- .range([0, innerWidth])
232
- .padding(0.3);
233
-
234
- const y = d3
235
- .scaleLinear()
236
- .domain([d3.min(boxData, (d) => d.min), d3.max(boxData, (d) => d.max)])
237
- .nice()
238
- .range([innerHeight, 0]);
239
-
240
- boxData.forEach((d, i) => {
241
- const center = x(d.label) + x.bandwidth() / 2;
242
- const boxWidth = x.bandwidth();
243
-
244
- g.append("line")
245
- .attr("x1", center)
246
- .attr("x2", center)
247
- .attr("y1", y(d.min))
248
- .attr("y2", y(d.max))
249
- .attr("stroke", "#000")
250
- .attr("stroke-width", 1);
251
-
252
- g.append("rect")
253
- .attr("x", x(d.label))
254
- .attr("y", y(d.q3))
255
- .attr("width", boxWidth)
256
- .attr("height", y(d.q1) - y(d.q3))
257
- .attr("fill", color)
258
- .attr("stroke", "#000")
259
- .attr("opacity", 0.7);
260
-
261
- g.append("line")
262
- .attr("x1", x(d.label))
263
- .attr("x2", x(d.label) + boxWidth)
264
- .attr("y1", y(d.median))
265
- .attr("y2", y(d.median))
266
- .attr("stroke", "#000")
267
- .attr("stroke-width", 2);
268
-
269
- [d.min, d.max].forEach((val) => {
270
- g.append("line")
271
- .attr("x1", center - boxWidth / 4)
272
- .attr("x2", center + boxWidth / 4)
273
- .attr("y1", y(val))
274
- .attr("y2", y(val))
275
- .attr("stroke", "#000")
276
- .attr("stroke-width", 1);
277
- });
278
-
279
- d.outliers.forEach((outlier) => {
280
- g.append("circle")
281
- .attr("cx", center)
282
- .attr("cy", y(outlier))
283
- .attr("r", 3)
284
- .attr("fill", "red")
285
- .attr("opacity", 0.6);
286
- });
287
- });
288
-
289
- g.append("g")
290
- .attr("transform", `translate(0,${innerHeight})`)
291
- .call(d3.axisBottom(x))
292
- .append("text")
293
- .attr("x", innerWidth / 2)
294
- .attr("y", 40)
295
- .attr("fill", "#000")
296
- .attr("text-anchor", "middle")
297
- .text(xlabel);
298
-
299
- g.append("g")
300
- .call(d3.axisLeft(y))
301
- .append("text")
302
- .attr("transform", "rotate(-90)")
303
- .attr("x", -innerHeight / 2)
304
- .attr("y", -40)
305
- .attr("fill", "#000")
306
- .attr("text-anchor", "middle")
307
- .text(ylabel);
308
-
309
- svg
310
- .append("text")
311
- .attr("x", width / 2)
312
- .attr("y", 20)
313
- .attr("text-anchor", "middle")
314
- .style("font-size", "16px")
315
- .style("font-weight", "bold")
316
- .text(title);
317
-
318
- return this;
319
- }
320
-
321
- // ============================================
322
- // SCATTER PLOT
323
- // ============================================
324
- scatter(xData, yData, options = {}) {
325
- const {
326
- title = "Scatter Plot",
327
- xlabel = "X",
328
- ylabel = "Y",
329
- color = "#4299e1",
330
- size = 5,
331
- labels = null,
332
- width = this.defaultWidth,
333
- height = this.defaultHeight,
334
- containerId = null,
335
- } = options;
336
-
337
- const { svg } = this.createContainer(containerId, width, height);
338
- const margin = this.defaultMargin;
339
- const innerWidth = width - margin.left - margin.right;
340
- const innerHeight = height - margin.top - margin.bottom;
341
-
342
- const g = svg
343
- .append("g")
344
- .attr("transform", `translate(${margin.left},${margin.top})`);
345
-
346
- const data = xData.map((x, i) => ({
347
- x,
348
- y: yData[i],
349
- label: labels ? labels[i] : null,
350
- }));
351
-
352
- const x = d3
353
- .scaleLinear()
354
- .domain(d3.extent(xData))
355
- .nice()
356
- .range([0, innerWidth]);
357
-
358
- const y = d3
359
- .scaleLinear()
360
- .domain(d3.extent(yData))
361
- .nice()
362
- .range([innerHeight, 0]);
363
-
364
- const tooltip = d3
365
- .select("body")
366
- .append("div")
367
- .style("position", "absolute")
368
- .style("background", "rgba(0,0,0,0.8)")
369
- .style("color", "#fff")
370
- .style("padding", "8px")
371
- .style("border-radius", "4px")
372
- .style("font-size", "12px")
373
- .style("pointer-events", "none")
374
- .style("opacity", 0);
375
-
376
- g.selectAll("circle")
377
- .data(data)
378
- .join("circle")
379
- .attr("cx", (d) => x(d.x))
380
- .attr("cy", (d) => y(d.y))
381
- .attr("r", size)
382
- .attr("fill", color)
383
- .attr("opacity", 0.7)
384
- .on("mouseover", function (event, d) {
385
- d3.select(this)
386
- .attr("r", size * 1.5)
387
- .attr("opacity", 1);
388
- tooltip
389
- .style("opacity", 1)
390
- .html(
391
- `X: ${d.x.toFixed(2)}<br>Y: ${d.y.toFixed(2)}${
392
- d.label ? "<br>" + d.label : ""
393
- }`
394
- )
395
- .style("left", event.pageX + 10 + "px")
396
- .style("top", event.pageY - 10 + "px");
397
- })
398
- .on("mouseout", function () {
399
- d3.select(this).attr("r", size).attr("opacity", 0.7);
400
- tooltip.style("opacity", 0);
401
- });
402
-
403
- g.append("g")
404
- .attr("transform", `translate(0,${innerHeight})`)
405
- .call(d3.axisBottom(x))
406
- .append("text")
407
- .attr("x", innerWidth / 2)
408
- .attr("y", 40)
409
- .attr("fill", "#000")
410
- .attr("text-anchor", "middle")
411
- .text(xlabel);
412
-
413
- g.append("g")
414
- .call(d3.axisLeft(y))
415
- .append("text")
416
- .attr("transform", "rotate(-90)")
417
- .attr("x", -innerHeight / 2)
418
- .attr("y", -40)
419
- .attr("fill", "#000")
420
- .attr("text-anchor", "middle")
421
- .text(ylabel);
422
-
423
- svg
424
- .append("text")
425
- .attr("x", width / 2)
426
- .attr("y", 20)
427
- .attr("text-anchor", "middle")
428
- .style("font-size", "16px")
429
- .style("font-weight", "bold")
430
- .text(title);
431
-
432
- return this;
433
- }
434
-
435
- // ============================================
436
- // LINE CHART
437
- // ============================================
438
- line(xData, yData, options = {}) {
439
- const {
440
- title = "Line Chart",
441
- xlabel = "X",
442
- ylabel = "Y",
443
- color = "#4299e1",
444
- lineWidth = 2,
445
- showPoints = true,
446
- width = this.defaultWidth,
447
- height = this.defaultHeight,
448
- containerId = null,
449
- } = options;
450
-
451
- const { svg } = this.createContainer(containerId, width, height);
452
- const margin = this.defaultMargin;
453
- const innerWidth = width - margin.left - margin.right;
454
- const innerHeight = height - margin.top - margin.bottom;
455
-
456
- const g = svg
457
- .append("g")
458
- .attr("transform", `translate(${margin.left},${margin.top})`);
459
-
460
- const data = xData.map((x, i) => ({ x, y: yData[i] }));
461
-
462
- const x = d3.scaleLinear().domain(d3.extent(xData)).range([0, innerWidth]);
463
-
464
- const y = d3
465
- .scaleLinear()
466
- .domain(d3.extent(yData))
467
- .nice()
468
- .range([innerHeight, 0]);
469
-
470
- const line = d3
471
- .line()
472
- .x((d) => x(d.x))
473
- .y((d) => y(d.y));
474
-
475
- g.append("path")
476
- .datum(data)
477
- .attr("fill", "none")
478
- .attr("stroke", color)
479
- .attr("stroke-width", lineWidth)
480
- .attr("d", line);
481
-
482
- if (showPoints) {
483
- g.selectAll("circle")
484
- .data(data)
485
- .join("circle")
486
- .attr("cx", (d) => x(d.x))
487
- .attr("cy", (d) => y(d.y))
488
- .attr("r", 4)
489
- .attr("fill", color);
490
- }
491
-
492
- g.append("g")
493
- .attr("transform", `translate(0,${innerHeight})`)
494
- .call(d3.axisBottom(x))
495
- .append("text")
496
- .attr("x", innerWidth / 2)
497
- .attr("y", 40)
498
- .attr("fill", "#000")
499
- .attr("text-anchor", "middle")
500
- .text(xlabel);
501
-
502
- g.append("g")
503
- .call(d3.axisLeft(y))
504
- .append("text")
505
- .attr("transform", "rotate(-90)")
506
- .attr("x", -innerHeight / 2)
507
- .attr("y", -40)
508
- .attr("fill", "#000")
509
- .attr("text-anchor", "middle")
510
- .text(ylabel);
511
-
512
- svg
513
- .append("text")
514
- .attr("x", width / 2)
515
- .attr("y", 20)
516
- .attr("text-anchor", "middle")
517
- .style("font-size", "16px")
518
- .style("font-weight", "bold")
519
- .text(title);
520
-
521
- return this;
522
- }
523
-
524
- // ============================================
525
- // BAR CHART
526
- // ============================================
527
- bar(categories, values, options = {}) {
528
- const {
529
- title = "Bar Chart",
530
- xlabel = "Category",
531
- ylabel = "Value",
532
- color = "#4299e1",
533
- horizontal = false,
534
- width = this.defaultWidth,
535
- height = this.defaultHeight,
536
- containerId = null,
537
- } = options;
538
-
539
- const { svg } = this.createContainer(containerId, width, height);
540
- const margin = this.defaultMargin;
541
- const innerWidth = width - margin.left - margin.right;
542
- const innerHeight = height - margin.top - margin.bottom;
543
-
544
- const g = svg
545
- .append("g")
546
- .attr("transform", `translate(${margin.left},${margin.top})`);
547
-
548
- const data = categories.map((cat, i) => ({
549
- category: cat,
550
- value: values[i],
551
- }));
552
-
553
- if (!horizontal) {
554
- const x = d3
555
- .scaleBand()
556
- .domain(categories)
557
- .range([0, innerWidth])
558
- .padding(0.2);
559
-
560
- const y = d3
561
- .scaleLinear()
562
- .domain([0, d3.max(values)])
563
- .nice()
564
- .range([innerHeight, 0]);
565
-
566
- g.selectAll("rect")
567
- .data(data)
568
- .join("rect")
569
- .attr("x", (d) => x(d.category))
570
- .attr("y", (d) => y(d.value))
571
- .attr("width", x.bandwidth())
572
- .attr("height", (d) => innerHeight - y(d.value))
573
- .attr("fill", color)
574
- .attr("opacity", 0.8)
575
- .on("mouseover", function () {
576
- d3.select(this).attr("opacity", 1);
577
- })
578
- .on("mouseout", function () {
579
- d3.select(this).attr("opacity", 0.8);
580
- });
581
-
582
- g.append("g")
583
- .attr("transform", `translate(0,${innerHeight})`)
584
- .call(d3.axisBottom(x))
585
- .selectAll("text")
586
- .attr("transform", "rotate(-45)")
587
- .style("text-anchor", "end");
588
-
589
- g.append("g").call(d3.axisLeft(y));
590
- } else {
591
- const x = d3
592
- .scaleLinear()
593
- .domain([0, d3.max(values)])
594
- .nice()
595
- .range([0, innerWidth]);
596
-
597
- const y = d3
598
- .scaleBand()
599
- .domain(categories)
600
- .range([0, innerHeight])
601
- .padding(0.2);
602
-
603
- g.selectAll("rect")
604
- .data(data)
605
- .join("rect")
606
- .attr("x", 0)
607
- .attr("y", (d) => y(d.category))
608
- .attr("width", (d) => x(d.value))
609
- .attr("height", y.bandwidth())
610
- .attr("fill", color)
611
- .attr("opacity", 0.8)
612
- .on("mouseover", function () {
613
- d3.select(this).attr("opacity", 1);
614
- })
615
- .on("mouseout", function () {
616
- d3.select(this).attr("opacity", 0.8);
617
- });
618
-
619
- g.append("g")
620
- .attr("transform", `translate(0,${innerHeight})`)
621
- .call(d3.axisBottom(x));
622
-
623
- g.append("g").call(d3.axisLeft(y));
624
- }
625
-
626
- svg
627
- .append("text")
628
- .attr("x", width / 2)
629
- .attr("y", height - 10)
630
- .attr("text-anchor", "middle")
631
- .text(xlabel);
632
-
633
- svg
634
- .append("text")
635
- .attr("transform", "rotate(-90)")
636
- .attr("x", -height / 2)
637
- .attr("y", 15)
638
- .attr("text-anchor", "middle")
639
- .text(ylabel);
640
-
641
- svg
642
- .append("text")
643
- .attr("x", width / 2)
644
- .attr("y", 20)
645
- .attr("text-anchor", "middle")
646
- .style("font-size", "16px")
647
- .style("font-weight", "bold")
648
- .text(title);
649
-
650
- return this;
651
- }
652
-
653
- // ============================================
654
- // PIE CHART
655
- // ============================================
656
- pie(labels, values, options = {}) {
657
- const {
658
- title = "Pie Chart",
659
- width = this.defaultWidth,
660
- height = this.defaultHeight,
661
- showLabels = true,
662
- showPercentage = true,
663
- containerId = null,
664
- } = options;
665
-
666
- const { svg } = this.createContainer(containerId, width, height);
667
- const radius = Math.min(width, height) / 2 - 40;
668
-
669
- const g = svg
670
- .append("g")
671
- .attr("transform", `translate(${width / 2},${height / 2})`);
672
-
673
- const data = labels.map((label, i) => ({ label, value: values[i] }));
674
- const total = d3.sum(values);
675
-
676
- const color = d3.scaleOrdinal().domain(labels).range(this.colors);
677
-
678
- const pie = d3.pie().value((d) => d.value);
679
-
680
- const arc = d3.arc().innerRadius(0).outerRadius(radius);
681
-
682
- const labelArc = d3
683
- .arc()
684
- .innerRadius(radius * 0.7)
685
- .outerRadius(radius * 0.7);
686
-
687
- const arcs = g
688
- .selectAll("arc")
689
- .data(pie(data))
690
- .join("g")
691
- .attr("class", "arc");
692
-
693
- arcs
694
- .append("path")
695
- .attr("d", arc)
696
- .attr("fill", (d) => color(d.data.label))
697
- .attr("stroke", "#fff")
698
- .attr("stroke-width", 2)
699
- .attr("opacity", 0.8)
700
- .on("mouseover", function () {
701
- d3.select(this).attr("opacity", 1);
702
- })
703
- .on("mouseout", function () {
704
- d3.select(this).attr("opacity", 0.8);
705
- });
706
-
707
- if (showLabels) {
708
- arcs
709
- .append("text")
710
- .attr("transform", (d) => `translate(${labelArc.centroid(d)})`)
711
- .attr("text-anchor", "middle")
712
- .style("font-size", "12px")
713
- .style("font-weight", "bold")
714
- .style("fill", "#fff")
715
- .text((d) => {
716
- if (showPercentage) {
717
- const percentage = ((d.data.value / total) * 100).toFixed(1);
718
- return `${d.data.label}\n${percentage}%`;
719
- }
720
- return d.data.label;
721
- });
722
- }
723
-
724
- svg
725
- .append("text")
726
- .attr("x", width / 2)
727
- .attr("y", 20)
728
- .attr("text-anchor", "middle")
729
- .style("font-size", "16px")
730
- .style("font-weight", "bold")
731
- .text(title);
732
-
733
- return this;
734
- }
735
-
736
- // ============================================
737
- // HEATMAP
738
- // ============================================
739
- heatmap(matrix, options = {}) {
740
- const {
741
- title = "Heatmap",
742
- labels = null,
743
- colorScheme = "RdYlBu",
744
- showValues = true,
745
- width = this.defaultWidth,
746
- height = this.defaultHeight,
747
- containerId = null,
748
- } = options;
749
-
750
- const { svg } = this.createContainer(containerId, width, height);
751
- const margin = { top: 80, right: 40, bottom: 80, left: 80 };
752
- const innerWidth = width - margin.left - margin.right;
753
- const innerHeight = height - margin.top - margin.bottom;
754
-
755
- const g = svg
756
- .append("g")
757
- .attr("transform", `translate(${margin.left},${margin.top})`);
758
-
759
- const n = matrix.length;
760
- const rowLabels =
761
- labels || Array.from({ length: n }, (_, i) => `Var${i + 1}`);
762
- const colLabels =
763
- labels || Array.from({ length: n }, (_, i) => `Var${i + 1}`);
764
-
765
- const data = [];
766
- for (let i = 0; i < n; i++) {
767
- for (let j = 0; j < n; j++) {
768
- data.push({
769
- row: i,
770
- col: j,
771
- value: matrix[i][j],
772
- rowLabel: rowLabels[i],
773
- colLabel: colLabels[j],
774
- });
775
- }
776
- }
777
-
778
- const x = d3
779
- .scaleBand()
780
- .domain(colLabels)
781
- .range([0, innerWidth])
782
- .padding(0.05);
783
-
784
- const y = d3
785
- .scaleBand()
786
- .domain(rowLabels)
787
- .range([0, innerHeight])
788
- .padding(0.05);
789
-
790
- const colorScale = d3
791
- .scaleSequential()
792
- .domain([-1, 1])
793
- .interpolator(d3[`interpolate${colorScheme}`]);
794
-
795
- g.selectAll("rect")
796
- .data(data)
797
- .join("rect")
798
- .attr("x", (d) => x(d.colLabel))
799
- .attr("y", (d) => y(d.rowLabel))
800
- .attr("width", x.bandwidth())
801
- .attr("height", y.bandwidth())
802
- .attr("fill", (d) => colorScale(d.value))
803
- .attr("stroke", "#fff")
804
- .attr("stroke-width", 1);
805
-
806
- if (showValues) {
807
- g.selectAll("text.value")
808
- .data(data)
809
- .join("text")
810
- .attr("class", "value")
811
- .attr("x", (d) => x(d.colLabel) + x.bandwidth() / 2)
812
- .attr("y", (d) => y(d.rowLabel) + y.bandwidth() / 2)
813
- .attr("text-anchor", "middle")
814
- .attr("dominant-baseline", "middle")
815
- .style("font-size", "10px")
816
- .style("fill", (d) => (Math.abs(d.value) > 0.5 ? "#fff" : "#000"))
817
- .text((d) => d.value.toFixed(2));
818
- }
819
-
820
- g.append("g")
821
- .attr("transform", `translate(0,${innerHeight})`)
822
- .call(d3.axisBottom(x))
823
- .selectAll("text")
824
- .attr("transform", "rotate(-45)")
825
- .style("text-anchor", "end");
826
-
827
- g.append("g").call(d3.axisLeft(y));
828
-
829
- const legendWidth = 200;
830
- const legendHeight = 20;
831
- const legendG = svg
832
- .append("g")
833
- .attr(
834
- "transform",
835
- `translate(${width - margin.right - legendWidth},${margin.top - 40})`
836
- );
837
-
838
- const legendScale = d3
839
- .scaleLinear()
840
- .domain([-1, 1])
841
- .range([0, legendWidth]);
842
-
843
- const legendAxis = d3.axisBottom(legendScale).ticks(5);
844
-
845
- legendG
846
- .selectAll("rect")
847
- .data(d3.range(-1, 1, 0.01))
848
- .join("rect")
849
- .attr("x", (d) => legendScale(d))
850
- .attr("y", 0)
851
- .attr("width", legendWidth / 200)
852
- .attr("height", legendHeight)
853
- .attr("fill", (d) => colorScale(d));
854
-
855
- legendG
856
- .append("g")
857
- .attr("transform", `translate(0,${legendHeight})`)
858
- .call(legendAxis);
859
-
860
- svg
861
- .append("text")
862
- .attr("x", width / 2)
863
- .attr("y", 20)
864
- .attr("text-anchor", "middle")
865
- .style("font-size", "16px")
866
- .style("font-weight", "bold")
867
- .text(title);
868
-
869
- return this;
870
- }
871
-
872
- // ============================================
873
- // VIOLIN PLOT
874
- // ============================================
875
- violin(data, options = {}) {
876
- const {
877
- title = "Violin Plot",
878
- xlabel = "Category",
879
- ylabel = "Value",
880
- labels = null,
881
- color = "#4299e1",
882
- width = this.defaultWidth,
883
- height = this.defaultHeight,
884
- containerId = null,
885
- } = options;
886
-
887
- const { svg } = this.createContainer(containerId, width, height);
888
- const margin = this.defaultMargin;
889
- const innerWidth = width - margin.left - margin.right;
890
- const innerHeight = height - margin.top - margin.bottom;
891
-
892
- const g = svg
893
- .append("g")
894
- .attr("transform", `translate(${margin.left},${margin.top})`);
895
-
896
- const datasets = Array.isArray(data[0]) ? data : [data];
897
- const categoryLabels = labels || datasets.map((_, i) => `Group ${i + 1}`);
898
-
899
- const x = d3
900
- .scaleBand()
901
- .domain(categoryLabels)
902
- .range([0, innerWidth])
903
- .padding(0.3);
904
-
905
- const allValues = datasets.flat();
906
- const y = d3
907
- .scaleLinear()
908
- .domain(d3.extent(allValues))
909
- .nice()
910
- .range([innerHeight, 0]);
911
-
912
- const kde = (data, bandwidth = 0.5) => {
913
- const thresholds = y.ticks(50);
914
- return thresholds.map((t) => {
915
- const density = d3.mean(data, (d) => {
916
- return (
917
- Math.exp(-0.5 * Math.pow((d - t) / bandwidth, 2)) /
918
- (bandwidth * Math.sqrt(2 * Math.PI))
919
- );
920
- });
921
- return [t, density];
922
- });
923
- };
924
-
925
- datasets.forEach((dataset, i) => {
926
- const density = kde(dataset);
927
- const maxDensity = d3.max(density, (d) => d[1]);
928
-
929
- const xScale = d3
930
- .scaleLinear()
931
- .domain([0, maxDensity])
932
- .range([0, x.bandwidth() / 2]);
933
-
934
- const area = d3
935
- .area()
936
- .x0((d) => x(categoryLabels[i]) + x.bandwidth() / 2 - xScale(d[1]))
937
- .x1((d) => x(categoryLabels[i]) + x.bandwidth() / 2 + xScale(d[1]))
938
- .y((d) => y(d[0]))
939
- .curve(d3.curveBasis);
940
-
941
- g.append("path")
942
- .datum(density)
943
- .attr("fill", color)
944
- .attr("opacity", 0.6)
945
- .attr("stroke", "#000")
946
- .attr("stroke-width", 1)
947
- .attr("d", area);
948
- });
949
-
950
- g.append("g")
951
- .attr("transform", `translate(0,${innerHeight})`)
952
- .call(d3.axisBottom(x))
953
- .append("text")
954
- .attr("x", innerWidth / 2)
955
- .attr("y", 40)
956
- .attr("fill", "#000")
957
- .attr("text-anchor", "middle")
958
- .text(xlabel);
959
-
960
- g.append("g")
961
- .call(d3.axisLeft(y))
962
- .append("text")
963
- .attr("transform", "rotate(-90)")
964
- .attr("x", -innerHeight / 2)
965
- .attr("y", -40)
966
- .attr("fill", "#000")
967
- .attr("text-anchor", "middle")
968
- .text(ylabel);
969
-
970
- svg
971
- .append("text")
972
- .attr("x", width / 2)
973
- .attr("y", 20)
974
- .attr("text-anchor", "middle")
975
- .style("font-size", "16px")
976
- .style("font-weight", "bold")
977
- .text(title);
978
-
979
- return this;
980
- }
981
-
982
- // ============================================
983
- // QQ PLOT
984
- // ============================================
985
- qqplot(data, options = {}) {
986
- const {
987
- title = "Q-Q Plot",
988
- xlabel = "Theoretical Quantiles",
989
- ylabel = "Sample Quantiles",
990
- color = "#4299e1",
991
- width = this.defaultWidth,
992
- height = this.defaultHeight,
993
- containerId = null,
994
- } = options;
995
-
996
- const { svg } = this.createContainer(containerId, width, height);
997
- const margin = this.defaultMargin;
998
- const innerWidth = width - margin.left - margin.right;
999
- const innerHeight = height - margin.top - margin.bottom;
1000
-
1001
- const g = svg
1002
- .append("g")
1003
- .attr("transform", `translate(${margin.left},${margin.top})`);
1004
-
1005
- const sorted = [...data].sort((a, b) => a - b);
1006
- const n = sorted.length;
1007
-
1008
- const theoretical = sorted.map((_, i) => {
1009
- const p = (i + 0.5) / n;
1010
- return this.invNormalCDF(p);
1011
- });
1012
-
1013
- const qqData = theoretical.map((t, i) => ({ x: t, y: sorted[i] }));
1014
-
1015
- const x = d3
1016
- .scaleLinear()
1017
- .domain(d3.extent(theoretical))
1018
- .nice()
1019
- .range([0, innerWidth]);
1020
-
1021
- const y = d3
1022
- .scaleLinear()
1023
- .domain(d3.extent(sorted))
1024
- .nice()
1025
- .range([innerHeight, 0]);
1026
-
1027
- const minVal = Math.max(x.domain()[0], y.domain()[0]);
1028
- const maxVal = Math.min(x.domain()[1], y.domain()[1]);
1029
-
1030
- g.append("line")
1031
- .attr("x1", x(minVal))
1032
- .attr("y1", y(minVal))
1033
- .attr("x2", x(maxVal))
1034
- .attr("y2", y(maxVal))
1035
- .attr("stroke", "red")
1036
- .attr("stroke-width", 2)
1037
- .attr("stroke-dasharray", "5,5");
1038
-
1039
- g.selectAll("circle")
1040
- .data(qqData)
1041
- .join("circle")
1042
- .attr("cx", (d) => x(d.x))
1043
- .attr("cy", (d) => y(d.y))
1044
- .attr("r", 4)
1045
- .attr("fill", color)
1046
- .attr("opacity", 0.7);
1047
-
1048
- g.append("g")
1049
- .attr("transform", `translate(0,${innerHeight})`)
1050
- .call(d3.axisBottom(x))
1051
- .append("text")
1052
- .attr("x", innerWidth / 2)
1053
- .attr("y", 40)
1054
- .attr("fill", "#000")
1055
- .attr("text-anchor", "middle")
1056
- .text(xlabel);
1057
-
1058
- g.append("g")
1059
- .call(d3.axisLeft(y))
1060
- .append("text")
1061
- .attr("transform", "rotate(-90)")
1062
- .attr("x", -innerHeight / 2)
1063
- .attr("y", -40)
1064
- .attr("fill", "#000")
1065
- .attr("text-anchor", "middle")
1066
- .text(ylabel);
1067
-
1068
- svg
1069
- .append("text")
1070
- .attr("x", width / 2)
1071
- .attr("y", 20)
1072
- .attr("text-anchor", "middle")
1073
- .style("font-size", "16px")
1074
- .style("font-weight", "bold")
1075
- .text(title);
1076
-
1077
- return this;
1078
- }
1079
-
1080
- invNormalCDF(p) {
1081
- const a1 = -39.6968302866538;
1082
- const a2 = 220.946098424521;
1083
- const a3 = -275.928510446969;
1084
- const a4 = 138.357751867269;
1085
- const a5 = -30.6647980661472;
1086
- const a6 = 2.50662827745924;
1087
-
1088
- const b1 = -54.4760987982241;
1089
- const b2 = 161.585836858041;
1090
- const b3 = -155.698979859887;
1091
- const b4 = 66.8013118877197;
1092
- const b5 = -13.2806815528857;
1093
-
1094
- const c1 = -0.00778489400243029;
1095
- const c2 = -0.322396458041136;
1096
- const c3 = -2.40075827716184;
1097
- const c4 = -2.54973253934373;
1098
- const c5 = 4.37466414146497;
1099
- const c6 = 2.93816398269878;
1100
-
1101
- const d1 = 0.00778469570904146;
1102
- const d2 = 0.32246712907004;
1103
- const d3 = 2.445134137143;
1104
- const d4 = 3.75440866190742;
1105
-
1106
- const pLow = 0.02425;
1107
- const pHigh = 1 - pLow;
1108
-
1109
- let q, r, x;
1110
-
1111
- if (p < pLow) {
1112
- q = Math.sqrt(-2 * Math.log(p));
1113
- x =
1114
- (((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) /
1115
- ((((d1 * q + d2) * q + d3) * q + d4) * q + 1);
1116
- } else if (p <= pHigh) {
1117
- q = p - 0.5;
1118
- r = q * q;
1119
- x =
1120
- ((((((a1 * r + a2) * r + a3) * r + a4) * r + a5) * r + a6) * q) /
1121
- (((((b1 * r + b2) * r + b3) * r + b4) * r + b5) * r + 1);
1122
- } else {
1123
- q = Math.sqrt(-2 * Math.log(1 - p));
1124
- x =
1125
- -(((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) /
1126
- ((((d1 * q + d2) * q + d3) * q + d4) * q + 1);
1127
- }
1128
-
1129
- return x;
1130
- }
1131
-
1132
- // ============================================
1133
- // DENSITY PLOT
1134
- // ============================================
1135
- density(data, options = {}) {
1136
- const {
1137
- title = "Density Plot",
1138
- xlabel = "Value",
1139
- ylabel = "Density",
1140
- color = "#4299e1",
1141
- bandwidth = null,
1142
- width = this.defaultWidth,
1143
- height = this.defaultHeight,
1144
- containerId = null,
1145
- } = options;
1146
-
1147
- const { svg } = this.createContainer(containerId, width, height);
1148
- const margin = this.defaultMargin;
1149
- const innerWidth = width - margin.left - margin.right;
1150
- const innerHeight = height - margin.top - margin.bottom;
1151
-
1152
- const g = svg
1153
- .append("g")
1154
- .attr("transform", `translate(${margin.left},${margin.top})`);
1155
-
1156
- const bw =
1157
- bandwidth || 1.06 * d3.deviation(data) * Math.pow(data.length, -1 / 5);
1158
-
1159
- const extent = d3.extent(data);
1160
- const range = extent[1] - extent[0];
1161
- const xPoints = d3.range(
1162
- extent[0] - range * 0.1,
1163
- extent[1] + range * 0.1,
1164
- range / 200
1165
- );
1166
-
1167
- const densityData = xPoints.map((xi) => {
1168
- const density = d3.mean(data, (d) => {
1169
- return (
1170
- Math.exp(-0.5 * Math.pow((d - xi) / bw, 2)) /
1171
- (bw * Math.sqrt(2 * Math.PI))
1172
- );
1173
- });
1174
- return { x: xi, y: density };
1175
- });
1176
-
1177
- const x = d3
1178
- .scaleLinear()
1179
- .domain([xPoints[0], xPoints[xPoints.length - 1]])
1180
- .range([0, innerWidth]);
1181
-
1182
- const y = d3
1183
- .scaleLinear()
1184
- .domain([0, d3.max(densityData, (d) => d.y)])
1185
- .nice()
1186
- .range([innerHeight, 0]);
1187
-
1188
- const area = d3
1189
- .area()
1190
- .x((d) => x(d.x))
1191
- .y0(innerHeight)
1192
- .y1((d) => y(d.y))
1193
- .curve(d3.curveBasis);
1194
-
1195
- const line = d3
1196
- .line()
1197
- .x((d) => x(d.x))
1198
- .y((d) => y(d.y))
1199
- .curve(d3.curveBasis);
1200
-
1201
- g.append("path")
1202
- .datum(densityData)
1203
- .attr("fill", color)
1204
- .attr("opacity", 0.3)
1205
- .attr("d", area);
1206
-
1207
- g.append("path")
1208
- .datum(densityData)
1209
- .attr("fill", "none")
1210
- .attr("stroke", color)
1211
- .attr("stroke-width", 2)
1212
- .attr("d", line);
1213
-
1214
- g.append("g")
1215
- .attr("transform", `translate(0,${innerHeight})`)
1216
- .call(d3.axisBottom(x))
1217
- .append("text")
1218
- .attr("x", innerWidth / 2)
1219
- .attr("y", 40)
1220
- .attr("fill", "#000")
1221
- .attr("text-anchor", "middle")
1222
- .text(xlabel);
1223
-
1224
- g.append("g")
1225
- .call(d3.axisLeft(y))
1226
- .append("text")
1227
- .attr("transform", "rotate(-90)")
1228
- .attr("x", -innerHeight / 2)
1229
- .attr("y", -40)
1230
- .attr("fill", "#000")
1231
- .attr("text-anchor", "middle")
1232
- .text(ylabel);
1233
-
1234
- svg
1235
- .append("text")
1236
- .attr("x", width / 2)
1237
- .attr("y", 20)
1238
- .attr("text-anchor", "middle")
1239
- .style("font-size", "16px")
1240
- .style("font-weight", "bold")
1241
- .text(title);
1242
-
1243
- return this;
1244
- }
1245
-
1246
- // ============================================
1247
- // PARALLEL COORDINATES
1248
- // ============================================
1249
- parallel(data, dimensions, options = {}) {
1250
- const {
1251
- title = "Parallel Coordinates",
1252
- colors = null,
1253
- width = this.defaultWidth,
1254
- height = this.defaultHeight,
1255
- containerId = null,
1256
- } = options;
1257
-
1258
- const { svg } = this.createContainer(containerId, width, height);
1259
- const margin = { top: 60, right: 40, bottom: 40, left: 40 };
1260
- const innerWidth = width - margin.left - margin.right;
1261
- const innerHeight = height - margin.top - margin.bottom;
1262
-
1263
- const g = svg
1264
- .append("g")
1265
- .attr("transform", `translate(${margin.left},${margin.top})`);
1266
-
1267
- const y = {};
1268
- dimensions.forEach((dim) => {
1269
- y[dim] = d3
1270
- .scaleLinear()
1271
- .domain(d3.extent(data, (d) => d[dim]))
1272
- .range([innerHeight, 0]);
1273
- });
1274
-
1275
- const x = d3.scalePoint().domain(dimensions).range([0, innerWidth]);
1276
-
1277
- const line = d3.line();
1278
-
1279
- const path = (d) => {
1280
- return line(dimensions.map((dim) => [x(dim), y[dim](d[dim])]));
1281
- };
1282
-
1283
- const colorScale = colors
1284
- ? d3.scaleOrdinal().domain(d3.range(data.length)).range(colors)
1285
- : d3.scaleSequential(d3.interpolateViridis).domain([0, data.length]);
1286
-
1287
- g.selectAll("path.line")
1288
- .data(data)
1289
- .join("path")
1290
- .attr("class", "line")
1291
- .attr("d", path)
1292
- .attr("fill", "none")
1293
- .attr("stroke", (d, i) => colorScale(i))
1294
- .attr("opacity", 0.3)
1295
- .attr("stroke-width", 2)
1296
- .on("mouseover", function () {
1297
- d3.select(this).attr("opacity", 1).attr("stroke-width", 3);
1298
- })
1299
- .on("mouseout", function () {
1300
- d3.select(this).attr("opacity", 0.3).attr("stroke-width", 2);
1301
- });
1302
-
1303
- dimensions.forEach((dim) => {
1304
- const axis = g
1305
- .append("g")
1306
- .attr("transform", `translate(${x(dim)},0)`)
1307
- .call(d3.axisLeft(y[dim]));
1308
-
1309
- axis
1310
- .append("text")
1311
- .attr("y", -10)
1312
- .attr("text-anchor", "middle")
1313
- .style("fill", "#000")
1314
- .style("font-weight", "bold")
1315
- .text(dim);
1316
- });
1317
-
1318
- svg
1319
- .append("text")
1320
- .attr("x", width / 2)
1321
- .attr("y", 20)
1322
- .attr("text-anchor", "middle")
1323
- .style("font-size", "16px")
1324
- .style("font-weight", "bold")
1325
- .text(title);
1326
-
1327
- return this;
1328
- }
1329
-
1330
- // ============================================
1331
- // PAIR PLOT
1332
- // ============================================
1333
- pairplot(data, variables, options = {}) {
1334
- const {
1335
- title = "Pair Plot",
1336
- color = "#4299e1",
1337
- size = 3,
1338
- width = 900,
1339
- height = 900,
1340
- containerId = null,
1341
- } = options;
1342
-
1343
- const { svg } = this.createContainer(containerId, width, height);
1344
- const n = variables.length;
1345
- const padding = 60;
1346
- const cellSize = (Math.min(width, height) - padding * 2) / n;
1347
-
1348
- const scales = {};
1349
- variables.forEach((variable) => {
1350
- scales[variable] = d3
1351
- .scaleLinear()
1352
- .domain(d3.extent(data, (d) => d[variable]))
1353
- .range([0, cellSize - 20]);
1354
- });
1355
-
1356
- for (let i = 0; i < n; i++) {
1357
- for (let j = 0; j < n; j++) {
1358
- const xVar = variables[j];
1359
- const yVar = variables[i];
1360
-
1361
- const cellG = svg
1362
- .append("g")
1363
- .attr(
1364
- "transform",
1365
- `translate(${padding + j * cellSize},${padding + i * cellSize})`
1366
- );
1367
-
1368
- if (i === j) {
1369
- const values = data.map((d) => d[xVar]);
1370
- const histogram = d3
1371
- .histogram()
1372
- .domain(scales[xVar].domain())
1373
- .thresholds(20);
1374
-
1375
- const bins = histogram(values);
1376
- const yScale = d3
1377
- .scaleLinear()
1378
- .domain([0, d3.max(bins, (d) => d.length)])
1379
- .range([cellSize - 20, 0]);
1380
-
1381
- cellG
1382
- .selectAll("rect")
1383
- .data(bins)
1384
- .join("rect")
1385
- .attr("x", (d) => scales[xVar](d.x0))
1386
- .attr("y", (d) => yScale(d.length))
1387
- .attr("width", (d) => scales[xVar](d.x1) - scales[xVar](d.x0) - 1)
1388
- .attr("height", (d) => cellSize - 20 - yScale(d.length))
1389
- .attr("fill", color)
1390
- .attr("opacity", 0.7);
1391
- } else {
1392
- cellG
1393
- .selectAll("circle")
1394
- .data(data)
1395
- .join("circle")
1396
- .attr("cx", (d) => scales[xVar](d[xVar]))
1397
- .attr("cy", (d) => scales[yVar](d[yVar]))
1398
- .attr("r", size)
1399
- .attr("fill", color)
1400
- .attr("opacity", 0.5);
1401
- }
1402
-
1403
- if (i === n - 1) {
1404
- cellG
1405
- .append("g")
1406
- .attr("transform", `translate(0,${cellSize - 20})`)
1407
- .call(d3.axisBottom(scales[xVar]).ticks(3));
1408
- }
1409
-
1410
- if (j === 0) {
1411
- cellG.append("g").call(d3.axisLeft(scales[yVar]).ticks(3));
1412
- }
1413
-
1414
- if (i === n - 1) {
1415
- cellG
1416
- .append("text")
1417
- .attr("x", (cellSize - 20) / 2)
1418
- .attr("y", cellSize - 5)
1419
- .attr("text-anchor", "middle")
1420
- .style("font-size", "10px")
1421
- .text(xVar);
1422
- }
1423
-
1424
- if (j === 0) {
1425
- cellG
1426
- .append("text")
1427
- .attr("transform", "rotate(-90)")
1428
- .attr("x", -(cellSize - 20) / 2)
1429
- .attr("y", -25)
1430
- .attr("text-anchor", "middle")
1431
- .style("font-size", "10px")
1432
- .text(yVar);
1433
- }
1434
- }
1435
- }
1436
-
1437
- svg
1438
- .append("text")
1439
- .attr("x", width / 2)
1440
- .attr("y", 30)
1441
- .attr("text-anchor", "middle")
1442
- .style("font-size", "16px")
1443
- .style("font-weight", "bold")
1444
- .text(title);
1445
-
1446
- return this;
1447
- }
1448
-
1449
- // ============================================
1450
- // MULTI-LINE CHART
1451
- // ============================================
1452
- multiline(series, options = {}) {
1453
- const {
1454
- title = "Multi-Line Chart",
1455
- xlabel = "X",
1456
- ylabel = "Y",
1457
- legend = true,
1458
- width = this.defaultWidth,
1459
- height = this.defaultHeight,
1460
- containerId = null,
1461
- } = options;
1462
-
1463
- const { svg } = this.createContainer(containerId, width, height);
1464
- const margin = this.defaultMargin;
1465
- const innerWidth = width - margin.left - margin.right;
1466
- const innerHeight = height - margin.top - margin.bottom;
1467
-
1468
- const g = svg
1469
- .append("g")
1470
- .attr("transform", `translate(${margin.left},${margin.top})`);
1471
-
1472
- const allX = series.flatMap((s) => s.data.map((d) => d.x));
1473
- const allY = series.flatMap((s) => s.data.map((d) => d.y));
1474
-
1475
- const x = d3.scaleLinear().domain(d3.extent(allX)).range([0, innerWidth]);
1476
-
1477
- const y = d3
1478
- .scaleLinear()
1479
- .domain(d3.extent(allY))
1480
- .nice()
1481
- .range([innerHeight, 0]);
1482
-
1483
- const color = d3
1484
- .scaleOrdinal()
1485
- .domain(series.map((s) => s.name))
1486
- .range(this.colors);
1487
-
1488
- const line = d3
1489
- .line()
1490
- .x((d) => x(d.x))
1491
- .y((d) => y(d.y));
1492
-
1493
- series.forEach((s) => {
1494
- g.append("path")
1495
- .datum(s.data)
1496
- .attr("fill", "none")
1497
- .attr("stroke", color(s.name))
1498
- .attr("stroke-width", 2)
1499
- .attr("d", line);
1500
- });
1501
-
1502
- g.append("g")
1503
- .attr("transform", `translate(0,${innerHeight})`)
1504
- .call(d3.axisBottom(x))
1505
- .append("text")
1506
- .attr("x", innerWidth / 2)
1507
- .attr("y", 40)
1508
- .attr("fill", "#000")
1509
- .attr("text-anchor", "middle")
1510
- .text(xlabel);
1511
-
1512
- g.append("g")
1513
- .call(d3.axisLeft(y))
1514
- .append("text")
1515
- .attr("transform", "rotate(-90)")
1516
- .attr("x", -innerHeight / 2)
1517
- .attr("y", -40)
1518
- .attr("fill", "#000")
1519
- .attr("text-anchor", "middle")
1520
- .text(ylabel);
1521
-
1522
- if (legend) {
1523
- const legendG = svg
1524
- .append("g")
1525
- .attr(
1526
- "transform",
1527
- `translate(${width - margin.right - 100},${margin.top})`
1528
- );
1529
-
1530
- series.forEach((s, i) => {
1531
- const legendItem = legendG
1532
- .append("g")
1533
- .attr("transform", `translate(0,${i * 25})`);
1534
-
1535
- legendItem
1536
- .append("line")
1537
- .attr("x1", 0)
1538
- .attr("x2", 30)
1539
- .attr("y1", 10)
1540
- .attr("y2", 10)
1541
- .attr("stroke", color(s.name))
1542
- .attr("stroke-width", 2);
1543
-
1544
- legendItem
1545
- .append("text")
1546
- .attr("x", 35)
1547
- .attr("y", 15)
1548
- .style("font-size", "12px")
1549
- .text(s.name);
1550
- });
1551
- }
1552
-
1553
- svg
1554
- .append("text")
1555
- .attr("x", width / 2)
1556
- .attr("y", 20)
1557
- .attr("text-anchor", "middle")
1558
- .style("font-size", "16px")
1559
- .style("font-weight", "bold")
1560
- .text(title);
1561
-
1562
- return this;
1563
- }
1564
- }
1565
-
1566
- export default DataViz;