@fundar/data-chart-telling 0.0.4 → 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 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` | `SegmentsMap<TStyle>` | `{}` / `[]` | ✅ | ✅ | ✅ | ✅ |
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` | `SegmentsMap<TStyle>` | `{}` / `[]` | ✅ | ✅ | ✅ | ✅ |
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` | `SegmentsMap<LineSegmentStyle>` |
181
- | `BarPlot` / `BarChart` | `SegmentsMap<BarSegmentStyle>` |
182
- | `PyramidPlot` / `PyramidChart` | `SegmentsMap<BarSegmentStyle>` |
183
- | `HeatmapPlot` / `HeatmapChart` | `SegmentsMap<CellSegmentStyle>` (array form only) |
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 = { from?: AxisValue; to?: AxisValue }; // half-open on either side
189
- type Area = { x?: Range; y?: Range }; // 2-D rectangular region
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` (or providing an empty array) makes the segment match **all** data useful as a baseline style. Segments are rendered in order; later ones appear on top.
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
- #### `SegmentsMap` forms
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
- // Array formsame segments apply to every series (required for heatmaps)
201
- type SegmentsMap<TStyle> = Segment<TStyle>[];
202
+ // 'default' keyapplies 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
- // Record formsegments keyed by series name (line / bar / pyramid)
204
- type SegmentsMap<TStyle> = SeriesAwareSegmentsMap<TStyle>;
205
- = Record<string, Segment<TStyle>[]>;
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 period and leave the rest as the palette default:**
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
- gdp: [
218
- { style: { stroke: { stroke: '#94a3b8', strokeWidth: 1 } } },
219
- { areas: [{ x: { from: 2008, to: 2010 } }],
220
- style: { stroke: { stroke: '#ef4444', strokeWidth: 3 } } },
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
- { areas: [{ x: { from: 'Q1', to: 'Q1' } }, { x: { from: 'Q3', to: 'Q3' } }],
233
- style: { stroke: '#f59e0b', strokeWidth: 2 } },
234
- { areas: [{ x: { from: 'Q2', to: 'Q2' }, y: { from: 1, to: 2 } }],
235
- style: { fill: '#ef4444', fillOpacity: 0.8 } },
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, SegmentsMap, ValueAnchor, ValuesStyle, ColorsStyle, PlotStyles, } from './types/plots/styles';
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';
@@ -30,7 +30,7 @@
30
30
  y,
31
31
  z,
32
32
  styles = {},
33
- segments = {},
33
+ segments = [],
34
34
  markers = [],
35
35
  scales = {},
36
36
  margins,
@@ -43,9 +43,12 @@
43
43
  'HeatmapPlot: markers scoped to a `series` are ignored — heatmaps have no series concept.',
44
44
  );
45
45
  }
46
- if (!Array.isArray(segments) && Object.keys(segments).length > 0) {
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
- 'HeatmapPlot: `segments` was passed as a record (SeriesAwareSegmentsMap) but heatmaps have no named series use the array form (Segment[]) instead.',
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(Array.isArray(segments) ? segments : []);
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>();
@@ -25,7 +25,7 @@
25
25
  y,
26
26
  z,
27
27
  styles = {},
28
- segments = {},
28
+ segments = [],
29
29
  markers = [],
30
30
  scales = {},
31
31
  margins,
@@ -40,7 +40,7 @@
40
40
  styles = {},
41
41
  scales = {},
42
42
  margins,
43
- segments = {},
43
+ segments = [],
44
44
  markers = [],
45
45
  tooltip = undefined,
46
46
  }: PlotProps<TData, BarSegmentStyle> = $props();
@@ -1,13 +1,11 @@
1
1
  import type { AxisValue, Series } from '../../types/plots/common';
2
- import type { Segment, SegmentsMap, SeriesAwareSegmentsMap, LineSegmentStyle, VisualGroup } from '../../types/plots/styles';
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 relevant for one series. If `segments` is
7
- * the global array form every series gets the same list; if it is the record
8
- * form only the entry keyed by `seriesName` is used (falling back to `[]`).
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: SegmentsMap<TStyle>, seriesName: string): Segment<TStyle>[];
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
- * - Multiple segments with no `areas` in the same series (they all match
25
- * everything; only the last style will be visible).
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: SegmentsMap<TStyle>): void;
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 from the series' entry in
31
- * `segments` (or the global array when `segments` is in array form).
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: SegmentsMap<TStyle>): VisualGroup<S, TStyle>[];
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
- /** Narrows a {@link SegmentsMap} to its per-series record form. */
3
- export function isSeriesAware(segments) {
4
- return !Array.isArray(segments);
5
- }
2
+ const DEFAULT_KEY = 'default';
6
3
  /**
7
- * Returns the {@link Segment} list relevant for one series. If `segments` is
8
- * the global array form every series gets the same list; if it is the record
9
- * form only the entry keyed by `seriesName` is used (falling back to `[]`).
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
- return Array.isArray(segments) ? segments : (segments[seriesName] ?? []);
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
- * - Multiple segments with no `areas` in the same series (they all match
58
- * everything; only the last style will be visible).
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
- `they each match every data point; only the last one's style will be visible.`);
71
+ `only the first catch-all's style will be used.`);
66
72
  }
67
73
  };
68
- if (Array.isArray(segments)) {
69
- check('(global)', segments);
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
- return segs.findIndex((seg) => {
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 from the series' entry in
146
- * `segments` (or the global array when `segments` is in array form).
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, SegmentsMap } from '../plots/styles';
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?: SegmentsMap<TSegmentStyle>;
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, SegmentsMap } from './styles';
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?: SegmentsMap<TSegmentStyle>;
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 match **all** data useful as a baseline style.
138
- * `TStyle` is the per-mark shape ({@link LineSegmentStyle}, {@link BarSegmentStyle},
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
- * Segments are applied in order; later ones are rendered on top of earlier ones.
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
- * Unified segments prop accepted by every plot kind.
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
- * - **Array form** (`Segment<TStyle>[]`): segments are applied to **all** series
153
- * (or to the implicit single dataset of a heatmap). Only form supported by
154
- * `HeatmapPlot` / `HeatmapChart`.
155
- * - **Record form** (`SeriesAwareSegmentsMap<TStyle>`): each key is a series
156
- * name; segments apply only to that series. Supported by line, bar, and
157
- * pyramid plots; when an array is passed to those plots the same segments
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 SegmentsMap<TStyle> = Segment<TStyle>[] | SeriesAwareSegmentsMap<TStyle>;
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
- "name": "@fundar/data-chart-telling",
3
- "version": "0.0.4",
4
- "type": "module",
5
- "scripts": {
6
- "build": "svelte-package",
7
- "prepack": "svelte-package && publint",
8
- "check": "svelte-check --tsconfig ./tsconfig.json",
9
- "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
10
- "lint": "prettier --check . && eslint .",
11
- "format": "prettier --write .",
12
- "test": "vitest run --project=unit --project=browser",
13
- "test:stories": "vitest run --project=storybook",
14
- "storybook": "storybook dev -p 6006",
15
- "build-storybook": "storybook build"
16
- },
17
- "files": [
18
- "dist"
19
- ],
20
- "sideEffects": [
21
- "**/*.css"
22
- ],
23
- "svelte": "./dist/index.js",
24
- "types": "./dist/index.d.ts",
25
- "exports": {
26
- ".": {
27
- "types": "./dist/index.d.ts",
28
- "svelte": "./dist/index.js",
29
- "default": "./dist/index.js"
30
- }
31
- },
32
- "dependencies": {
33
- "d3-interpolate": "^3.0.1"
34
- },
35
- "peerDependencies": {
36
- "svelte": "^5.0.0",
37
- "svelteplot": "^0.14.0"
38
- },
39
- "devDependencies": {
40
- "@eslint/compat": "^2.0.4",
41
- "@eslint/js": "^10.0.1",
42
- "@sveltejs/package": "^2.5.7",
43
- "@sveltejs/vite-plugin-svelte": "^7.0.0",
44
- "@types/d3-interpolate": "^3.0.4",
45
- "@types/node": "^22",
46
- "@vitest/browser": "^4.1.7",
47
- "@vitest/browser-playwright": "^4.1.7",
48
- "eslint": "^10.2.0",
49
- "eslint-config-prettier": "^10.1.8",
50
- "eslint-plugin-svelte": "^3.17.0",
51
- "globals": "^17.4.0",
52
- "playwright": "^1.60.0",
53
- "prettier": "^3.8.1",
54
- "prettier-plugin-svelte": "^3.5.1",
55
- "publint": "^0.3.18",
56
- "svelte": "^5.55.2",
57
- "svelte-check": "^4.4.6",
58
- "svelteplot": "^0.14.0",
59
- "typescript": "^6.0.2",
60
- "typescript-eslint": "^8.58.1",
61
- "vite": "^8.0.7",
62
- "vitest": "^4.1.3",
63
- "vitest-browser-svelte": "^2.1.1",
64
- "storybook": "^10.4.1",
65
- "@storybook/svelte-vite": "^10.4.1",
66
- "@storybook/addon-svelte-csf": "^5.1.2",
67
- "@chromatic-com/storybook": "^5.2.1",
68
- "@storybook/addon-vitest": "^10.4.1",
69
- "@storybook/addon-a11y": "^10.4.1",
70
- "@storybook/addon-docs": "^10.4.1",
71
- "eslint-plugin-storybook": "^10.4.1",
72
- "@vitest/coverage-v8": "4.1.7"
73
- },
74
- "keywords": [
75
- "svelte"
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
+ }