@internetstiftelsen/charts 0.10.1 → 0.11.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 +64 -0
- package/dist/area.d.ts +9 -1
- package/dist/area.js +174 -38
- package/dist/bar.d.ts +9 -1
- package/dist/bar.js +130 -47
- package/dist/base-chart.js +11 -1
- package/dist/donut-chart.js +3 -18
- package/dist/gauge-chart.d.ts +3 -4
- package/dist/gauge-chart.js +7 -53
- package/dist/lazy-mount.d.ts +13 -0
- package/dist/lazy-mount.js +90 -0
- package/dist/line.d.ts +9 -1
- package/dist/line.js +141 -23
- package/dist/pie-chart.js +5 -29
- package/dist/radial-chart-base.d.ts +4 -3
- package/dist/radial-chart-base.js +27 -12
- package/dist/scatter.d.ts +5 -1
- package/dist/scatter.js +89 -8
- package/dist/theme.js +17 -0
- package/dist/tooltip.d.ts +55 -3
- package/dist/tooltip.js +950 -137
- package/dist/types.d.ts +20 -0
- package/dist/xy-animation.d.ts +3 -0
- package/dist/xy-animation.js +2 -0
- package/dist/xy-chart.d.ts +11 -1
- package/dist/xy-chart.js +107 -10
- package/dist/xy-motion/config.d.ts +2 -0
- package/dist/xy-motion/config.js +177 -0
- package/dist/xy-motion/driver.d.ts +9 -0
- package/dist/xy-motion/driver.js +10 -0
- package/dist/xy-motion/helpers.d.ts +17 -0
- package/dist/xy-motion/helpers.js +105 -0
- package/dist/xy-motion/live-state.d.ts +8 -0
- package/dist/xy-motion/live-state.js +240 -0
- package/dist/xy-motion/noop-xy-motion-driver.d.ts +9 -0
- package/dist/xy-motion/noop-xy-motion-driver.js +15 -0
- package/dist/xy-motion/types.d.ts +85 -0
- package/dist/xy-motion/types.js +1 -0
- package/dist/xy-motion/xy-motion-driver.d.ts +19 -0
- package/dist/xy-motion/xy-motion-driver.js +130 -0
- package/docs/components.md +36 -0
- package/docs/getting-started.md +35 -0
- package/docs/theming.md +14 -0
- package/docs/xy-chart.md +67 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
|
|
|
11
11
|
- **Divergent Bar Support** - Bar charts automatically render from zero and diverge around `0` for mixed positive/negative values
|
|
12
12
|
- **Mirrored Bar Sides** - Horizontal bars can mirror a series to the left for population-pyramid style charts without changing source data
|
|
13
13
|
- **Custom Value Labels** - XY, pie, and donut charts support optional on-chart labels with custom formatters
|
|
14
|
+
- **Optional XY Animation** - Animate XY series on first render and `chart.update(...)` with `animate`
|
|
14
15
|
- **Optional Gauge Animation** - Animate gauge value transitions with `gauge.animate`
|
|
15
16
|
- **Stacking Control** - Bar stacking modes with optional reversed visual series order
|
|
16
17
|
- **Axis Direction Control** - Use `scales.x.reverse` / `scales.y.reverse` to flip an axis when needed
|
|
@@ -18,6 +19,7 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
|
|
|
18
19
|
- **Explicit or Responsive Sizing** - Set top-level `width`/`height` or let the container drive size
|
|
19
20
|
- **Auto Resize** - Built-in ResizeObserver handles responsive behavior
|
|
20
21
|
- **Responsive Policy** - Chart-level container-query overrides for theme and components
|
|
22
|
+
- **Lazy Mount Utility** - Observe a container and defer chart imports until it approaches the viewport
|
|
21
23
|
- **Type Safe** - Written in TypeScript with full type definitions
|
|
22
24
|
- **Data Validation** - Built-in validation with helpful error messages
|
|
23
25
|
- **Auto Colors** - Smart color palette with sensible defaults
|
|
@@ -110,6 +112,68 @@ from the render container.
|
|
|
110
112
|
Theme overrides are deep-partial, so nested overrides like
|
|
111
113
|
`theme.axis.fontSize` preserve the rest of the theme defaults.
|
|
112
114
|
|
|
115
|
+
## XY Animation
|
|
116
|
+
|
|
117
|
+
Enable `animate` on `XYChart` when you want series marks to animate on the
|
|
118
|
+
first render and on later `chart.update(...)` calls.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const chart = new XYChart({
|
|
122
|
+
data,
|
|
123
|
+
animate: {
|
|
124
|
+
duration: 700,
|
|
125
|
+
easing: 'ease-in-out',
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
chart
|
|
130
|
+
.addChild(new XAxis({ dataKey: 'month' }))
|
|
131
|
+
.addChild(new YAxis())
|
|
132
|
+
.addChild(new Line({ dataKey: 'value' }));
|
|
133
|
+
|
|
134
|
+
chart.render('#chart-container');
|
|
135
|
+
|
|
136
|
+
await chart.whenReady();
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Animation is off by default, applies to XY series marks only, and visual
|
|
140
|
+
exports always render the final static state.
|
|
141
|
+
|
|
142
|
+
## Lazy Loading
|
|
143
|
+
|
|
144
|
+
Use `mountChartWhenVisible` when you want the page to wait until a chart is
|
|
145
|
+
near the viewport before importing chart code and rendering it.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { mountChartWhenVisible } from '@internetstiftelsen/charts/lazy-mount';
|
|
149
|
+
|
|
150
|
+
const lazyChart = mountChartWhenVisible(
|
|
151
|
+
'#chart-container',
|
|
152
|
+
async (container) => {
|
|
153
|
+
const [{ XYChart }, { Line }, { XAxis }, { YAxis }] = await Promise.all([import('@internetstiftelsen/charts/xy-chart'), import('@internetstiftelsen/charts/line'), import('@internetstiftelsen/charts/x-axis'), import('@internetstiftelsen/charts/y-axis')]);
|
|
154
|
+
|
|
155
|
+
const chart = new XYChart({ data });
|
|
156
|
+
chart
|
|
157
|
+
.addChild(new XAxis({ dataKey: 'month' }))
|
|
158
|
+
.addChild(new YAxis())
|
|
159
|
+
.addChild(new Line({ dataKey: 'value' }));
|
|
160
|
+
|
|
161
|
+
chart.render(container);
|
|
162
|
+
return chart;
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
rootMargin: '240px 0px',
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Optional: preload manually before it scrolls into view
|
|
170
|
+
await lazyChart.load();
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The utility is intentionally small: it observes the container, calls your async
|
|
174
|
+
factory once, and gives you `load()` plus `destroy()` so higher-level DOM
|
|
175
|
+
conventions such as `data-chart` scanners can build on top of it.
|
|
176
|
+
|
|
113
177
|
## Chart Groups
|
|
114
178
|
|
|
115
179
|
Use `ChartGroup` when you want to combine existing charts into one layout while
|
package/dist/area.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
2
|
import type { AreaConfig, AreaCurveType, AreaConfigBase, AreaStackingContext, ChartTheme, D3Scale, DataItem, ExportHooks, LineValueLabelConfig, ScaleType } from './types.js';
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
|
+
import type { XYAreaAnimationContext, XYAreaPointSnapshot, XYSeriesRenderResult } from './xy-motion/types.js';
|
|
4
5
|
export declare class Area implements ChartComponent<AreaConfigBase> {
|
|
5
6
|
readonly type: "area";
|
|
6
7
|
readonly dataKey: string;
|
|
@@ -20,8 +21,15 @@ export declare class Area implements ChartComponent<AreaConfigBase> {
|
|
|
20
21
|
getExportConfig(): AreaConfigBase;
|
|
21
22
|
createExportComponent(override?: Partial<AreaConfigBase>): ChartComponent<AreaConfigBase>;
|
|
22
23
|
private getStackValues;
|
|
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
|
|
24
|
+
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>, animation?: XYAreaAnimationContext): XYSeriesRenderResult<XYAreaPointSnapshot>;
|
|
25
|
+
private buildAreaData;
|
|
26
|
+
private buildAnimatedAreaData;
|
|
27
|
+
private createSnapshot;
|
|
28
|
+
private renderAreaFill;
|
|
24
29
|
private renderLinePath;
|
|
30
|
+
private getInitialAreaPathValue;
|
|
31
|
+
private getInitialLinePathValue;
|
|
32
|
+
private createRevealTransitions;
|
|
25
33
|
private renderPoints;
|
|
26
34
|
private renderValueLabels;
|
|
27
35
|
}
|
package/dist/area.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { area, curveBasis, curveCardinal, curveLinear, curveMonotoneX, curveNatural, curveStep, line, } from 'd3';
|
|
2
2
|
import { mergeDeep, sanitizeForCSS } from './utils.js';
|
|
3
3
|
import { getScalePosition } from './scale-utils.js';
|
|
4
|
+
import { buildXYDatumSnapshotKeys, createTransitionCompletionPromise, createLeftToRightRevealTransition, getEnterStaggerTiming, } from './xy-motion/helpers.js';
|
|
4
5
|
const AREA_CURVE_FACTORIES = {
|
|
5
6
|
linear: curveLinear,
|
|
6
7
|
monotone: curveMonotoneX,
|
|
@@ -157,9 +158,9 @@ export class Area {
|
|
|
157
158
|
y1: cumulative + value,
|
|
158
159
|
};
|
|
159
160
|
}
|
|
160
|
-
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext, valueLabelLayer) {
|
|
161
|
-
const getXPosition = (
|
|
162
|
-
const scaled = getScalePosition(x,
|
|
161
|
+
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext, valueLabelLayer, animation) {
|
|
162
|
+
const getXPosition = (dataPoint) => {
|
|
163
|
+
const scaled = getScalePosition(x, dataPoint[xKey], xScaleType);
|
|
163
164
|
return scaled + (x.bandwidth ? x.bandwidth() / 2 : 0);
|
|
164
165
|
};
|
|
165
166
|
const hasValidValue = (d) => {
|
|
@@ -169,80 +170,215 @@ export class Area {
|
|
|
169
170
|
}
|
|
170
171
|
return Number.isFinite(parseValue(value));
|
|
171
172
|
};
|
|
172
|
-
const
|
|
173
|
-
|
|
173
|
+
const snapshotKeys = buildXYDatumSnapshotKeys(data, xKey);
|
|
174
|
+
const areaData = this.buildAreaData(data, xKey, snapshotKeys, y, parseValue, stackingContext, getXPosition, hasValidValue);
|
|
175
|
+
const animatedAreaData = this.buildAnimatedAreaData(areaData, animation);
|
|
176
|
+
const curveFactory = AREA_CURVE_FACTORIES[this.curve] || curveLinear;
|
|
177
|
+
const sanitizedKey = sanitizeForCSS(this.dataKey);
|
|
178
|
+
const transitions = this.renderAreaFill(plotGroup, areaData, animatedAreaData, curveFactory, sanitizedKey, animation);
|
|
179
|
+
const snapshot = this.createSnapshot(areaData);
|
|
180
|
+
if (this.showLine) {
|
|
181
|
+
const lineTransitions = this.renderLinePath(plotGroup, areaData, animatedAreaData, curveFactory, theme, sanitizedKey, animation);
|
|
182
|
+
transitions.push(...lineTransitions);
|
|
183
|
+
}
|
|
184
|
+
if (this.showPoints) {
|
|
185
|
+
const pointTransitions = this.renderPoints(plotGroup, areaData, animatedAreaData, theme, sanitizedKey, animation);
|
|
186
|
+
transitions.push(...pointTransitions);
|
|
187
|
+
}
|
|
188
|
+
if (this.valueLabel?.show) {
|
|
189
|
+
this.renderValueLabels(valueLabelLayer ?? plotGroup, areaData.filter((d) => d.valid), y, parseValue, theme);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
snapshot,
|
|
193
|
+
transitions,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
buildAreaData(data, xKey, snapshotKeys, y, parseValue, stackingContext, getXPosition, hasValidValue) {
|
|
197
|
+
return data.map((entry, index) => {
|
|
198
|
+
const valid = hasValidValue(entry);
|
|
174
199
|
const stackValues = valid
|
|
175
|
-
? this.getStackValues(
|
|
200
|
+
? this.getStackValues(entry, xKey, parseValue, stackingContext)
|
|
176
201
|
: { y0: this.baseline, y1: this.baseline };
|
|
177
202
|
return {
|
|
178
|
-
data:
|
|
203
|
+
data: entry,
|
|
179
204
|
valid,
|
|
180
|
-
|
|
181
|
-
|
|
205
|
+
snapshotKey: snapshotKeys[index] ?? String(index),
|
|
206
|
+
x: getXPosition(entry),
|
|
207
|
+
y0: y(stackValues.y0) ?? 0,
|
|
208
|
+
y1: y(stackValues.y1) ?? 0,
|
|
182
209
|
};
|
|
183
210
|
});
|
|
184
|
-
|
|
211
|
+
}
|
|
212
|
+
buildAnimatedAreaData(areaData, animation) {
|
|
213
|
+
return areaData.map((entry) => {
|
|
214
|
+
if (!animation || !entry.valid) {
|
|
215
|
+
return entry;
|
|
216
|
+
}
|
|
217
|
+
const previousSnapshot = animation.previousSnapshot?.get(entry.snapshotKey);
|
|
218
|
+
return {
|
|
219
|
+
...entry,
|
|
220
|
+
x: previousSnapshot?.x ?? entry.x,
|
|
221
|
+
y0: previousSnapshot?.y0 ?? entry.y0,
|
|
222
|
+
y1: previousSnapshot?.y1 ?? entry.y0,
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
createSnapshot(areaData) {
|
|
227
|
+
const snapshot = new Map();
|
|
228
|
+
areaData.forEach((entry) => {
|
|
229
|
+
if (!entry.valid) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
snapshot.set(entry.snapshotKey, {
|
|
233
|
+
x: entry.x,
|
|
234
|
+
y0: entry.y0,
|
|
235
|
+
y1: entry.y1,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
return snapshot;
|
|
239
|
+
}
|
|
240
|
+
renderAreaFill(plotGroup, areaData, animatedAreaData, curveFactory, sanitizedKey, animation) {
|
|
241
|
+
const areaOpacity = Math.max(0, Math.min(1, this.opacity));
|
|
185
242
|
const areaGenerator = area()
|
|
186
243
|
.defined((d) => d.valid)
|
|
187
244
|
.curve(curveFactory)
|
|
188
|
-
.x(
|
|
189
|
-
.y0((
|
|
190
|
-
.y1((
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
plotGroup
|
|
245
|
+
.x((entry) => entry.x)
|
|
246
|
+
.y0((entry) => entry.y0)
|
|
247
|
+
.y1((entry) => entry.y1);
|
|
248
|
+
const finalAreaPath = areaGenerator(areaData);
|
|
249
|
+
const areaPath = plotGroup
|
|
194
250
|
.append('path')
|
|
195
251
|
.datum(areaData)
|
|
196
252
|
.attr('class', `area-${sanitizedKey}`)
|
|
197
253
|
.attr('fill', this.fill)
|
|
198
254
|
.attr('fill-opacity', areaOpacity)
|
|
199
255
|
.attr('stroke', 'none')
|
|
200
|
-
.attr('d', areaGenerator);
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
if (this.showPoints) {
|
|
205
|
-
this.renderPoints(plotGroup, areaData, getXPosition, y, theme, sanitizedKey);
|
|
206
|
-
}
|
|
207
|
-
if (this.valueLabel?.show) {
|
|
208
|
-
this.renderValueLabels(valueLabelLayer ?? plotGroup, areaData.filter((d) => d.valid), y, parseValue, theme, getXPosition);
|
|
256
|
+
.attr('d', this.getInitialAreaPathValue(finalAreaPath, areaData, animatedAreaData, areaGenerator, animation));
|
|
257
|
+
if (!animation || !finalAreaPath) {
|
|
258
|
+
return [];
|
|
209
259
|
}
|
|
260
|
+
const revealTransitions = this.createRevealTransitions(areaPath.node(), `area-${sanitizedKey}-reveal`, animation, animation.previousAreaRevealProgress);
|
|
261
|
+
const transition = areaPath
|
|
262
|
+
.transition()
|
|
263
|
+
.duration(animation.duration)
|
|
264
|
+
.ease(animation.easing)
|
|
265
|
+
.attr('d', finalAreaPath);
|
|
266
|
+
return [
|
|
267
|
+
...revealTransitions,
|
|
268
|
+
createTransitionCompletionPromise(transition),
|
|
269
|
+
];
|
|
210
270
|
}
|
|
211
|
-
renderLinePath(plotGroup, areaData,
|
|
271
|
+
renderLinePath(plotGroup, areaData, animatedAreaData, curveFactory, theme, sanitizedKey, animation) {
|
|
212
272
|
const lineGenerator = line()
|
|
213
273
|
.defined((d) => d.valid)
|
|
214
274
|
.curve(curveFactory)
|
|
215
|
-
.x(
|
|
216
|
-
.y((
|
|
275
|
+
.x((entry) => entry.x)
|
|
276
|
+
.y((entry) => entry.y1);
|
|
217
277
|
const lineStrokeWidth = this.strokeWidth ?? theme.line.strokeWidth;
|
|
218
|
-
|
|
278
|
+
const finalPath = lineGenerator(areaData);
|
|
279
|
+
const linePath = plotGroup
|
|
219
280
|
.append('path')
|
|
220
281
|
.datum(areaData)
|
|
221
282
|
.attr('class', `area-line-${sanitizedKey}`)
|
|
222
283
|
.attr('fill', 'none')
|
|
223
284
|
.attr('stroke', this.stroke)
|
|
224
285
|
.attr('stroke-width', lineStrokeWidth)
|
|
225
|
-
.attr('d', lineGenerator);
|
|
286
|
+
.attr('d', this.getInitialLinePathValue(finalPath, areaData, animatedAreaData, lineGenerator, animation));
|
|
287
|
+
if (!animation || !finalPath) {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
const revealTransitions = this.createRevealTransitions(linePath.node(), `area-line-${sanitizedKey}-reveal`, animation, animation.previousLineRevealProgress, lineStrokeWidth);
|
|
291
|
+
const transition = linePath
|
|
292
|
+
.transition()
|
|
293
|
+
.duration(animation.duration)
|
|
294
|
+
.ease(animation.easing)
|
|
295
|
+
.attr('d', finalPath);
|
|
296
|
+
return [
|
|
297
|
+
...revealTransitions,
|
|
298
|
+
createTransitionCompletionPromise(transition),
|
|
299
|
+
];
|
|
226
300
|
}
|
|
227
|
-
|
|
301
|
+
getInitialAreaPathValue(finalAreaPath, areaData, animatedAreaData, areaGenerator, animation) {
|
|
302
|
+
if (animation?.mode === 'initial') {
|
|
303
|
+
return finalAreaPath;
|
|
304
|
+
}
|
|
305
|
+
if (!animation) {
|
|
306
|
+
return finalAreaPath;
|
|
307
|
+
}
|
|
308
|
+
return (animation.previousAreaPath ??
|
|
309
|
+
areaGenerator(animation ? animatedAreaData : areaData));
|
|
310
|
+
}
|
|
311
|
+
getInitialLinePathValue(finalPath, areaData, animatedAreaData, lineGenerator, animation) {
|
|
312
|
+
if (animation?.mode === 'initial') {
|
|
313
|
+
return finalPath;
|
|
314
|
+
}
|
|
315
|
+
if (!animation) {
|
|
316
|
+
return finalPath;
|
|
317
|
+
}
|
|
318
|
+
return (animation.previousLinePath ??
|
|
319
|
+
lineGenerator(animation ? animatedAreaData : areaData));
|
|
320
|
+
}
|
|
321
|
+
createRevealTransitions(path, revealId, animation, previousRevealProgress, padding = 0) {
|
|
322
|
+
if (animation.mode === 'initial') {
|
|
323
|
+
return createLeftToRightRevealTransition(path, animation.duration, animation.easing, revealId, padding);
|
|
324
|
+
}
|
|
325
|
+
if (previousRevealProgress == null) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
return createLeftToRightRevealTransition(path, animation.duration, animation.easing, revealId, padding, previousRevealProgress);
|
|
329
|
+
}
|
|
330
|
+
renderPoints(plotGroup, areaData, animatedAreaData, theme, sanitizedKey, animation) {
|
|
228
331
|
const validData = areaData.filter((d) => d.valid);
|
|
332
|
+
const validAnimatedData = animatedAreaData.filter((d) => d.valid);
|
|
229
333
|
const pointSize = this.pointSize ?? theme.line.point.size;
|
|
230
334
|
const pointStrokeWidth = theme.line.point.strokeWidth;
|
|
231
335
|
const pointStrokeColor = theme.line.point.strokeColor || this.stroke;
|
|
232
336
|
const pointColor = theme.line.point.color || this.stroke;
|
|
233
|
-
|
|
337
|
+
const isInitialAnimation = animation?.mode === 'initial';
|
|
338
|
+
const circles = plotGroup
|
|
234
339
|
.selectAll(`.area-point-${sanitizedKey}`)
|
|
235
340
|
.data(validData)
|
|
236
341
|
.join('circle')
|
|
237
342
|
.attr('class', `area-point-${sanitizedKey}`)
|
|
238
|
-
.attr('cx',
|
|
239
|
-
|
|
240
|
-
|
|
343
|
+
.attr('cx', (_, index) => (isInitialAnimation
|
|
344
|
+
? validData[index]?.x
|
|
345
|
+
: animation
|
|
346
|
+
? validAnimatedData[index]?.x
|
|
347
|
+
: validData[index]?.x) ?? 0)
|
|
348
|
+
.attr('cy', (_, index) => (isInitialAnimation
|
|
349
|
+
? validData[index]?.y1
|
|
350
|
+
: animation
|
|
351
|
+
? validAnimatedData[index]?.y1
|
|
352
|
+
: validData[index]?.y1) ?? 0)
|
|
353
|
+
.attr('r', isInitialAnimation ? 0 : pointSize)
|
|
241
354
|
.attr('fill', pointColor)
|
|
242
355
|
.attr('stroke', pointStrokeColor)
|
|
243
356
|
.attr('stroke-width', pointStrokeWidth);
|
|
357
|
+
if (!animation) {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
if (isInitialAnimation) {
|
|
361
|
+
const transition = circles
|
|
362
|
+
.transition()
|
|
363
|
+
.delay((_, index) => {
|
|
364
|
+
return getEnterStaggerTiming(index, validData.length, animation.duration).delay;
|
|
365
|
+
})
|
|
366
|
+
.duration((_, index) => {
|
|
367
|
+
return getEnterStaggerTiming(index, validData.length, animation.duration).duration;
|
|
368
|
+
})
|
|
369
|
+
.ease(animation.easing)
|
|
370
|
+
.attr('r', pointSize);
|
|
371
|
+
return [createTransitionCompletionPromise(transition)];
|
|
372
|
+
}
|
|
373
|
+
const transition = circles
|
|
374
|
+
.transition()
|
|
375
|
+
.duration(animation.duration)
|
|
376
|
+
.ease(animation.easing)
|
|
377
|
+
.attr('cx', (_, index) => validData[index]?.x ?? 0)
|
|
378
|
+
.attr('cy', (_, index) => validData[index]?.y1 ?? 0);
|
|
379
|
+
return [createTransitionCompletionPromise(transition)];
|
|
244
380
|
}
|
|
245
|
-
renderValueLabels(plotGroup, data, y, parseValue, theme
|
|
381
|
+
renderValueLabels(plotGroup, data, y, parseValue, theme) {
|
|
246
382
|
const config = this.valueLabel;
|
|
247
383
|
const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
|
|
248
384
|
const fontFamily = config.fontFamily ?? theme.valueLabel.fontFamily;
|
|
@@ -269,8 +405,8 @@ export class Area {
|
|
|
269
405
|
const valueText = config.formatter
|
|
270
406
|
? config.formatter(this.dataKey, parsedValue, d.data)
|
|
271
407
|
: String(parsedValue);
|
|
272
|
-
const xPos =
|
|
273
|
-
const yPos =
|
|
408
|
+
const xPos = d.x;
|
|
409
|
+
const yPos = d.y1;
|
|
274
410
|
const tempText = labelGroup
|
|
275
411
|
.append('text')
|
|
276
412
|
.style('font-size', `${fontSize}px`)
|
package/dist/bar.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Selection } from 'd3';
|
|
2
2
|
import type { BarConfig, BarStackingContext, BarSide, BarValueLabelConfig, ChartTheme, D3Scale, DataItem, Orientation, ScaleType, ExportHooks, BarConfigBase } from './types.js';
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
|
+
import type { XYBarAnimationContext, XYBarSnapshot, XYSeriesRenderResult } from './xy-motion/types.js';
|
|
4
5
|
export declare class Bar implements ChartComponent<BarConfigBase> {
|
|
5
6
|
readonly type: "bar";
|
|
6
7
|
readonly dataKey: string;
|
|
@@ -14,9 +15,16 @@ export declare class Bar implements ChartComponent<BarConfigBase> {
|
|
|
14
15
|
getExportConfig(): BarConfigBase;
|
|
15
16
|
createExportComponent(override?: Partial<BarConfigBase>): ChartComponent<BarConfigBase>;
|
|
16
17
|
getRenderedValue(value: number, orientation?: Orientation): number;
|
|
17
|
-
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, orientation?: 'vertical' | 'horizontal'):
|
|
18
|
+
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, orientation?: 'vertical' | 'horizontal', animation?: XYBarAnimationContext): XYSeriesRenderResult<XYBarSnapshot>;
|
|
18
19
|
private renderVertical;
|
|
19
20
|
private renderHorizontal;
|
|
21
|
+
private createVerticalLayoutData;
|
|
22
|
+
private createHorizontalLayoutData;
|
|
23
|
+
private createAnimatedVerticalLayoutData;
|
|
24
|
+
private createAnimatedHorizontalLayoutData;
|
|
25
|
+
private resolveAnimatedLayoutDatum;
|
|
26
|
+
private createSnapshot;
|
|
27
|
+
private renderBars;
|
|
20
28
|
private resolveValueLabelConfig;
|
|
21
29
|
private resolveValueLabelPlacement;
|
|
22
30
|
private resolveValueLabelStyle;
|
package/dist/bar.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getContrastTextColor, sanitizeForCSS, mergeDeep } from './utils.js';
|
|
2
2
|
import { getScalePosition } from './scale-utils.js';
|
|
3
|
+
import { buildXYDatumSnapshotKeys, createTransitionCompletionPromise, } from './xy-motion/helpers.js';
|
|
3
4
|
const LABEL_INSET_DEFAULT = 4;
|
|
4
5
|
const LABEL_INSET_STACKED = 6;
|
|
5
6
|
const LABEL_MIN_PADDING_DEFAULT = 8;
|
|
@@ -198,13 +199,10 @@ export class Bar {
|
|
|
198
199
|
}
|
|
199
200
|
return value;
|
|
200
201
|
}
|
|
201
|
-
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext, orientation = 'vertical') {
|
|
202
|
-
|
|
203
|
-
this.renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext)
|
|
204
|
-
|
|
205
|
-
else {
|
|
206
|
-
this.renderHorizontal(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext);
|
|
207
|
-
}
|
|
202
|
+
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext, orientation = 'vertical', animation) {
|
|
203
|
+
const result = orientation === 'vertical'
|
|
204
|
+
? this.renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext, animation)
|
|
205
|
+
: this.renderHorizontal(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext, animation);
|
|
208
206
|
// Render value labels if enabled
|
|
209
207
|
if (this.valueLabel?.show && theme) {
|
|
210
208
|
if (orientation === 'vertical') {
|
|
@@ -214,8 +212,9 @@ export class Bar {
|
|
|
214
212
|
this.renderHorizontalValueLabels(plotGroup, data, xKey, x, y, parseValue, xScaleType, theme, stackingContext);
|
|
215
213
|
}
|
|
216
214
|
}
|
|
215
|
+
return result;
|
|
217
216
|
}
|
|
218
|
-
renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext) {
|
|
217
|
+
renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext, animation) {
|
|
219
218
|
const bandwidth = x.bandwidth ? x.bandwidth() : 20;
|
|
220
219
|
const mode = stackingContext?.mode ?? 'normal';
|
|
221
220
|
const { thickness: barWidth, offset: barOffset } = getBarSlotLayout(bandwidth, mode, this.maxBarSize, stackingContext?.totalSeries ?? 1, stackingContext?.seriesIndex ?? 0, stackingContext?.gap ?? 0.1);
|
|
@@ -225,29 +224,15 @@ export class Bar {
|
|
|
225
224
|
const { start, end } = getBarValueRange(categoryKey, value, stackingContext);
|
|
226
225
|
return getScaledValueRange(y, start, end);
|
|
227
226
|
};
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
.
|
|
233
|
-
.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const xPos = getScalePosition(x, d[xKey], xScaleType);
|
|
238
|
-
return xScaleType === 'band'
|
|
239
|
-
? xPos + barOffset
|
|
240
|
-
: xPos - barWidth / 2;
|
|
241
|
-
})
|
|
242
|
-
.attr('y', (d) => getVerticalBounds(d).min)
|
|
243
|
-
.attr('width', barWidth)
|
|
244
|
-
.attr('height', (d) => {
|
|
245
|
-
const bounds = getVerticalBounds(d);
|
|
246
|
-
return Math.abs(bounds.max - bounds.min);
|
|
247
|
-
})
|
|
248
|
-
.attr('fill', (d, i) => this.colorAdapter ? this.colorAdapter(d, i) : this.fill);
|
|
249
|
-
}
|
|
250
|
-
renderHorizontal(plotGroup, data, xKey, x, y, parseValue, yScaleType, stackingContext) {
|
|
227
|
+
const snapshotKeys = buildXYDatumSnapshotKeys(data, xKey);
|
|
228
|
+
const layoutData = this.createVerticalLayoutData(data, xKey, snapshotKeys, x, xScaleType, barWidth, barOffset, getVerticalBounds);
|
|
229
|
+
const animatedLayoutData = this.createAnimatedVerticalLayoutData(layoutData, animation);
|
|
230
|
+
return {
|
|
231
|
+
snapshot: this.createSnapshot(layoutData),
|
|
232
|
+
transitions: this.renderBars(plotGroup, layoutData, animatedLayoutData, animation),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
renderHorizontal(plotGroup, data, xKey, x, y, parseValue, yScaleType, stackingContext, animation) {
|
|
251
236
|
const bandwidth = y.bandwidth ? y.bandwidth() : 20;
|
|
252
237
|
const mode = stackingContext?.mode ?? 'normal';
|
|
253
238
|
const { thickness: barHeight, offset: barOffset } = getBarSlotLayout(bandwidth, mode, this.maxBarSize, stackingContext?.totalSeries ?? 1, stackingContext?.seriesIndex ?? 0, stackingContext?.gap ?? 0.1);
|
|
@@ -257,27 +242,125 @@ export class Bar {
|
|
|
257
242
|
const { start, end } = getBarValueRange(categoryKey, value, stackingContext);
|
|
258
243
|
return getScaledValueRange(x, start, end);
|
|
259
244
|
};
|
|
260
|
-
|
|
245
|
+
const snapshotKeys = buildXYDatumSnapshotKeys(data, xKey);
|
|
246
|
+
const layoutData = this.createHorizontalLayoutData(data, xKey, snapshotKeys, y, yScaleType, barHeight, barOffset, getHorizontalBounds);
|
|
247
|
+
const animatedLayoutData = this.createAnimatedHorizontalLayoutData(layoutData, animation);
|
|
248
|
+
return {
|
|
249
|
+
snapshot: this.createSnapshot(layoutData),
|
|
250
|
+
transitions: this.renderBars(plotGroup, layoutData, animatedLayoutData, animation),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
createVerticalLayoutData(data, xKey, snapshotKeys, x, xScaleType, barWidth, barOffset, getVerticalBounds) {
|
|
254
|
+
return data.map((entry, index) => {
|
|
255
|
+
const xPos = getScalePosition(x, entry[xKey], xScaleType);
|
|
256
|
+
const bounds = getVerticalBounds(entry);
|
|
257
|
+
return {
|
|
258
|
+
data: entry,
|
|
259
|
+
snapshotKey: snapshotKeys[index] ?? String(index),
|
|
260
|
+
x: xScaleType === 'band'
|
|
261
|
+
? xPos + barOffset
|
|
262
|
+
: xPos - barWidth / 2,
|
|
263
|
+
y: bounds.min,
|
|
264
|
+
width: barWidth,
|
|
265
|
+
height: Math.abs(bounds.max - bounds.min),
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
createHorizontalLayoutData(data, xKey, snapshotKeys, y, yScaleType, barHeight, barOffset, getHorizontalBounds) {
|
|
270
|
+
return data.map((entry, index) => {
|
|
271
|
+
const yPos = getScalePosition(y, entry[xKey], yScaleType);
|
|
272
|
+
const bounds = getHorizontalBounds(entry);
|
|
273
|
+
return {
|
|
274
|
+
data: entry,
|
|
275
|
+
snapshotKey: snapshotKeys[index] ?? String(index),
|
|
276
|
+
x: bounds.min,
|
|
277
|
+
y: yScaleType === 'band'
|
|
278
|
+
? yPos + barOffset
|
|
279
|
+
: yPos - barHeight / 2,
|
|
280
|
+
width: Math.abs(bounds.max - bounds.min),
|
|
281
|
+
height: barHeight,
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
createAnimatedVerticalLayoutData(layoutData, animation) {
|
|
286
|
+
return layoutData.map((entry) => {
|
|
287
|
+
return this.resolveAnimatedLayoutDatum(entry, animation, {
|
|
288
|
+
y: animation?.baselineValuePosition,
|
|
289
|
+
height: 0,
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
createAnimatedHorizontalLayoutData(layoutData, animation) {
|
|
294
|
+
return layoutData.map((entry) => {
|
|
295
|
+
return this.resolveAnimatedLayoutDatum(entry, animation, {
|
|
296
|
+
x: animation?.baselineValuePosition,
|
|
297
|
+
width: 0,
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
resolveAnimatedLayoutDatum(entry, animation, baselineOverride) {
|
|
302
|
+
if (!animation) {
|
|
303
|
+
return entry;
|
|
304
|
+
}
|
|
305
|
+
const previousSnapshot = animation.previousSnapshot?.get(entry.snapshotKey);
|
|
306
|
+
if (previousSnapshot) {
|
|
307
|
+
return {
|
|
308
|
+
...entry,
|
|
309
|
+
...previousSnapshot,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
...entry,
|
|
314
|
+
...baselineOverride,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
createSnapshot(layoutData) {
|
|
318
|
+
const snapshot = new Map();
|
|
319
|
+
layoutData.forEach((entry) => {
|
|
320
|
+
snapshot.set(entry.snapshotKey, {
|
|
321
|
+
x: entry.x,
|
|
322
|
+
y: entry.y,
|
|
323
|
+
width: entry.width,
|
|
324
|
+
height: entry.height,
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
return snapshot;
|
|
328
|
+
}
|
|
329
|
+
renderBars(plotGroup, layoutData, animatedLayoutData, animation) {
|
|
261
330
|
const sanitizedKey = sanitizeForCSS(this.dataKey);
|
|
262
|
-
plotGroup
|
|
331
|
+
const bars = plotGroup
|
|
263
332
|
.selectAll(`.bar-${sanitizedKey}`)
|
|
264
|
-
.data(
|
|
333
|
+
.data(layoutData)
|
|
265
334
|
.join('rect')
|
|
266
335
|
.attr('class', `bar-${sanitizedKey}`)
|
|
267
336
|
.attr('data-index', (_, i) => i)
|
|
268
|
-
.attr('x', (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
.attr('fill', (
|
|
337
|
+
.attr('x', (_, index) => (animation
|
|
338
|
+
? animatedLayoutData[index]?.x
|
|
339
|
+
: layoutData[index]?.x) ?? 0)
|
|
340
|
+
.attr('y', (_, index) => (animation
|
|
341
|
+
? animatedLayoutData[index]?.y
|
|
342
|
+
: layoutData[index]?.y) ?? 0)
|
|
343
|
+
.attr('width', (_, index) => (animation
|
|
344
|
+
? animatedLayoutData[index]?.width
|
|
345
|
+
: layoutData[index]?.width) ?? 0)
|
|
346
|
+
.attr('height', (_, index) => (animation
|
|
347
|
+
? animatedLayoutData[index]?.height
|
|
348
|
+
: layoutData[index]?.height) ?? 0)
|
|
349
|
+
.attr('fill', (entry, i) => this.colorAdapter
|
|
350
|
+
? this.colorAdapter(entry.data, i)
|
|
351
|
+
: this.fill);
|
|
352
|
+
if (!animation) {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
const transition = bars
|
|
356
|
+
.transition()
|
|
357
|
+
.duration(animation.duration)
|
|
358
|
+
.ease(animation.easing)
|
|
359
|
+
.attr('x', (_, index) => layoutData[index]?.x ?? 0)
|
|
360
|
+
.attr('y', (_, index) => layoutData[index]?.y ?? 0)
|
|
361
|
+
.attr('width', (_, index) => layoutData[index]?.width ?? 0)
|
|
362
|
+
.attr('height', (_, index) => layoutData[index]?.height ?? 0);
|
|
363
|
+
return [createTransitionCompletionPromise(transition)];
|
|
281
364
|
}
|
|
282
365
|
resolveValueLabelConfig(theme) {
|
|
283
366
|
const config = this.valueLabel;
|
package/dist/base-chart.js
CHANGED
|
@@ -737,7 +737,17 @@ export class BaseChart {
|
|
|
737
737
|
if (this.resizeObserver) {
|
|
738
738
|
this.resizeObserver.disconnect();
|
|
739
739
|
}
|
|
740
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
740
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
741
|
+
if (!this.container) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const nextDimensions = this.resolveRenderDimensions(this.container.getBoundingClientRect());
|
|
745
|
+
if (nextDimensions.width === this.width &&
|
|
746
|
+
nextDimensions.height === this.height) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
this.rerender();
|
|
750
|
+
});
|
|
741
751
|
this.resizeObserver.observe(this.container);
|
|
742
752
|
}
|
|
743
753
|
setReadyPromise(promise) {
|