@fundar/data-chart-telling 0.0.4 → 0.0.6

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
 
@@ -31,7 +31,7 @@
31
31
  y,
32
32
  z,
33
33
  styles = {},
34
- segments = [],
34
+ segments = {},
35
35
  scales = {},
36
36
  margins,
37
37
  markers = [],
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';
@@ -27,7 +27,7 @@
27
27
  y,
28
28
  z,
29
29
  styles = {},
30
- segments = [],
30
+ segments = {},
31
31
  scales = {},
32
32
  margins,
33
33
  markers = [],
@@ -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>();
@@ -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.
5
+ * Series-specific segments are added before `'default'` segments so that
6
+ * series-specific catch-alls win over default catch-alls 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,14 @@
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.
5
+ * Series-specific segments are added before `'default'` segments so that
6
+ * series-specific catch-alls win over default catch-alls on tie-breaks.
10
7
  */
11
8
  export function resolveSegmentsForSeries(segments, seriesName) {
12
- return Array.isArray(segments) ? segments : (segments[seriesName] ?? []);
9
+ const specific = segments[seriesName] ?? [];
10
+ const defaults = segments[DEFAULT_KEY] ?? [];
11
+ return [...specific, ...defaults];
13
12
  }
14
13
  /** Returns true when `xVal` falls inside any of a segment's x ranges. */
15
14
  export function matchesSegmentX(xVal, seg) {
@@ -53,25 +52,20 @@ export function getDataForSegments(data, xAccessor, segs) {
53
52
  });
54
53
  }
55
54
  /**
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).
55
+ * Warns about obvious configuration mistakes: multiple catch-all segments
56
+ * (no `areas`) in the same series list. Since catch-alls are evaluated last
57
+ * and in order, only the first one can ever match; later ones are dead code.
59
58
  */
60
59
  export function validateSegments(segments) {
61
60
  const check = (label, segs) => {
62
61
  const catchAlls = segs.filter((s) => !s.areas || s.areas.length === 0).length;
63
62
  if (catchAlls > 1) {
64
63
  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.`);
64
+ `only the first catch-all's style will be used.`);
66
65
  }
67
66
  };
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
- }
67
+ for (const [key, segs] of Object.entries(segments)) {
68
+ check(key === DEFAULT_KEY ? '(default)' : `"${key}"`, segs);
75
69
  }
76
70
  }
77
71
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -81,16 +75,29 @@ function resolveSeriesSegments(series, segs) {
81
75
  }
82
76
  const getX = resolveAccessor(series.x);
83
77
  const getY = resolveAccessor(series.y);
78
+ // Evaluate area-specific segments before catch-alls (no areas), preserving
79
+ // relative order within each group. This lets a catch-all placed first in
80
+ // the list act as a fallback baseline rather than matching everything.
81
+ const evalOrder = segs
82
+ .map((seg, i) => ({ seg, i }))
83
+ .sort((a, b) => {
84
+ const aSpecific = a.seg.areas != null && a.seg.areas.length > 0;
85
+ const bSpecific = b.seg.areas != null && b.seg.areas.length > 0;
86
+ if (aSpecific === bSpecific)
87
+ return a.i - b.i;
88
+ return aSpecific ? -1 : 1;
89
+ });
84
90
  // For each data point, find the first matching segment index (-1 = unmatched).
85
91
  const matchIdx = series.data.map((d) => {
86
92
  const xVal = getX(d);
87
93
  const yVal = getY(d);
88
- return segs.findIndex((seg) => {
94
+ const found = evalOrder.find(({ seg }) => {
89
95
  const areas = seg.areas;
90
96
  if (!areas || areas.length === 0)
91
97
  return true;
92
98
  return areas.some((area) => areaMatchesXY(xVal, yVal, area));
93
99
  });
100
+ return found !== undefined ? found.i : -1;
94
101
  });
95
102
  const runs = [];
96
103
  for (let i = 0; i < series.data.length; i++) {
@@ -103,6 +110,16 @@ function resolveSeriesSegments(series, segs) {
103
110
  runs.push({ data: [series.data[i]], segIdx: idx });
104
111
  }
105
112
  }
113
+ // Connector lines need to match the catch-all's stroke so they don't reveal
114
+ // the default palette color when the user has set a baseline via catch-all.
115
+ // Dots are excluded to avoid duplicate marks at the shared boundary points.
116
+ const catchAll = segs.find((s) => !s.areas || s.areas.length === 0);
117
+ const connectorStyle = (() => {
118
+ if (!catchAll?.style)
119
+ return {};
120
+ const { dots: _, ...rest } = catchAll.style;
121
+ return rest;
122
+ })();
106
123
  const defaultVisual = [];
107
124
  const connectors = [];
108
125
  const userVisual = [];
@@ -120,7 +137,15 @@ function resolveSeriesSegments(series, segs) {
120
137
  defaultVisual.push({ data: bridged, style: {} });
121
138
  }
122
139
  else {
123
- userVisual.push({ data: run.data, style: segs[run.segIdx].style ?? {} });
140
+ const seg = segs[run.segIdx];
141
+ const segStyle = seg.style ?? {};
142
+ // Area-specific segments merge the catch-all as a baseline: catch-all
143
+ // keys fill in whatever the area segment omits; the area wins on conflict.
144
+ const isAreaSpecific = seg.areas != null && seg.areas.length > 0;
145
+ const resolvedStyle = isAreaSpecific && catchAll?.style
146
+ ? { ...catchAll.style, ...segStyle }
147
+ : segStyle;
148
+ userVisual.push({ data: run.data, style: resolvedStyle });
124
149
  // When the next run is also a user segment (no unmatched data between them),
125
150
  // insert a line-only connector so the lines join without a gap. Using a
126
151
  // separate mark (rather than appending a bridge point to the current run)
@@ -129,7 +154,7 @@ function resolveSeriesSegments(series, segs) {
129
154
  if (nextRun && nextRun.segIdx !== -1) {
130
155
  connectors.push({
131
156
  data: [run.data[run.data.length - 1], nextRun.data[0]],
132
- style: {},
157
+ style: connectorStyle,
133
158
  });
134
159
  }
135
160
  }
@@ -142,8 +167,8 @@ function resolveSeriesSegments(series, segs) {
142
167
  }
143
168
  /**
144
169
  * 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).
170
+ * Each group's `visualSegments` is derived by resolving the flat segment list
171
+ * for that series from all groups in `segments`.
147
172
  */
148
173
  export function buildGroupedSeries(seriesList, segments) {
149
174
  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,30 +134,41 @@ 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[];
145
147
  style?: TStyle;
146
148
  };
147
- /** Per-series list of {@link Segment}s, keyed by series name. */
148
- export type SeriesAwareSegmentsMap<TStyle> = Record<string, Segment<TStyle>[]>;
149
149
  /**
150
- * Unified segments prop accepted by every plot kind.
150
+ * The `segments` prop accepted by every plot kind.
151
151
  *
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.
152
+ * A record mapping series names (or `'default'` for all series) to lists of
153
+ * {@link Segment}s. Segments in a named key apply only to that series;
154
+ * segments under `'default'` apply to every series.
155
+ *
156
+ * ```ts
157
+ * segments: {
158
+ * female: [{ style: { stroke: { stroke: 'tomato' } } }], // catch-all for female
159
+ * male: [{ style: { stroke: { stroke: 'steelblue' } } }], // catch-all for male
160
+ * default: [{ areas: [{ x: { from: 2012, to: 2014 } }], style: { dots: { dotSymbol: 'circle' } } }],
161
+ * }
162
+ * ```
163
+ *
164
+ * Within each resolved series list, catch-alls (no `areas`) are evaluated last
165
+ * so that area-specific segments always override them, regardless of position.
166
+ * Area-specific segments also inherit any style key absent from their own style
167
+ * by merging the catch-all as a baseline — the area's own style wins on conflict.
168
+ * This lets you set a series colour once in a catch-all and add per-range dots
169
+ * or dash patterns without repeating the colour on every area segment.
159
170
  */
160
- export type SegmentsMap<TStyle> = Segment<TStyle>[] | SeriesAwareSegmentsMap<TStyle>;
171
+ export type Segments<TStyle> = Record<string, Segment<TStyle>[]>;
161
172
  /** One contiguous, styled slice of a series' data — ready to draw as its own mark. */
162
173
  export type VisualSegment<T extends Record<string, unknown>, TStyle = LineSegmentStyle> = {
163
174
  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.6",
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
+ }