@internetstiftelsen/charts 0.7.1 → 0.9.0
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.
- package/README.md +54 -8
- package/area.d.ts +0 -1
- package/area.js +2 -19
- package/bar.d.ts +0 -1
- package/bar.js +64 -136
- package/base-chart.d.ts +54 -5
- package/base-chart.js +260 -73
- package/donut-chart.d.ts +7 -9
- package/donut-chart.js +51 -131
- package/gauge-chart.d.ts +18 -7
- package/gauge-chart.js +315 -106
- package/line.js +3 -25
- package/package.json +3 -1
- package/pie-chart.d.ts +7 -11
- package/pie-chart.js +30 -153
- package/radial-chart-base.d.ts +25 -0
- package/radial-chart-base.js +77 -0
- package/scale-utils.d.ts +3 -0
- package/scale-utils.js +14 -0
- package/types.d.ts +2 -0
- package/utils.d.ts +7 -0
- package/utils.js +24 -0
- package/word-cloud-chart.d.ts +32 -0
- package/word-cloud-chart.js +199 -0
- package/xy-chart.d.ts +8 -4
- package/xy-chart.js +127 -127
package/README.md
CHANGED
|
@@ -6,7 +6,9 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
|
|
|
6
6
|
|
|
7
7
|
- **Framework Agnostic** - Works with vanilla JS, React, Vue, Svelte, or any framework
|
|
8
8
|
- **Composable Architecture** - Build charts by composing components
|
|
9
|
-
- **Multiple Chart Types** - XYChart (lines, areas, bars), DonutChart, PieChart, and GaugeChart
|
|
9
|
+
- **Multiple Chart Types** - XYChart (lines, areas, bars), WordCloudChart, DonutChart, PieChart, and GaugeChart
|
|
10
|
+
- **Optional Gauge Animation** - Animate gauge value transitions with `gauge.animate`
|
|
11
|
+
- **Stacking Control** - Bar stacking modes with optional reversed visual series order
|
|
10
12
|
- **Flexible Scales** - Band, linear, time, and logarithmic scales
|
|
11
13
|
- **Auto Resize** - Built-in ResizeObserver handles responsive behavior
|
|
12
14
|
- **Responsive Policy** - Chart-level container-query overrides for theme and components
|
|
@@ -80,6 +82,37 @@ chart
|
|
|
80
82
|
chart.render('#chart-container');
|
|
81
83
|
```
|
|
82
84
|
|
|
85
|
+
## Word Cloud
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
import { WordCloudChart } from '@internetstiftelsen/charts/word-cloud-chart';
|
|
89
|
+
|
|
90
|
+
const data = [
|
|
91
|
+
{ word: 'internet', count: 96 },
|
|
92
|
+
{ word: 'social', count: 82 },
|
|
93
|
+
{ word: 'news', count: 75 },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const chart = new WordCloudChart({
|
|
97
|
+
data,
|
|
98
|
+
wordCloud: {
|
|
99
|
+
minValue: 5,
|
|
100
|
+
minWordLength: 3,
|
|
101
|
+
minFontSize: 3,
|
|
102
|
+
maxFontSize: 20,
|
|
103
|
+
padding: 1,
|
|
104
|
+
spiral: 'archimedean',
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
chart.render('#word-cloud');
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`minFontSize` and `maxFontSize` are percentages of the smaller plot-area
|
|
112
|
+
dimension and define the relative size range passed into `d3-cloud`. The chart
|
|
113
|
+
expects flat `{ word, count }` rows, aggregates duplicate words after trimming,
|
|
114
|
+
and maps theme typography and colors directly into the layout and rendered SVG.
|
|
115
|
+
|
|
83
116
|
## Export
|
|
84
117
|
|
|
85
118
|
`chart.export()` supports `svg`, `json`, `csv`, `xlsx`, `png`, `jpg`, and `pdf`.
|
|
@@ -103,12 +136,9 @@ It auto-detects grouped and normal (flat) table layouts.
|
|
|
103
136
|
import { toChartData } from '@internetstiftelsen/charts/utils';
|
|
104
137
|
import { XYChart } from '@internetstiftelsen/charts/xy-chart';
|
|
105
138
|
|
|
106
|
-
const data = toChartData(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
categoryKey: 'Category',
|
|
110
|
-
},
|
|
111
|
-
);
|
|
139
|
+
const data = toChartData('\t\tDaily\tWeekly\nAll users\tSegment A\t85%\t92%\n\tSegment B\t84%\t91%', {
|
|
140
|
+
categoryKey: 'Category',
|
|
141
|
+
});
|
|
112
142
|
|
|
113
143
|
const chart = new XYChart({ data });
|
|
114
144
|
chart.render('#chart-container');
|
|
@@ -117,16 +147,32 @@ chart.render('#chart-container');
|
|
|
117
147
|
The parser supports JSON-escaped string payloads and grouped carry-forward row
|
|
118
148
|
structure (blank first column on continuation rows).
|
|
119
149
|
|
|
150
|
+
Supported input shapes:
|
|
151
|
+
|
|
152
|
+
- Plain tab-delimited strings
|
|
153
|
+
- JSON-escaped string payloads
|
|
154
|
+
|
|
155
|
+
Auto-detection behavior:
|
|
156
|
+
|
|
157
|
+
- Grouped rows when a carry-forward group structure is present
|
|
158
|
+
- Flat rows when no grouped continuation rows are detected
|
|
159
|
+
|
|
160
|
+
Grouped parsing rules:
|
|
161
|
+
|
|
162
|
+
- Header row starts with two structural columns (`group`, `category`) before metrics
|
|
163
|
+
- Continuation rows leave the first column blank to inherit the previous group
|
|
164
|
+
- Blank separator rows are ignored
|
|
165
|
+
|
|
120
166
|
## Documentation
|
|
121
167
|
|
|
122
168
|
- [Getting Started](./docs/getting-started.md) - Installation, Vanilla JS, React integration
|
|
123
169
|
- [XYChart](./docs/xy-chart.md) - Line, area, and bar charts API
|
|
170
|
+
- [WordCloudChart](./docs/word-cloud-chart.md) - Word frequency visualization API
|
|
124
171
|
- [DonutChart](./docs/donut-chart.md) - Donut/pie charts API
|
|
125
172
|
- [PieChart](./docs/pie-chart.md) - Pie chart API
|
|
126
173
|
- [GaugeChart](./docs/gauge-chart.md) - Gauge chart API
|
|
127
174
|
- [Components](./docs/components.md) - Axes, Grid, Tooltip, Legend, Title
|
|
128
175
|
- [Theming](./docs/theming.md) - Colors, fonts, and styling
|
|
129
|
-
- [Advanced](./docs/advanced.md) - Scales, TypeScript, architecture, performance
|
|
130
176
|
|
|
131
177
|
## Browser Support
|
|
132
178
|
|
package/area.d.ts
CHANGED
|
@@ -19,7 +19,6 @@ export declare class Area implements ChartComponent<AreaConfigBase> {
|
|
|
19
19
|
constructor(config: AreaConfig);
|
|
20
20
|
getExportConfig(): AreaConfigBase;
|
|
21
21
|
createExportComponent(override?: Partial<AreaConfigBase>): ChartComponent;
|
|
22
|
-
private getScaledPosition;
|
|
23
22
|
private getStackValues;
|
|
24
23
|
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType: ScaleType | undefined, theme: ChartTheme, stackingContext?: AreaStackingContext, valueLabelLayer?: Selection<SVGGElement, undefined, null, undefined>): void;
|
|
25
24
|
private renderValueLabels;
|
package/area.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { area, curveBasis, curveCardinal, curveLinear, curveMonotoneX, curveNatural, curveStep, line, } from 'd3';
|
|
2
2
|
import { mergeDeep, sanitizeForCSS } from './utils.js';
|
|
3
|
+
import { getScalePosition } from './scale-utils.js';
|
|
3
4
|
const AREA_CURVE_FACTORIES = {
|
|
4
5
|
linear: curveLinear,
|
|
5
6
|
monotone: curveMonotoneX,
|
|
@@ -132,24 +133,6 @@ export class Area {
|
|
|
132
133
|
exportHooks: this.exportHooks,
|
|
133
134
|
});
|
|
134
135
|
}
|
|
135
|
-
getScaledPosition(data, key, scale, scaleType) {
|
|
136
|
-
const value = data[key];
|
|
137
|
-
let scaledValue;
|
|
138
|
-
switch (scaleType) {
|
|
139
|
-
case 'band':
|
|
140
|
-
scaledValue = String(value);
|
|
141
|
-
break;
|
|
142
|
-
case 'time':
|
|
143
|
-
scaledValue =
|
|
144
|
-
value instanceof Date ? value : new Date(String(value));
|
|
145
|
-
break;
|
|
146
|
-
case 'linear':
|
|
147
|
-
case 'log':
|
|
148
|
-
scaledValue = typeof value === 'number' ? value : Number(value);
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
return scale(scaledValue) || 0;
|
|
152
|
-
}
|
|
153
136
|
getStackValues(dataPoint, xKey, parseValue, stackingContext) {
|
|
154
137
|
const value = parseValue(dataPoint[this.dataKey]);
|
|
155
138
|
if (!stackingContext || stackingContext.mode === 'none') {
|
|
@@ -176,7 +159,7 @@ export class Area {
|
|
|
176
159
|
}
|
|
177
160
|
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext, valueLabelLayer) {
|
|
178
161
|
const getXPosition = (d) => {
|
|
179
|
-
const scaled =
|
|
162
|
+
const scaled = getScalePosition(x, d.data[xKey], xScaleType);
|
|
180
163
|
return scaled + (x.bandwidth ? x.bandwidth() / 2 : 0);
|
|
181
164
|
};
|
|
182
165
|
const hasValidValue = (d) => {
|
package/bar.d.ts
CHANGED
|
@@ -13,7 +13,6 @@ export declare class Bar implements ChartComponent<BarConfigBase> {
|
|
|
13
13
|
constructor(config: BarConfig);
|
|
14
14
|
getExportConfig(): BarConfigBase;
|
|
15
15
|
createExportComponent(override?: Partial<BarConfigBase>): ChartComponent;
|
|
16
|
-
private getScaledPosition;
|
|
17
16
|
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType?: ScaleType, theme?: ChartTheme, stackingContext?: BarStackingContext): void;
|
|
18
17
|
private renderVertical;
|
|
19
18
|
private renderHorizontal;
|
package/bar.js
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { getContrastTextColor, sanitizeForCSS, mergeDeep } from './utils.js';
|
|
2
|
+
import { getScalePosition } from './scale-utils.js';
|
|
3
|
+
const LABEL_INSET_DEFAULT = 4;
|
|
4
|
+
const LABEL_INSET_STACKED = 6;
|
|
5
|
+
const LABEL_MIN_PADDING_DEFAULT = 8;
|
|
6
|
+
const LABEL_MIN_PADDING_STACKED = 16;
|
|
7
|
+
const LAYER_LABEL_GAP = 6;
|
|
8
|
+
function getLabelSpacing(mode) {
|
|
9
|
+
const stacked = mode !== 'none';
|
|
10
|
+
return {
|
|
11
|
+
inset: stacked ? LABEL_INSET_STACKED : LABEL_INSET_DEFAULT,
|
|
12
|
+
minPadding: stacked
|
|
13
|
+
? LABEL_MIN_PADDING_STACKED
|
|
14
|
+
: LABEL_MIN_PADDING_DEFAULT,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
2
17
|
export class Bar {
|
|
3
18
|
constructor(config) {
|
|
4
19
|
Object.defineProperty(this, "type", {
|
|
@@ -74,24 +89,6 @@ export class Bar {
|
|
|
74
89
|
exportHooks: this.exportHooks,
|
|
75
90
|
});
|
|
76
91
|
}
|
|
77
|
-
getScaledPosition(data, key, scale, scaleType) {
|
|
78
|
-
const value = data[key];
|
|
79
|
-
let scaledValue;
|
|
80
|
-
switch (scaleType) {
|
|
81
|
-
case 'band':
|
|
82
|
-
scaledValue = String(value);
|
|
83
|
-
break;
|
|
84
|
-
case 'time':
|
|
85
|
-
scaledValue =
|
|
86
|
-
value instanceof Date ? value : new Date(String(value));
|
|
87
|
-
break;
|
|
88
|
-
case 'linear':
|
|
89
|
-
case 'log':
|
|
90
|
-
scaledValue = typeof value === 'number' ? value : Number(value);
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
return scale(scaledValue) || 0;
|
|
94
|
-
}
|
|
95
92
|
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext) {
|
|
96
93
|
if (this.orientation === 'vertical') {
|
|
97
94
|
this.renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext);
|
|
@@ -164,7 +161,7 @@ export class Bar {
|
|
|
164
161
|
.attr('class', `bar-${sanitizedKey}`)
|
|
165
162
|
.attr('data-index', (_, i) => i)
|
|
166
163
|
.attr('x', (d) => {
|
|
167
|
-
const xPos =
|
|
164
|
+
const xPos = getScalePosition(x, d[xKey], xScaleType);
|
|
168
165
|
return xScaleType === 'band'
|
|
169
166
|
? xPos + barOffset
|
|
170
167
|
: xPos - barWidth / 2;
|
|
@@ -287,7 +284,7 @@ export class Bar {
|
|
|
287
284
|
}
|
|
288
285
|
})
|
|
289
286
|
.attr('y', (d) => {
|
|
290
|
-
const yPos =
|
|
287
|
+
const yPos = getScalePosition(y, d[xKey], yScaleType);
|
|
291
288
|
return yScaleType === 'band'
|
|
292
289
|
? yPos + barOffset
|
|
293
290
|
: yPos - barHeight / 2;
|
|
@@ -374,7 +371,7 @@ export class Bar {
|
|
|
374
371
|
const categoryKey = String(d[xKey]);
|
|
375
372
|
const value = parseValue(d[this.dataKey]);
|
|
376
373
|
const valueText = String(value);
|
|
377
|
-
const xPos =
|
|
374
|
+
const xPos = getScalePosition(x, d[xKey], xScaleType);
|
|
378
375
|
const barColor = this.colorAdapter
|
|
379
376
|
? this.colorAdapter(d, i)
|
|
380
377
|
: this.fill;
|
|
@@ -426,76 +423,42 @@ export class Bar {
|
|
|
426
423
|
}
|
|
427
424
|
}
|
|
428
425
|
else {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const seriesIndex = stackingContext?.seriesIndex ?? 0;
|
|
433
|
-
const isTopLayer = seriesIndex === totalSeries - 1;
|
|
434
|
-
switch (insidePosition) {
|
|
435
|
-
case 'top':
|
|
436
|
-
// For layer mode + inside + top: check if there's enough space in the gap
|
|
437
|
-
if (seriesIndex < totalSeries - 1) {
|
|
438
|
-
// Calculate the gap to the next layer
|
|
439
|
-
const nextLayerScaleFactor = 1 - ((seriesIndex + 1) / totalSeries) * 0.7;
|
|
440
|
-
const nextLayerWidth = (this.maxBarSize
|
|
441
|
-
? Math.min(bandwidth, this.maxBarSize)
|
|
442
|
-
: bandwidth) * nextLayerScaleFactor;
|
|
443
|
-
const gap = (barWidth - nextLayerWidth) / 2;
|
|
444
|
-
const marginBelow = 4; // Minimum margin below text
|
|
445
|
-
if (boxHeight + marginBelow <= gap) {
|
|
446
|
-
labelY =
|
|
447
|
-
barTop + boxHeight / 2 + marginBelow;
|
|
448
|
-
}
|
|
449
|
-
else {
|
|
450
|
-
shouldRender = false;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
else {
|
|
454
|
-
// Top layer - use normal top position if it fits
|
|
455
|
-
labelY = barTop + boxHeight / 2 + 4;
|
|
456
|
-
if (boxHeight + 8 > barHeight) {
|
|
457
|
-
shouldRender = false;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
break;
|
|
461
|
-
case 'middle':
|
|
462
|
-
// For layer mode + inside + middle: only show what fits
|
|
463
|
-
labelY = (barTop + barBottom) / 2;
|
|
464
|
-
if (boxHeight + 8 > barHeight) {
|
|
465
|
-
shouldRender = false;
|
|
466
|
-
}
|
|
467
|
-
break;
|
|
468
|
-
case 'bottom':
|
|
469
|
-
// For layer mode + inside + bottom: only show for top layer if it fits
|
|
470
|
-
if (isTopLayer) {
|
|
471
|
-
labelY = barBottom - boxHeight / 2 - 4;
|
|
472
|
-
if (boxHeight + 8 > barHeight) {
|
|
473
|
-
shouldRender = false;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
else {
|
|
477
|
-
shouldRender = false;
|
|
478
|
-
}
|
|
479
|
-
break;
|
|
480
|
-
}
|
|
426
|
+
if (mode === 'layer' && insidePosition === 'bottom') {
|
|
427
|
+
// Bottom labels in layer mode are visually ambiguous and often hidden by overlap.
|
|
428
|
+
shouldRender = false;
|
|
481
429
|
}
|
|
482
430
|
else {
|
|
483
|
-
|
|
431
|
+
const { inset, minPadding } = getLabelSpacing(mode);
|
|
484
432
|
switch (insidePosition) {
|
|
485
433
|
case 'top':
|
|
486
|
-
labelY = barTop + boxHeight / 2 +
|
|
434
|
+
labelY = barTop + boxHeight / 2 + inset;
|
|
487
435
|
break;
|
|
488
436
|
case 'middle':
|
|
489
437
|
labelY = (barTop + barBottom) / 2;
|
|
490
438
|
break;
|
|
491
439
|
case 'bottom':
|
|
492
|
-
labelY = barBottom - boxHeight / 2 -
|
|
440
|
+
labelY = barBottom - boxHeight / 2 - inset;
|
|
493
441
|
break;
|
|
494
442
|
}
|
|
495
443
|
// Check if it fits inside the bar
|
|
496
|
-
if (boxHeight +
|
|
444
|
+
if (boxHeight + minPadding > barHeight) {
|
|
497
445
|
shouldRender = false;
|
|
498
446
|
}
|
|
447
|
+
// In layer mode, check the label fits in the visible gap
|
|
448
|
+
// above the next layer's bar top
|
|
449
|
+
if (shouldRender &&
|
|
450
|
+
mode === 'layer' &&
|
|
451
|
+
insidePosition === 'top' &&
|
|
452
|
+
stackingContext?.nextLayerData) {
|
|
453
|
+
const nextValue = stackingContext.nextLayerData.get(categoryKey);
|
|
454
|
+
if (nextValue !== undefined) {
|
|
455
|
+
const nextBarTop = y(nextValue) || 0;
|
|
456
|
+
const labelBottom = labelY + boxHeight / 2;
|
|
457
|
+
if (labelBottom + LAYER_LABEL_GAP > nextBarTop) {
|
|
458
|
+
shouldRender = false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
499
462
|
}
|
|
500
463
|
}
|
|
501
464
|
tempText.remove();
|
|
@@ -592,7 +555,7 @@ export class Bar {
|
|
|
592
555
|
const categoryKey = String(d[xKey]);
|
|
593
556
|
const value = parseValue(d[this.dataKey]);
|
|
594
557
|
const valueText = String(value);
|
|
595
|
-
const yPos =
|
|
558
|
+
const yPos = getScalePosition(y, d[xKey], yScaleType);
|
|
596
559
|
const barColor = this.colorAdapter
|
|
597
560
|
? this.colorAdapter(d, i)
|
|
598
561
|
: this.fill;
|
|
@@ -644,78 +607,43 @@ export class Bar {
|
|
|
644
607
|
}
|
|
645
608
|
}
|
|
646
609
|
else {
|
|
647
|
-
//
|
|
648
|
-
if (mode === 'layer') {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const isTopLayer = seriesIndex === totalSeries - 1;
|
|
652
|
-
// Map top/middle/bottom to start/middle/end for horizontal
|
|
653
|
-
switch (insidePosition) {
|
|
654
|
-
case 'top': // start of bar (left side)
|
|
655
|
-
// For layer mode + inside + top(left): check if there's enough space in the gap
|
|
656
|
-
if (seriesIndex < totalSeries - 1) {
|
|
657
|
-
// Calculate the gap to the next layer
|
|
658
|
-
const nextLayerScaleFactor = 1 - ((seriesIndex + 1) / totalSeries) * 0.7;
|
|
659
|
-
const nextLayerHeight = (this.maxBarSize
|
|
660
|
-
? Math.min(bandwidth, this.maxBarSize)
|
|
661
|
-
: bandwidth) * nextLayerScaleFactor;
|
|
662
|
-
const gap = (barHeight - nextLayerHeight) / 2;
|
|
663
|
-
const marginRight = 4; // Minimum margin to the right of text
|
|
664
|
-
if (boxWidth + marginRight <= gap) {
|
|
665
|
-
labelX =
|
|
666
|
-
barLeft + boxWidth / 2 + marginRight;
|
|
667
|
-
}
|
|
668
|
-
else {
|
|
669
|
-
shouldRender = false;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
else {
|
|
673
|
-
// Top layer - use normal left position if it fits
|
|
674
|
-
labelX = barLeft + boxWidth / 2 + 4;
|
|
675
|
-
if (boxWidth + 8 > barWidth) {
|
|
676
|
-
shouldRender = false;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
break;
|
|
680
|
-
case 'middle':
|
|
681
|
-
// For layer mode + inside + middle: only show what fits
|
|
682
|
-
labelX = (barLeft + barRight) / 2;
|
|
683
|
-
if (boxWidth + 8 > barWidth) {
|
|
684
|
-
shouldRender = false;
|
|
685
|
-
}
|
|
686
|
-
break;
|
|
687
|
-
case 'bottom': // end of bar (right side)
|
|
688
|
-
// For layer mode + inside + bottom(right): only show for top layer if it fits
|
|
689
|
-
if (isTopLayer) {
|
|
690
|
-
labelX = barRight - boxWidth / 2 - 4;
|
|
691
|
-
if (boxWidth + 8 > barWidth) {
|
|
692
|
-
shouldRender = false;
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
else {
|
|
696
|
-
shouldRender = false;
|
|
697
|
-
}
|
|
698
|
-
break;
|
|
699
|
-
}
|
|
610
|
+
// Map top/middle/bottom to start/middle/end for horizontal
|
|
611
|
+
if (mode === 'layer' && insidePosition === 'bottom') {
|
|
612
|
+
// Bottom labels in layer mode are visually ambiguous and often hidden by overlap.
|
|
613
|
+
shouldRender = false;
|
|
700
614
|
}
|
|
701
615
|
else {
|
|
702
|
-
|
|
703
|
-
// Map top/middle/bottom to start/middle/end for horizontal
|
|
616
|
+
const { inset, minPadding } = getLabelSpacing(mode);
|
|
704
617
|
switch (insidePosition) {
|
|
705
618
|
case 'top': // start of bar (left side)
|
|
706
|
-
labelX = barLeft + boxWidth / 2 +
|
|
619
|
+
labelX = barLeft + boxWidth / 2 + inset;
|
|
707
620
|
break;
|
|
708
621
|
case 'middle':
|
|
709
622
|
labelX = (barLeft + barRight) / 2;
|
|
710
623
|
break;
|
|
711
624
|
case 'bottom': // end of bar (right side)
|
|
712
|
-
labelX = barRight - boxWidth / 2 -
|
|
625
|
+
labelX = barRight - boxWidth / 2 - inset;
|
|
713
626
|
break;
|
|
714
627
|
}
|
|
715
628
|
// Check if it fits inside the bar
|
|
716
|
-
if (boxWidth +
|
|
629
|
+
if (boxWidth + minPadding > barWidth) {
|
|
717
630
|
shouldRender = false;
|
|
718
631
|
}
|
|
632
|
+
// In layer mode, check the label fits in the visible gap
|
|
633
|
+
// before the next layer's bar end
|
|
634
|
+
if (shouldRender &&
|
|
635
|
+
mode === 'layer' &&
|
|
636
|
+
insidePosition === 'top' &&
|
|
637
|
+
stackingContext?.nextLayerData) {
|
|
638
|
+
const nextValue = stackingContext.nextLayerData.get(categoryKey);
|
|
639
|
+
if (nextValue !== undefined) {
|
|
640
|
+
const nextBarRight = x(nextValue) || 0;
|
|
641
|
+
const labelRight = labelX + boxWidth / 2;
|
|
642
|
+
if (labelRight + LAYER_LABEL_GAP > nextBarRight) {
|
|
643
|
+
shouldRender = false;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
719
647
|
}
|
|
720
648
|
}
|
|
721
649
|
tempText.remove();
|
package/base-chart.d.ts
CHANGED
|
@@ -18,6 +18,34 @@ type ResponsiveOverrides = {
|
|
|
18
18
|
theme?: DeepPartial<ChartTheme>;
|
|
19
19
|
components: Map<ChartComponent, Record<string, unknown>>;
|
|
20
20
|
};
|
|
21
|
+
type BaseLayoutComponentsOptions = {
|
|
22
|
+
title?: boolean;
|
|
23
|
+
xAxis?: boolean;
|
|
24
|
+
yAxis?: boolean;
|
|
25
|
+
inlineLegend?: boolean;
|
|
26
|
+
};
|
|
27
|
+
type BaseExportComponentsOptions = {
|
|
28
|
+
title?: boolean;
|
|
29
|
+
grid?: boolean;
|
|
30
|
+
xAxis?: boolean;
|
|
31
|
+
yAxis?: boolean;
|
|
32
|
+
tooltip?: boolean;
|
|
33
|
+
legend?: boolean;
|
|
34
|
+
};
|
|
35
|
+
export type BaseLayoutContext = {
|
|
36
|
+
svg: Selection<SVGSVGElement, undefined, null, undefined>;
|
|
37
|
+
svgNode: SVGSVGElement;
|
|
38
|
+
};
|
|
39
|
+
export type BaseRenderContext = BaseLayoutContext & {
|
|
40
|
+
plotGroup: Selection<SVGGElement, undefined, null, undefined>;
|
|
41
|
+
plotArea: PlotAreaBounds;
|
|
42
|
+
};
|
|
43
|
+
type ComponentSlot<TComponent extends ChartComponent = ChartComponent> = {
|
|
44
|
+
type: TComponent['type'];
|
|
45
|
+
get: () => TComponent | null;
|
|
46
|
+
set: (component: TComponent | null) => void;
|
|
47
|
+
onRegister?: (component: TComponent) => void;
|
|
48
|
+
};
|
|
21
49
|
export type BaseChartConfig = {
|
|
22
50
|
data: ChartData;
|
|
23
51
|
theme?: Partial<ChartTheme>;
|
|
@@ -49,12 +77,14 @@ export declare abstract class BaseChart {
|
|
|
49
77
|
protected resizeObserver: ResizeObserver | null;
|
|
50
78
|
protected layoutManager: LayoutManager;
|
|
51
79
|
protected plotArea: PlotAreaBounds | null;
|
|
80
|
+
private readyPromise;
|
|
52
81
|
private disconnectedLegendContainer;
|
|
82
|
+
private renderThemeOverride;
|
|
53
83
|
protected constructor(config: BaseChartConfig);
|
|
54
84
|
/**
|
|
55
85
|
* Adds a component (axis, grid, tooltip, etc.) to the chart
|
|
56
86
|
*/
|
|
57
|
-
|
|
87
|
+
addChild(component: ChartComponent): this;
|
|
58
88
|
/**
|
|
59
89
|
* Renders the chart to the specified target element
|
|
60
90
|
*/
|
|
@@ -71,13 +101,18 @@ export declare abstract class BaseChart {
|
|
|
71
101
|
}): ResponsiveRenderContext;
|
|
72
102
|
private resolveBreakpointName;
|
|
73
103
|
private resolveRenderTheme;
|
|
74
|
-
private
|
|
104
|
+
private applyRenderTheme;
|
|
105
|
+
protected get renderTheme(): ChartTheme;
|
|
75
106
|
/**
|
|
76
107
|
* Get layout-aware components in order
|
|
77
108
|
* Override in subclasses to provide chart-specific components
|
|
78
109
|
*/
|
|
79
110
|
protected getLayoutComponents(): LayoutAwareComponent[];
|
|
111
|
+
protected getBaseLayoutComponents(options: BaseLayoutComponentsOptions): LayoutAwareComponent[];
|
|
80
112
|
protected getExportComponents(): ChartComponent[];
|
|
113
|
+
protected getOverrideableComponents(): ChartComponent[];
|
|
114
|
+
protected getBaseExportComponents(options: BaseExportComponentsOptions): ChartComponent[];
|
|
115
|
+
protected registerBaseComponent(component: ChartComponent): boolean;
|
|
81
116
|
protected collectExportOverrides(context: ExportRenderContext): Map<ChartComponent, Record<string, unknown>>;
|
|
82
117
|
protected collectResponsiveOverrides(context: ResponsiveRenderContext): ResponsiveOverrides;
|
|
83
118
|
protected runExportHooks(context: ExportHookContext): void;
|
|
@@ -85,7 +120,14 @@ export declare abstract class BaseChart {
|
|
|
85
120
|
private createOverrideComponents;
|
|
86
121
|
protected applyComponentOverrides(overrides: Map<ChartComponent, ChartComponent>): () => void;
|
|
87
122
|
private renderExportChart;
|
|
88
|
-
protected
|
|
123
|
+
protected renderTitle(svg: Selection<SVGSVGElement, undefined, null, undefined>): void;
|
|
124
|
+
protected renderInlineLegend(svg: Selection<SVGSVGElement, undefined, null, undefined>): void;
|
|
125
|
+
protected measureInlineLegend(svgNode: SVGSVGElement): void;
|
|
126
|
+
protected filterVisibleItems<T>(items: T[], getDataKey: (item: T) => string): T[];
|
|
127
|
+
protected validateSourceData(_data: ChartData): void;
|
|
128
|
+
protected syncDerivedState(_previousData?: DataItem[]): void;
|
|
129
|
+
protected initializeDataState(): void;
|
|
130
|
+
protected prepareLayout(context: BaseLayoutContext): void;
|
|
89
131
|
/**
|
|
90
132
|
* Setup ResizeObserver for automatic resize handling
|
|
91
133
|
*/
|
|
@@ -93,8 +135,10 @@ export declare abstract class BaseChart {
|
|
|
93
135
|
/**
|
|
94
136
|
* Subclasses must implement this method to define their rendering logic
|
|
95
137
|
*/
|
|
96
|
-
protected abstract renderChart(): void;
|
|
138
|
+
protected abstract renderChart(context: BaseRenderContext): void;
|
|
97
139
|
protected abstract createExportChart(): BaseChart;
|
|
140
|
+
protected setReadyPromise(promise: Promise<void>): void;
|
|
141
|
+
whenReady(): Promise<void>;
|
|
98
142
|
protected getLegendSeries(): LegendSeries[];
|
|
99
143
|
getLegendItems(): LegendItem[];
|
|
100
144
|
isLegendSeriesVisible(dataKey: string): boolean;
|
|
@@ -114,6 +158,11 @@ export declare abstract class BaseChart {
|
|
|
114
158
|
private resolveDisconnectedLegendHost;
|
|
115
159
|
private cleanupDisconnectedLegendContainer;
|
|
116
160
|
protected parseValue(value: unknown): number;
|
|
161
|
+
protected rerender(): void;
|
|
162
|
+
protected tryRegisterComponent(component: ChartComponent, slots: readonly ComponentSlot[]): boolean;
|
|
163
|
+
protected applySlotOverrides(overrides: Map<ChartComponent, ChartComponent>, slots: readonly ComponentSlot[]): () => void;
|
|
164
|
+
protected applyArrayComponentOverrides<TComponent extends ChartComponent>(components: TComponent[], overrides: Map<ChartComponent, ChartComponent>, isComponent: (component: ChartComponent) => component is TComponent): () => void;
|
|
165
|
+
private getBaseComponentSlots;
|
|
117
166
|
/**
|
|
118
167
|
* Exports the chart in the specified format
|
|
119
168
|
* @param format - The export format
|
|
@@ -131,7 +180,7 @@ export declare abstract class BaseChart {
|
|
|
131
180
|
private exportXLSX;
|
|
132
181
|
private exportImage;
|
|
133
182
|
private exportPDF;
|
|
134
|
-
protected exportSVG(options?: ExportOptions, formatForHooks?: VisualExportFormat): string
|
|
183
|
+
protected exportSVG(options?: ExportOptions, formatForHooks?: VisualExportFormat): Promise<string>;
|
|
135
184
|
protected exportJSON(): string;
|
|
136
185
|
}
|
|
137
186
|
export {};
|