@fundar/data-chart-telling 0.0.3 → 0.0.5
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 +63 -28
- package/dist/index.d.ts +1 -1
- package/dist/layout/plot/PlotLayout.svelte +1 -1
- package/dist/plots/bar/Plot.svelte +1 -1
- package/dist/plots/heatmap/Plot.svelte +6 -3
- package/dist/plots/line/Plot.svelte +1 -1
- package/dist/plots/markers/DeltaMarker.svelte +25 -13
- package/dist/plots/pyramid/Plot.svelte +1 -1
- package/dist/plots/utils/segments.d.ts +12 -14
- package/dist/plots/utils/segments.js +48 -22
- package/dist/types/charts/props.d.ts +2 -2
- package/dist/types/plots/props.d.ts +2 -2
- package/dist/types/plots/styles.d.ts +45 -13
- package/package.json +75 -76
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ Plots accept either `series` (pre-built `Series[]`) or the `data` + `x`/`y`/`z`
|
|
|
116
116
|
| `x` | `Accessor` | — | ✅ | ✅ | ✅ | ✅ |
|
|
117
117
|
| `y` | `Accessor` | — | ✅ | ✅ | ✅ | ✅ |
|
|
118
118
|
| `z` | `Accessor` | — | ✅ | ✅ | ✅ | ✅ |
|
|
119
|
-
| `segments` | `
|
|
119
|
+
| `segments` | `Segments<TStyle>` | `[]` | ✅ | ✅ | ✅ | ✅ |
|
|
120
120
|
| `markers` | `Marker[]` | `[]` | ✅ | ✅ | ✅ | ✅ |
|
|
121
121
|
| `scales` | `ScalesConfig` | `{}` | ✅ | ✅ | ✅ | ✅ |
|
|
122
122
|
| `margins` | `MarginConfig` | `{}` | ✅ | ✅ | ✅ | ✅ |
|
|
@@ -158,7 +158,7 @@ Plots accept either `series` (pre-built `Series[]`) or the `data` + `x`/`y`/`z`
|
|
|
158
158
|
| `subtitle` | `string` | | ✅ | ✅ | ✅ | ✅ |
|
|
159
159
|
| `caption` | `string` | | ✅ | ✅ | ✅ | ✅ |
|
|
160
160
|
| `legend` | `LegendSection[]` | | ✅ | ✅ | ✅ | ✅ |
|
|
161
|
-
| `segments` | `
|
|
161
|
+
| `segments` | `Segments<TStyle>` | `[]` | ✅ | ✅ | ✅ | ✅ |
|
|
162
162
|
| `markers` | `Marker[]` | `[]` | ✅ | ✅ | ✅ | ✅ |
|
|
163
163
|
| `scales` | `ScalesConfig` | `{}` | ✅ | ✅ | ✅ | ✅ |
|
|
164
164
|
| `margins` | `MarginConfig` | `{}` | ✅ | ✅ | ✅ | ✅ |
|
|
@@ -177,49 +177,80 @@ The `segments` prop separates data ranges from visual styling. Every plot kind s
|
|
|
177
177
|
|
|
178
178
|
| Plot | `segments` type |
|
|
179
179
|
| ---- | --------------- |
|
|
180
|
-
| `LinePlot` / `LineChart` | `
|
|
181
|
-
| `BarPlot` / `BarChart` | `
|
|
182
|
-
| `PyramidPlot` / `PyramidChart` | `
|
|
183
|
-
| `HeatmapPlot` / `HeatmapChart` | `
|
|
180
|
+
| `LinePlot` / `LineChart` | `Segments<LineSegmentStyle>` |
|
|
181
|
+
| `BarPlot` / `BarChart` | `Segments<BarSegmentStyle>` |
|
|
182
|
+
| `PyramidPlot` / `PyramidChart` | `Segments<BarSegmentStyle>` |
|
|
183
|
+
| `HeatmapPlot` / `HeatmapChart` | `Segments<CellSegmentStyle>` (`'default'` key only) |
|
|
184
184
|
|
|
185
185
|
#### Core types
|
|
186
186
|
|
|
187
187
|
```ts
|
|
188
|
-
type Range
|
|
189
|
-
type Area
|
|
188
|
+
type Range = { from?: AxisValue; to?: AxisValue }; // inclusive on both sides
|
|
189
|
+
type Area = { x?: Range; y?: Range }; // 2-D rectangular region
|
|
190
190
|
type Segment<TStyle> = { areas?: Area[]; style?: TStyle };
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
A **`Segment`** matches a data point (or heatmap cell) when the point falls inside **any** of its `areas`. Omitting `areas`
|
|
193
|
+
A **`Segment`** matches a data point (or heatmap cell) when the point falls inside **any** of its `areas`. Omitting `areas` creates a **catch-all** that matches every data point — but catch-alls are always evaluated **last**, regardless of their position in the list, so area-specific segments always win. This lets a catch-all placed first act as a fallback baseline that highlighted ranges override.
|
|
194
194
|
|
|
195
195
|
An **`Area`** restricts by x, y, or both. Both bounds of a `Range` are **inclusive**. Omitting `x` or `y` leaves that axis unrestricted.
|
|
196
196
|
|
|
197
|
-
#### `
|
|
197
|
+
#### `Segments` structure
|
|
198
|
+
|
|
199
|
+
`segments` is always an **array of groups**. Each group is a plain object whose keys map to `Segment[]` lists:
|
|
198
200
|
|
|
199
201
|
```ts
|
|
200
|
-
//
|
|
201
|
-
type
|
|
202
|
+
// 'default' key — applies its segments to every series
|
|
203
|
+
type DefaultSegmentsMap<TStyle> = { default: Segment<TStyle>[] };
|
|
204
|
+
|
|
205
|
+
// named keys — applies segments only to the named series
|
|
206
|
+
type SeriesAwareSegmentsMap<TStyle> = Record<string, Segment<TStyle>[]>;
|
|
202
207
|
|
|
203
|
-
//
|
|
204
|
-
type
|
|
205
|
-
|
|
208
|
+
// one element of the array — either form, or a mix of both key types
|
|
209
|
+
type SegmentsStyle<TStyle> = DefaultSegmentsMap<TStyle> | SeriesAwareSegmentsMap<TStyle>;
|
|
210
|
+
|
|
211
|
+
// the prop type: an ordered list of groups
|
|
212
|
+
type Segments<TStyle> = SegmentsStyle<TStyle>[];
|
|
206
213
|
```
|
|
207
214
|
|
|
215
|
+
Having multiple groups lets you organise segments by concern — for example one group for per-series stroke colours and a second for shared dot markers applied via `'default'`. Groups are processed in order and their resolved segments are concatenated per series before evaluation.
|
|
216
|
+
|
|
208
217
|
#### Examples
|
|
209
218
|
|
|
210
|
-
**Line chart — colour a
|
|
219
|
+
**Line chart — baseline colour with a highlighted range (catch-all as fallback):**
|
|
211
220
|
|
|
212
221
|
```svelte
|
|
213
222
|
<LinePlot
|
|
214
223
|
{width} {height}
|
|
215
224
|
series={[{ name: 'gdp', x: 'year', y: 'value', data: GDP }]}
|
|
216
|
-
segments={
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
segments={[
|
|
226
|
+
{
|
|
227
|
+
gdp: [
|
|
228
|
+
{ style: { stroke: { stroke: '#94a3b8', strokeWidth: 1 } } },
|
|
229
|
+
{ areas: [{ x: { from: 2008, to: 2010 } }],
|
|
230
|
+
style: { stroke: { stroke: '#ef4444', strokeWidth: 3 } } },
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
]}
|
|
234
|
+
/>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Line chart — per-series baseline colours + shared dot markers (two-group approach):**
|
|
238
|
+
|
|
239
|
+
```svelte
|
|
240
|
+
<LinePlot
|
|
241
|
+
{width} {height}
|
|
242
|
+
data={LIFE_EXPECTANCY} x="year" y="value" z="sex"
|
|
243
|
+
segments={[
|
|
244
|
+
// Group 1: per-series catch-all stroke colours
|
|
245
|
+
{ female: [{ style: { stroke: { stroke: 'steelblue' } } }],
|
|
246
|
+
male: [{ style: { stroke: { stroke: 'tomato' } } }] },
|
|
247
|
+
// Group 2: dots for all series in a specific range
|
|
248
|
+
{ default: [
|
|
249
|
+
{ areas: [{ x: { from: 2010, to: 2012 } }],
|
|
250
|
+
style: { dots: { dotRadius: 5, dotSymbol: 'circle' } } },
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
]}
|
|
223
254
|
/>
|
|
224
255
|
```
|
|
225
256
|
|
|
@@ -229,15 +260,19 @@ type SegmentsMap<TStyle> = SeriesAwareSegmentsMap<TStyle>;
|
|
|
229
260
|
<HeatmapChart
|
|
230
261
|
{width} {height} {data} {x} {y} {z}
|
|
231
262
|
segments={[
|
|
232
|
-
{
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
263
|
+
{
|
|
264
|
+
default: [
|
|
265
|
+
{ areas: [{ x: { from: 'Q1', to: 'Q1' } }, { x: { from: 'Q3', to: 'Q3' } }],
|
|
266
|
+
style: { stroke: '#f59e0b', strokeWidth: 2 } },
|
|
267
|
+
{ areas: [{ x: { from: 'Q2', to: 'Q2' }, y: { from: 1, to: 2 } }],
|
|
268
|
+
style: { fill: '#ef4444', fillOpacity: 0.8 } },
|
|
269
|
+
],
|
|
270
|
+
},
|
|
236
271
|
]}
|
|
237
272
|
/>
|
|
238
273
|
```
|
|
239
274
|
|
|
240
|
-
Omitting `segments` renders every mark in its default palette colour.
|
|
275
|
+
Omitting `segments` (or passing `[]`) renders every mark in its default palette colour.
|
|
241
276
|
|
|
242
277
|
---
|
|
243
278
|
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export { groupBy, buildSeries } from './utils/grouping';
|
|
|
26
26
|
export { makeColorScale, normalize, resolveCssColor, paletteColor } from './utils/color';
|
|
27
27
|
export type { AxisValue, Accessor, AxisScale, ScalesConfig, Series, SeriesProps, DataProps } from './types/plots/common';
|
|
28
28
|
export type { DeltaConfig, } from './types/plots/delta';
|
|
29
|
-
export type { SymbolType, LineStyle, StrokeStyle, DotStyle, FontStyle, LineSegmentStyle, BarSegmentStyle, CellSegmentStyle, Range, Area, Segment, SeriesAwareSegmentsMap,
|
|
29
|
+
export type { SymbolType, LineStyle, StrokeStyle, DotStyle, FontStyle, LineSegmentStyle, BarSegmentStyle, CellSegmentStyle, Range, Area, Segment, SeriesAwareSegmentsMap, DefaultSegmentsMap, SegmentsStyle, Segments, ValueAnchor, ValuesStyle, ColorsStyle, PlotStyles, } from './types/plots/styles';
|
|
30
30
|
export type { Marker, MarkerProps } from './types/plots/markers';
|
|
31
31
|
export type { PlotProps } from './types/plots/props';
|
|
32
32
|
export type { TimeValue, Orientation, FacetColumns, FacetConfig, TimelineConfig, ChartPlotContext, ChartPlotSnippet, } from './types/charts/common';
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
</Plot>
|
|
219
219
|
|
|
220
220
|
{#if tooltip && ctrl.showTooltip && ctrl.tooltipPosition}
|
|
221
|
-
<Tooltip x={ctrl.tooltipPosition
|
|
221
|
+
<Tooltip x={ctrl.tooltipPosition?.x ?? 0} y={ctrl.tooltipPosition?.y ?? 0}>
|
|
222
222
|
{#if tooltip.content}
|
|
223
223
|
{@render tooltip.content({ rows: ctrl.matchedRows.map((p) => p.row) })}
|
|
224
224
|
{:else}
|
|
@@ -43,9 +43,12 @@
|
|
|
43
43
|
'HeatmapPlot: markers scoped to a `series` are ignored — heatmaps have no series concept.',
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
const seriesKeys = segments.flatMap((g) =>
|
|
47
|
+
Object.keys(g as Record<string, unknown>).filter((k) => k !== 'default'),
|
|
48
|
+
);
|
|
49
|
+
if (seriesKeys.length > 0) {
|
|
47
50
|
console.warn(
|
|
48
|
-
|
|
51
|
+
`HeatmapPlot: segment groups have series-specific keys (${[...new Set(seriesKeys)].join(', ')}) — heatmaps have no named series. Use the 'default' key instead.`,
|
|
49
52
|
);
|
|
50
53
|
}
|
|
51
54
|
});
|
|
@@ -87,7 +90,7 @@
|
|
|
87
90
|
makeColorScale(valueDomain[0], valueDomain[1], minColor, maxColor),
|
|
88
91
|
);
|
|
89
92
|
|
|
90
|
-
const segList = $derived(
|
|
93
|
+
const segList = $derived(segments.flatMap((g) => (g as Record<string, CellSegmentStyle[]>)['default'] ?? []));
|
|
91
94
|
|
|
92
95
|
const segStyleMap = $derived.by(() => {
|
|
93
96
|
const map = new Map<string, CellSegmentStyle>();
|
|
@@ -59,15 +59,27 @@
|
|
|
59
59
|
? Math.min(fromVis, toVis)
|
|
60
60
|
: (fromVis + toVis) / 2;
|
|
61
61
|
}
|
|
62
|
-
</script>
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
// Pre-derive all coordinates as script-level signals so prop getters read safe
|
|
64
|
+
// values during Svelte 5 teardown, before the {#if} guard finishes unmounting.
|
|
65
|
+
const coords = $derived.by(() => {
|
|
66
|
+
if (!resolved) return null;
|
|
67
|
+
const fromVis = clamp(resolved.from ?? 0);
|
|
68
|
+
const toVis = clamp(resolved.to ?? 0);
|
|
69
|
+
const at = resolved.at ?? 0;
|
|
70
|
+
return {
|
|
71
|
+
fromPt: pt(at, fromVis),
|
|
72
|
+
toPt: pt(at, toVis),
|
|
73
|
+
textPt: pt(at, textVaryCoord(fromVis, toVis)),
|
|
74
|
+
lineData: [pt(at, fromVis), pt(at, toVis)],
|
|
75
|
+
diff: resolved.diff ?? 0,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
</script>
|
|
68
79
|
|
|
80
|
+
{#if coords}
|
|
69
81
|
<Line
|
|
70
|
-
data={
|
|
82
|
+
data={coords.lineData}
|
|
71
83
|
x={(p) => p.x}
|
|
72
84
|
y={(p) => p.y}
|
|
73
85
|
stroke={strokeStyle?.stroke ?? 'currentColor'}
|
|
@@ -77,19 +89,19 @@
|
|
|
77
89
|
strokeLinecap={strokeStyle?.strokeLinecap ?? 'round'}
|
|
78
90
|
/>
|
|
79
91
|
<DotMarker
|
|
80
|
-
x={
|
|
81
|
-
y={
|
|
92
|
+
x={coords?.fromPt?.x ?? 0}
|
|
93
|
+
y={coords?.fromPt?.y ?? 0}
|
|
82
94
|
style={{ dotRadius: 4, dotFill: strokeStyle?.stroke, dotStroke: strokeStyle?.stroke, dotStrokeWidth: 2, ...dotStyle }}
|
|
83
95
|
/>
|
|
84
96
|
<DotMarker
|
|
85
|
-
x={
|
|
86
|
-
y={
|
|
97
|
+
x={coords?.toPt?.x ?? 0}
|
|
98
|
+
y={coords?.toPt?.y ?? 0}
|
|
87
99
|
style={{ dotRadius: 4, dotFill: strokeStyle?.stroke, dotStroke: strokeStyle?.stroke, dotStrokeWidth: 2, ...dotStyle }}
|
|
88
100
|
/>
|
|
89
101
|
<TextMarker
|
|
90
|
-
x={textPt
|
|
91
|
-
y={textPt
|
|
92
|
-
text={(formatDiff ?? ((v: number) => v.toFixed(1)))(
|
|
102
|
+
x={coords?.textPt?.x ?? 0}
|
|
103
|
+
y={coords?.textPt?.y ?? 0}
|
|
104
|
+
text={(formatDiff ?? ((v: number) => v.toFixed(1)))(coords?.diff ?? 0)}
|
|
93
105
|
style={{
|
|
94
106
|
fontWeight: 'bold',
|
|
95
107
|
fontSize: 14,
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { AxisValue, Series } from '../../types/plots/common';
|
|
2
|
-
import type { Segment,
|
|
3
|
-
/** Narrows a {@link SegmentsMap} to its per-series record form. */
|
|
4
|
-
export declare function isSeriesAware<TStyle>(segments: SegmentsMap<TStyle>): segments is SeriesAwareSegmentsMap<TStyle>;
|
|
2
|
+
import type { Segment, Segments, LineSegmentStyle, VisualGroup } from '../../types/plots/styles';
|
|
5
3
|
/**
|
|
6
|
-
* Returns the {@link Segment} list
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Returns the flat {@link Segment} list for one series by merging all groups.
|
|
5
|
+
* Within each group, series-specific segments are added before `'default'`
|
|
6
|
+
* segments, so series-specific rules win on tie-breaks.
|
|
9
7
|
*/
|
|
10
|
-
export declare function resolveSegmentsForSeries<TStyle>(segments:
|
|
8
|
+
export declare function resolveSegmentsForSeries<TStyle>(segments: Segments<TStyle>, seriesName: string): Segment<TStyle>[];
|
|
11
9
|
/** Returns true when `xVal` falls inside any of a segment's x ranges. */
|
|
12
10
|
export declare function matchesSegmentX<TStyle>(xVal: AxisValue, seg: Segment<TStyle>): boolean;
|
|
13
11
|
/**
|
|
@@ -20,14 +18,14 @@ export declare function matchesSegmentX<TStyle>(xVal: AxisValue, seg: Segment<TS
|
|
|
20
18
|
*/
|
|
21
19
|
export declare function getDataForSegments<TData, TStyle>(data: TData[], xAccessor: (d: TData) => AxisValue, segs: Segment<TStyle>[]): TData[][];
|
|
22
20
|
/**
|
|
23
|
-
* Warns about obvious configuration mistakes:
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* Warns about obvious configuration mistakes: multiple catch-all segments
|
|
22
|
+
* (no `areas`) in the same series list. Since catch-alls are evaluated last
|
|
23
|
+
* and in order, only the first one can ever match; later ones are dead code.
|
|
26
24
|
*/
|
|
27
|
-
export declare function validateSegments<TStyle>(segments:
|
|
25
|
+
export declare function validateSegments<TStyle>(segments: Segments<TStyle>): void;
|
|
28
26
|
/**
|
|
29
27
|
* Builds the full list of visual groups — one per series — ready to render.
|
|
30
|
-
* Each group's `visualSegments` is derived
|
|
31
|
-
*
|
|
28
|
+
* Each group's `visualSegments` is derived by resolving the flat segment list
|
|
29
|
+
* for that series from all groups in `segments`.
|
|
32
30
|
*/
|
|
33
|
-
export declare function buildGroupedSeries<S extends Series<any>, TStyle = LineSegmentStyle>(seriesList: S[], segments:
|
|
31
|
+
export declare function buildGroupedSeries<S extends Series<any>, TStyle = LineSegmentStyle>(seriesList: S[], segments: Segments<TStyle>): VisualGroup<S, TStyle>[];
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { resolveAccessor } from './accessors';
|
|
2
|
-
|
|
3
|
-
export function isSeriesAware(segments) {
|
|
4
|
-
return !Array.isArray(segments);
|
|
5
|
-
}
|
|
2
|
+
const DEFAULT_KEY = 'default';
|
|
6
3
|
/**
|
|
7
|
-
* Returns the {@link Segment} list
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Returns the flat {@link Segment} list for one series by merging all groups.
|
|
5
|
+
* Within each group, series-specific segments are added before `'default'`
|
|
6
|
+
* segments, so series-specific rules win on tie-breaks.
|
|
10
7
|
*/
|
|
11
8
|
export function resolveSegmentsForSeries(segments, seriesName) {
|
|
12
|
-
|
|
9
|
+
const result = [];
|
|
10
|
+
for (const group of segments) {
|
|
11
|
+
const specific = group[seriesName];
|
|
12
|
+
const defaults = group[DEFAULT_KEY];
|
|
13
|
+
if (specific)
|
|
14
|
+
result.push(...specific);
|
|
15
|
+
if (defaults)
|
|
16
|
+
result.push(...defaults);
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
13
19
|
}
|
|
14
20
|
/** Returns true when `xVal` falls inside any of a segment's x ranges. */
|
|
15
21
|
export function matchesSegmentX(xVal, seg) {
|
|
@@ -53,24 +59,21 @@ export function getDataForSegments(data, xAccessor, segs) {
|
|
|
53
59
|
});
|
|
54
60
|
}
|
|
55
61
|
/**
|
|
56
|
-
* Warns about obvious configuration mistakes:
|
|
57
|
-
*
|
|
58
|
-
*
|
|
62
|
+
* Warns about obvious configuration mistakes: multiple catch-all segments
|
|
63
|
+
* (no `areas`) in the same series list. Since catch-alls are evaluated last
|
|
64
|
+
* and in order, only the first one can ever match; later ones are dead code.
|
|
59
65
|
*/
|
|
60
66
|
export function validateSegments(segments) {
|
|
61
67
|
const check = (label, segs) => {
|
|
62
68
|
const catchAlls = segs.filter((s) => !s.areas || s.areas.length === 0).length;
|
|
63
69
|
if (catchAlls > 1) {
|
|
64
70
|
console.warn(`segments ${label}: ${catchAlls} entries have no areas — ` +
|
|
65
|
-
`
|
|
71
|
+
`only the first catch-all's style will be used.`);
|
|
66
72
|
}
|
|
67
73
|
};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
else {
|
|
72
|
-
for (const [name, segs] of Object.entries(segments)) {
|
|
73
|
-
check(`"${name}"`, segs);
|
|
74
|
+
for (const group of segments) {
|
|
75
|
+
for (const [key, segs] of Object.entries(group)) {
|
|
76
|
+
check(key === DEFAULT_KEY ? '(default)' : `"${key}"`, segs);
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
}
|
|
@@ -81,16 +84,29 @@ function resolveSeriesSegments(series, segs) {
|
|
|
81
84
|
}
|
|
82
85
|
const getX = resolveAccessor(series.x);
|
|
83
86
|
const getY = resolveAccessor(series.y);
|
|
87
|
+
// Evaluate area-specific segments before catch-alls (no areas), preserving
|
|
88
|
+
// relative order within each group. This lets a catch-all placed first in
|
|
89
|
+
// the list act as a fallback baseline rather than matching everything.
|
|
90
|
+
const evalOrder = segs
|
|
91
|
+
.map((seg, i) => ({ seg, i }))
|
|
92
|
+
.sort((a, b) => {
|
|
93
|
+
const aSpecific = a.seg.areas != null && a.seg.areas.length > 0;
|
|
94
|
+
const bSpecific = b.seg.areas != null && b.seg.areas.length > 0;
|
|
95
|
+
if (aSpecific === bSpecific)
|
|
96
|
+
return a.i - b.i;
|
|
97
|
+
return aSpecific ? -1 : 1;
|
|
98
|
+
});
|
|
84
99
|
// For each data point, find the first matching segment index (-1 = unmatched).
|
|
85
100
|
const matchIdx = series.data.map((d) => {
|
|
86
101
|
const xVal = getX(d);
|
|
87
102
|
const yVal = getY(d);
|
|
88
|
-
|
|
103
|
+
const found = evalOrder.find(({ seg }) => {
|
|
89
104
|
const areas = seg.areas;
|
|
90
105
|
if (!areas || areas.length === 0)
|
|
91
106
|
return true;
|
|
92
107
|
return areas.some((area) => areaMatchesXY(xVal, yVal, area));
|
|
93
108
|
});
|
|
109
|
+
return found !== undefined ? found.i : -1;
|
|
94
110
|
});
|
|
95
111
|
const runs = [];
|
|
96
112
|
for (let i = 0; i < series.data.length; i++) {
|
|
@@ -103,6 +119,16 @@ function resolveSeriesSegments(series, segs) {
|
|
|
103
119
|
runs.push({ data: [series.data[i]], segIdx: idx });
|
|
104
120
|
}
|
|
105
121
|
}
|
|
122
|
+
// Connector lines need to match the catch-all's stroke so they don't reveal
|
|
123
|
+
// the default palette color when the user has set a baseline via catch-all.
|
|
124
|
+
// Dots are excluded to avoid duplicate marks at the shared boundary points.
|
|
125
|
+
const catchAll = segs.find((s) => !s.areas || s.areas.length === 0);
|
|
126
|
+
const connectorStyle = (() => {
|
|
127
|
+
if (!catchAll?.style)
|
|
128
|
+
return {};
|
|
129
|
+
const { dots: _, ...rest } = catchAll.style;
|
|
130
|
+
return rest;
|
|
131
|
+
})();
|
|
106
132
|
const defaultVisual = [];
|
|
107
133
|
const connectors = [];
|
|
108
134
|
const userVisual = [];
|
|
@@ -129,7 +155,7 @@ function resolveSeriesSegments(series, segs) {
|
|
|
129
155
|
if (nextRun && nextRun.segIdx !== -1) {
|
|
130
156
|
connectors.push({
|
|
131
157
|
data: [run.data[run.data.length - 1], nextRun.data[0]],
|
|
132
|
-
style:
|
|
158
|
+
style: connectorStyle,
|
|
133
159
|
});
|
|
134
160
|
}
|
|
135
161
|
}
|
|
@@ -142,8 +168,8 @@ function resolveSeriesSegments(series, segs) {
|
|
|
142
168
|
}
|
|
143
169
|
/**
|
|
144
170
|
* Builds the full list of visual groups — one per series — ready to render.
|
|
145
|
-
* Each group's `visualSegments` is derived
|
|
146
|
-
*
|
|
171
|
+
* Each group's `visualSegments` is derived by resolving the flat segment list
|
|
172
|
+
* for that series from all groups in `segments`.
|
|
147
173
|
*/
|
|
148
174
|
export function buildGroupedSeries(seriesList, segments) {
|
|
149
175
|
return seriesList.map((s) => ({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Accessor, AxisValue, ScalesConfig, MarginConfig } from '../plots/common';
|
|
2
|
-
import type { PlotStyles,
|
|
2
|
+
import type { PlotStyles, Segments } from '../plots/styles';
|
|
3
3
|
import type { Marker } from '../plots/markers';
|
|
4
4
|
import type { TooltipProp } from '../layout/tooltip';
|
|
5
5
|
import type { LegendSection } from './legend';
|
|
@@ -25,7 +25,7 @@ export type ChartProps<TRow extends Record<string, unknown>, TSegmentStyle exten
|
|
|
25
25
|
y: Accessor<TRow, AxisValue>;
|
|
26
26
|
z?: Accessor<TRow, unknown>;
|
|
27
27
|
styles?: PlotStyles;
|
|
28
|
-
segments?:
|
|
28
|
+
segments?: Segments<TSegmentStyle>;
|
|
29
29
|
markers?: Marker[];
|
|
30
30
|
scales?: ScalesConfig;
|
|
31
31
|
margins?: MarginConfig;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Marker } from './markers';
|
|
2
2
|
import type { TooltipRuntime } from '../layout/tooltip';
|
|
3
|
-
import type { PlotStyles,
|
|
3
|
+
import type { PlotStyles, Segments } from './styles';
|
|
4
4
|
import type { SeriesProps, DataProps, ScalesConfig, MarginConfig } from './common';
|
|
5
5
|
/**
|
|
6
6
|
* Common props accepted by every plot component (bar, line, heatmap, pyramid,
|
|
@@ -15,7 +15,7 @@ export type PlotProps<TData extends Record<string, unknown>, TSegmentStyle exten
|
|
|
15
15
|
width: number;
|
|
16
16
|
height: number;
|
|
17
17
|
styles?: PlotStyles;
|
|
18
|
-
segments?:
|
|
18
|
+
segments?: Segments<TSegmentStyle>;
|
|
19
19
|
markers?: Marker[];
|
|
20
20
|
scales?: ScalesConfig;
|
|
21
21
|
margins?: MarginConfig;
|
|
@@ -134,11 +134,13 @@ export type Area = {
|
|
|
134
134
|
/**
|
|
135
135
|
* One styled rule for a plot. A data point (or cell) is styled when it falls
|
|
136
136
|
* inside **any** of the listed `areas`. Omitting `areas` (or passing an empty
|
|
137
|
-
* array) makes the segment
|
|
138
|
-
* `TStyle` is the per-mark shape ({@link LineSegmentStyle},
|
|
139
|
-
* {@link CellSegmentStyle}, …).
|
|
137
|
+
* array) makes the segment a catch-all that matches every data point — useful
|
|
138
|
+
* as a baseline style. `TStyle` is the per-mark shape ({@link LineSegmentStyle},
|
|
139
|
+
* {@link BarSegmentStyle}, {@link CellSegmentStyle}, …).
|
|
140
140
|
*
|
|
141
|
-
*
|
|
141
|
+
* Catch-alls are evaluated last so that area-specific segments always take
|
|
142
|
+
* priority, regardless of their position in the list. A catch-all placed first
|
|
143
|
+
* therefore acts as a fallback baseline that area-specific segments override.
|
|
142
144
|
*/
|
|
143
145
|
export type Segment<TStyle> = {
|
|
144
146
|
areas?: Area[];
|
|
@@ -147,17 +149,47 @@ export type Segment<TStyle> = {
|
|
|
147
149
|
/** Per-series list of {@link Segment}s, keyed by series name. */
|
|
148
150
|
export type SeriesAwareSegmentsMap<TStyle> = Record<string, Segment<TStyle>[]>;
|
|
149
151
|
/**
|
|
150
|
-
*
|
|
152
|
+
* A segment group that applies its segments to **every** series. Use the
|
|
153
|
+
* `'default'` key to target all series at once.
|
|
151
154
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* apply to every series.
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* const group: DefaultSegmentsMap<LineSegmentStyle> = {
|
|
158
|
+
* default: [{ areas: [{ x: { from: 2012, to: 2014 } }], style: { dots: { dotSymbol: 'circle' } } }],
|
|
159
|
+
* };
|
|
160
|
+
* ```
|
|
159
161
|
*/
|
|
160
|
-
export type
|
|
162
|
+
export type DefaultSegmentsMap<TStyle> = {
|
|
163
|
+
default: Segment<TStyle>[];
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* One element of a {@link Segments} array. Either a {@link DefaultSegmentsMap}
|
|
167
|
+
* (applies its segments to every series via the `'default'` key) or a
|
|
168
|
+
* {@link SeriesAwareSegmentsMap} (applies segments only to the named series).
|
|
169
|
+
* Both forms can be mixed in the same array.
|
|
170
|
+
*/
|
|
171
|
+
export type SegmentsStyle<TStyle> = DefaultSegmentsMap<TStyle> | SeriesAwareSegmentsMap<TStyle>;
|
|
172
|
+
/**
|
|
173
|
+
* The `segments` prop accepted by every plot kind.
|
|
174
|
+
*
|
|
175
|
+
* An array of {@link SegmentsStyle} groups processed in order. Each group maps
|
|
176
|
+
* either `'default'` (all series) or specific series names to lists of
|
|
177
|
+
* {@link Segment}s. Multiple groups let you organise segments by concern:
|
|
178
|
+
*
|
|
179
|
+
* ```ts
|
|
180
|
+
* segments: [
|
|
181
|
+
* // Group 1 – per-series stroke colours (catch-all baseline)
|
|
182
|
+
* { female: [{ style: { stroke: { stroke: 'steelblue' } } }],
|
|
183
|
+
* male: [{ style: { stroke: { stroke: 'tomato' } } }] },
|
|
184
|
+
* // Group 2 – shared dot symbols for every series
|
|
185
|
+
* { default: [{ areas: [{ x: { from: 2012, to: 2014 } }], style: { dots: { dotSymbol: 'circle' } } }] },
|
|
186
|
+
* ]
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* Within each resolved series list, catch-alls (no `areas`) are evaluated last
|
|
190
|
+
* so that area-specific segments always override them, regardless of position.
|
|
191
|
+
*/
|
|
192
|
+
export type Segments<TStyle> = SegmentsStyle<TStyle>[];
|
|
161
193
|
/** One contiguous, styled slice of a series' data — ready to draw as its own mark. */
|
|
162
194
|
export type VisualSegment<T extends Record<string, unknown>, TStyle = LineSegmentStyle> = {
|
|
163
195
|
data: T[];
|
package/package.json
CHANGED
|
@@ -1,77 +1,76 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
2
|
+
"name": "@fundar/data-chart-telling",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"sideEffects": [
|
|
9
|
+
"**/*.css"
|
|
10
|
+
],
|
|
11
|
+
"svelte": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"svelte": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"d3-interpolate": "^3.0.1"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"svelte": "^5.0.0",
|
|
25
|
+
"svelteplot": "^0.14.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@eslint/compat": "^2.0.4",
|
|
29
|
+
"@eslint/js": "^10.0.1",
|
|
30
|
+
"@sveltejs/package": "^2.5.7",
|
|
31
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
32
|
+
"@types/d3-interpolate": "^3.0.4",
|
|
33
|
+
"@types/node": "^22",
|
|
34
|
+
"@vitest/browser": "^4.1.7",
|
|
35
|
+
"@vitest/browser-playwright": "^4.1.7",
|
|
36
|
+
"eslint": "^10.2.0",
|
|
37
|
+
"eslint-config-prettier": "^10.1.8",
|
|
38
|
+
"eslint-plugin-svelte": "^3.17.0",
|
|
39
|
+
"globals": "^17.4.0",
|
|
40
|
+
"playwright": "^1.60.0",
|
|
41
|
+
"prettier": "^3.8.1",
|
|
42
|
+
"prettier-plugin-svelte": "^3.5.1",
|
|
43
|
+
"publint": "^0.3.18",
|
|
44
|
+
"svelte": "^5.55.2",
|
|
45
|
+
"svelte-check": "^4.4.6",
|
|
46
|
+
"svelteplot": "^0.14.0",
|
|
47
|
+
"typescript": "^6.0.2",
|
|
48
|
+
"typescript-eslint": "^8.58.1",
|
|
49
|
+
"vite": "^8.0.7",
|
|
50
|
+
"vitest": "^4.1.3",
|
|
51
|
+
"vitest-browser-svelte": "^2.1.1",
|
|
52
|
+
"storybook": "^10.4.1",
|
|
53
|
+
"@storybook/svelte-vite": "^10.4.1",
|
|
54
|
+
"@storybook/addon-svelte-csf": "^5.1.2",
|
|
55
|
+
"@chromatic-com/storybook": "^5.2.1",
|
|
56
|
+
"@storybook/addon-vitest": "^10.4.1",
|
|
57
|
+
"@storybook/addon-a11y": "^10.4.1",
|
|
58
|
+
"@storybook/addon-docs": "^10.4.1",
|
|
59
|
+
"eslint-plugin-storybook": "^10.4.1",
|
|
60
|
+
"@vitest/coverage-v8": "4.1.7"
|
|
61
|
+
},
|
|
62
|
+
"keywords": [
|
|
63
|
+
"svelte"
|
|
64
|
+
],
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "svelte-package",
|
|
67
|
+
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
68
|
+
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
|
69
|
+
"lint": "prettier --check . && eslint .",
|
|
70
|
+
"format": "prettier --write .",
|
|
71
|
+
"test": "vitest run --project=unit --project=browser",
|
|
72
|
+
"test:stories": "vitest run --project=storybook",
|
|
73
|
+
"storybook": "storybook dev -p 6006",
|
|
74
|
+
"build-storybook": "storybook build"
|
|
75
|
+
}
|
|
76
|
+
}
|