@fundar/data-chart-telling 1.9.0 → 1.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/configuration/config.svelte.js +1 -1
  3. package/dist/layout/base/BasePlotLayout.svelte +2 -1
  4. package/dist/layout/interaction/PlotInteraction.svelte.js +1 -1
  5. package/dist/layout/legend/DisabledStyle.d.ts +1 -1
  6. package/dist/layout/legend/DisabledStyle.js +9 -9
  7. package/dist/layout/plot/PlotContext.d.ts +3 -0
  8. package/dist/layout/plot/PlotContext.js +3 -0
  9. package/dist/layout/plot/PublicPlotContext.d.ts +2 -0
  10. package/dist/layout/plot/PublicPlotContext.js +4 -0
  11. package/dist/layout/styles/BasicStyles.d.ts +13 -0
  12. package/dist/layout/styles/BasicStyles.js +31 -0
  13. package/dist/markers/arrow/ArrowMarker.svelte +2 -0
  14. package/dist/markers/delta/Delta.d.ts +1 -1
  15. package/dist/markers/delta/Delta.js +2 -2
  16. package/dist/markers/delta/DeltaMarker.svelte +1 -1
  17. package/dist/markers/delta/DeltaPoint.svelte +31 -15
  18. package/dist/markers/rule/RuleMarker.svelte +2 -0
  19. package/dist/phases/data/DataPhase.js +5 -1
  20. package/dist/phases/transforms/TransformsPhase.js +2 -2
  21. package/dist/plots/correlation/heatmap/defaults.js +1 -1
  22. package/dist/plots/correlation/heatmap/markers.js +5 -1
  23. package/dist/plots/correlation/scatter/defaults.js +2 -1
  24. package/dist/plots/distribution/box/defaults.js +5 -1
  25. package/dist/plots/distribution/box/markers.js +9 -2
  26. package/dist/plots/distribution/density/defaults.js +7 -1
  27. package/dist/plots/distribution/density/markers.js +4 -0
  28. package/dist/plots/distribution/histogram/defaults.js +7 -1
  29. package/dist/plots/distribution/histogram/markers.js +10 -2
  30. package/dist/plots/distribution/pyramid/defaults.js +5 -1
  31. package/dist/plots/distribution/pyramid/markers.js +10 -1
  32. package/dist/plots/distribution/violin/defaults.js +5 -1
  33. package/dist/plots/distribution/violin/markers.js +4 -0
  34. package/dist/plots/evolution/area/defaults.js +3 -1
  35. package/dist/plots/evolution/area/markers.js +18 -9
  36. package/dist/plots/evolution/line/defaults.js +6 -1
  37. package/dist/plots/evolution/line/markers.d.ts +2 -2
  38. package/dist/plots/evolution/line/markers.js +5 -5
  39. package/dist/plots/evolution/slope/markers.js +1 -1
  40. package/dist/plots/ranking/bar/defaults.js +5 -1
  41. package/dist/plots/ranking/bar/markers.js +10 -2
  42. package/dist/plots/ranking/cleveland/ClevelandMarkers.js +37 -12
  43. package/dist/plots/ranking/cleveland/defaults.js +4 -2
  44. package/dist/plots/utils/segments.d.ts +3 -1
  45. package/dist/plots/utils/segments.js +40 -19
  46. package/dist/types/configuration/styling.d.ts +1 -1
  47. package/dist/types/layout/legend.d.ts +1 -1
  48. package/dist/types/layout/styles/base.d.ts +27 -1
  49. package/dist/types/markers/axis/delta.d.ts +4 -4
  50. package/dist/types/phases/data/phase.d.ts +3 -0
  51. package/dist/types/phases/state.d.ts +2 -0
  52. package/dist/types/phases/transforms/phase.d.ts +2 -0
  53. package/dist/types/plots/segments/common.d.ts +2 -2
  54. package/docs/markers.md +1 -1
  55. package/docs/plots/ranking/dumbbell.md +1 -1
  56. package/docs/plots/ranking/lollipop.md +1 -1
  57. package/docs/styles-and-segments.md +32 -2
  58. package/examples/SegmentsBySeries.svelte +1 -1
  59. package/package.json +1 -1
@@ -26,14 +26,23 @@ export function buildMarks(state, ctx) {
26
26
  };
27
27
  // The dim channel: each area reads its own series name, looked up in `opacityScale`.
28
28
  const dimByName = (name) => dimmed ? { opacity: () => name, scales: { opacity: opacityScale } } : {};
29
- const areas = (state.processedGroups ?? []).flatMap((group) => group.visualSegments.map((seg) => Transforms.finish({
30
- type: 'area',
31
- series: group.series.name,
32
- data: seg.data,
33
- ...channels,
34
- ...options,
35
- styles: seg.styles,
36
- ...dimByName(group.series.name)
37
- }, transforms)));
29
+ const areas = (state.processedGroups ?? []).flatMap((group) => group.visualSegments.map((seg) => {
30
+ // A stroke the segment sets is not a reason to lose the series' fill, as the marker would take it for the color.
31
+ const fill = seg.styles.fill ?? state.colorForSeries(group.series.name);
32
+ return Transforms.finish({
33
+ type: 'area',
34
+ series: group.series.name,
35
+ data: seg.data,
36
+ ...channels,
37
+ ...options,
38
+ styles: {
39
+ // An outline asked for without a color is the fill's.
40
+ ...(seg.styles.strokeWidth !== undefined ? { stroke: fill } : {}),
41
+ ...seg.styles,
42
+ fill
43
+ },
44
+ ...dimByName(group.series.name)
45
+ }, transforms);
46
+ }));
38
47
  return areas;
39
48
  }
@@ -3,7 +3,12 @@ import { ValueLabels } from '../../../layout/styles/ValueLabels';
3
3
  /** LinePlot on the shared pipeline: continuous segments colored by their stroke, and its own markers. Its plot defaults to: hover along x, cumulative timeline tweening `y` per series. */
4
4
  export function buildLineDefaults() {
5
5
  return {
6
- data: { segments: { color: (style) => style?.stroke?.stroke } },
6
+ data: {
7
+ segments: {
8
+ color: (style) => style?.stroke?.stroke,
9
+ base: { nested: ['stroke', 'dot'] }
10
+ }
11
+ },
7
12
  statistics: {
8
13
  // An unscoped marker takes the line's own stroke when its first segment restyles it,
9
14
  // else the first series' color.
@@ -10,7 +10,7 @@ import type { AxisBasedMarkersConfig } from '../../../types/markers/common';
10
10
  /**
11
11
  * Every `'line'` (+ `'dot'`, where a segment's style asks for them) marker
12
12
  * for one LinePlot render: one `'line'` marker per visual segment, plus a
13
- * `'dot'` marker right after it when that segment sets `style.dots`.
13
+ * `'dot'` marker right after it when that segment sets `style.dot`.
14
14
  */
15
15
  export declare function buildLineMarkers<TData extends Record<string, unknown>>(args: {
16
16
  groupedSeries: VisualGroup<Series<TData>, LineSegmentStyle>[];
@@ -21,7 +21,7 @@ export declare function buildLineMarkers<TData extends Record<string, unknown>>(
21
21
  curve?: CurveName;
22
22
  }): (LineMarkerConfig | DotMarkerConfig)[];
23
23
  /**
24
- * One `'line'` (+ `'dot'`, when its zone sets `style.dots`) marker per
24
+ * One `'line'` (+ `'dot'`, when its zone sets `style.dot`) marker per
25
25
  * detected, configured gap run across every series - the interpolated
26
26
  * bridge drawn over a missing-data hole. `gaps` is resolved per series the
27
27
  * same way `segments` is (a named entry, falling back to `'default'` at
@@ -3,7 +3,7 @@ import { resolveGapRuns, resolveGapsForSeries } from '../../utils/gaps';
3
3
  /**
4
4
  * Every `'line'` (+ `'dot'`, where a segment's style asks for them) marker
5
5
  * for one LinePlot render: one `'line'` marker per visual segment, plus a
6
- * `'dot'` marker right after it when that segment sets `style.dots`.
6
+ * `'dot'` marker right after it when that segment sets `style.dot`.
7
7
  */
8
8
  export function buildLineMarkers(args) {
9
9
  const { groupedSeries, colorFor, disabledStrokeFor, disabledDotsFor, curve } = args;
@@ -23,7 +23,7 @@ export function buildLineMarkers(args) {
23
23
  const y = group.series.y;
24
24
  for (const seg of group.visualSegments) {
25
25
  const strokeStyle = seg.styles.stroke;
26
- const dotStyle = seg.styles.dots;
26
+ const dotStyle = seg.styles.dot;
27
27
  markers.push({
28
28
  type: 'line',
29
29
  data: seg.data,
@@ -70,7 +70,7 @@ export function buildLineMarkers(args) {
70
70
  const gx = (p) => p.x;
71
71
  const gy = (p) => p.y;
72
72
  /**
73
- * One `'line'` (+ `'dot'`, when its zone sets `style.dots`) marker per
73
+ * One `'line'` (+ `'dot'`, when its zone sets `style.dot`) marker per
74
74
  * detected, configured gap run across every series - the interpolated
75
75
  * bridge drawn over a missing-data hole. `gaps` is resolved per series the
76
76
  * same way `segments` is (a named entry, falling back to `'default'` at
@@ -100,7 +100,7 @@ export function buildGapMarkers(args) {
100
100
  const disabledDots = disabledDotsFor(series.name);
101
101
  for (const run of runs) {
102
102
  const strokeStyle = run.zone.style?.stroke;
103
- const dotStyle = run.zone.style?.dots;
103
+ const dotStyle = run.zone.style?.dot;
104
104
  markers.push({
105
105
  type: 'line',
106
106
  data: run.points,
@@ -128,7 +128,7 @@ export function buildGapMarkers(args) {
128
128
  });
129
129
  if (!dotStyle)
130
130
  continue;
131
- const dotDefaults = gapDefaults.dots;
131
+ const dotDefaults = gapDefaults.dot;
132
132
  const endpoints = [run.points[0], run.points[run.points.length - 1]];
133
133
  markers.push({
134
134
  type: 'dot',
@@ -20,7 +20,7 @@ export function buildMarks(state, ctx) {
20
20
  styles: {
21
21
  ...segment.styles,
22
22
  stroke: { strokeWidth: slope.strokeWidth, ...segment.styles.stroke },
23
- dots: { dotRadius: slope.dotRadius, ...segment.styles.dots }
23
+ dot: { dotRadius: slope.dotRadius, ...segment.styles.dot }
24
24
  }
25
25
  }))
26
26
  }));
@@ -45,7 +45,11 @@ export function buildBarDefaults() {
45
45
  return {
46
46
  data: {
47
47
  discreteAxes: 1,
48
- segments: { color: (style) => style?.fill, connect: false }
48
+ segments: {
49
+ color: (style) => style?.fill,
50
+ connect: false,
51
+ base: { flat: 'area' }
52
+ }
49
53
  },
50
54
  scales: {
51
55
  zero: ['value'],
@@ -195,9 +195,17 @@ export function buildMarks(state, ctx) {
195
195
  const defaultColor = colorForSeries(name);
196
196
  const disabledFill = ctx.interaction?.fillFor(name);
197
197
  for (const seg of group.visualSegments) {
198
+ const fill = disabledFill?.fill ?? seg.styles.fill ?? defaultColor;
198
199
  const marker = makeMarker(seg.data.filter(isReal), slice, valueOf(group.series[valueAxis]), {
199
- fill: disabledFill?.fill ?? seg.styles.fill ?? defaultColor,
200
- fillOpacity: disabledFill?.fillOpacity ?? seg.styles.fillOpacity ?? 1
200
+ // An outline asked for without a color is the fill's.
201
+ ...(seg.styles.strokeWidth !== undefined ? { stroke: fill } : {}),
202
+ ...seg.styles,
203
+ fill,
204
+ fillOpacity: disabledFill?.fillOpacity ?? seg.styles.fillOpacity ?? 1,
205
+ // A dimmed series fades its outline with its fill.
206
+ ...(disabledFill?.fillOpacity !== undefined
207
+ ? { strokeOpacity: disabledFill.fillOpacity }
208
+ : {})
201
209
  });
202
210
  if (marker)
203
211
  segmentMarkers.push(marker);
@@ -4,6 +4,7 @@ import { SERIES_KEY } from '../../../phases/transforms/TransformsPhase';
4
4
  import { ValueLabels } from '../../../layout/styles/ValueLabels';
5
5
  import { ClevelandRanges } from './ClevelandRanges';
6
6
  import { ClevelandConnector } from './ClevelandConnector';
7
+ import { BasicStyles } from '../../../layout/styles/BasicStyles';
7
8
  /** The messages already warned about, so a plot that redraws warns once. */
8
9
  const warned = new Set();
9
10
  /**
@@ -45,14 +46,20 @@ export class ClevelandMarkers {
45
46
  const size = state.channels?.r;
46
47
  const dotRadius = typeof size === 'number' ? size : style.dotRadius;
47
48
  const neutral = style.connectorColor ?? cfg.axis.color;
49
+ // `styles.stroke` is the connectors' look below everything the author sets for them: a segment's
50
+ // stroke wins over `styles.connector`, which wins over it. A function of the series styles only
51
+ // the connector of a lone dot, which has a series; the connector of several dots has none.
52
+ const baseStroke = state.styles?.stroke;
48
53
  const looks = ranges.entries.map((entry) => {
49
54
  const range = connector.rangeOf(entry);
50
55
  const owner = range.series === undefined ? undefined : segmentOf.get(entry.dots[0].row);
51
56
  const stem = owner?.styles.stroke;
57
+ const base = BasicStyles.read(baseStroke, range.series);
52
58
  const disabled = owner && interaction?.strokeFor(owner.series);
53
59
  const strokeWidth = disabled?.strokeWidth ??
54
60
  stem?.strokeWidth ??
55
61
  connector.width(range, dotRadius) ??
62
+ base?.strokeWidth ??
56
63
  (style.stemWidth === 'dots' ? dotRadius * 2 : style.stemWidth);
57
64
  // The stretch between the `from` and `to` dots, in the connector's look...
58
65
  const [start, end] = range.span ?? [entry.from, entry.to];
@@ -65,9 +72,14 @@ export class ClevelandMarkers {
65
72
  stroke: disabled?.stroke ??
66
73
  stem?.stroke ??
67
74
  connector.color(range) ??
75
+ base?.stroke ??
68
76
  (owner ? state.colorForSeries(owner.series) : neutral),
69
77
  strokeWidth,
70
- strokeOpacity: disabled?.strokeOpacity ?? stem?.strokeOpacity ?? connector.opacity(range) ?? 1
78
+ strokeOpacity: disabled?.strokeOpacity ??
79
+ stem?.strokeOpacity ??
80
+ connector.opacity(range) ??
81
+ base?.strokeOpacity ??
82
+ 1
71
83
  };
72
84
  // ...as an arrow from one dot to the other, when the connector asks for one...
73
85
  const arrow = connector.arrow;
@@ -103,14 +115,19 @@ export class ClevelandMarkers {
103
115
  // A stretch drawn as an arrow is not one of the lines.
104
116
  rows: drawn ? beyond : [span, ...beyond],
105
117
  arrow: drawn,
106
- dash: disabled?.strokeDasharray ?? stem?.strokeDasharray
118
+ // What a stroke draws all of a marker's rows alike with, which a marker takes once for them.
119
+ pattern: ClevelandMarkers.#pattern(disabled?.strokeDasharray ?? stem?.strokeDasharray ?? base?.strokeDasharray, disabled?.strokeLinecap ?? stem?.strokeLinecap ?? base?.strokeLinecap, disabled?.strokeLinejoin ?? stem?.strokeLinejoin ?? base?.strokeLinejoin)
107
120
  };
108
121
  });
109
- // One rule marker per dash pattern, which a marker takes once for all its rows.
110
- const byDash = new Map();
111
- for (const look of looks)
112
- byDash.set(look.dash, [...(byDash.get(look.dash) ?? []), look]);
113
- const rules = [...byDash].map(([dash, group]) => ({
122
+ // One rule marker per dash, cap and join pattern, which a marker takes once for all its rows.
123
+ const byPattern = new Map();
124
+ for (const look of looks) {
125
+ const key = JSON.stringify(look.pattern);
126
+ const group = byPattern.get(key) ?? { pattern: look.pattern, looks: [] };
127
+ group.looks.push(look);
128
+ byPattern.set(key, group);
129
+ }
130
+ const rules = [...byPattern.values()].map(({ pattern, looks: group }) => ({
114
131
  type: 'rule',
115
132
  // A connector is not what the series look like: its dots are.
116
133
  legend: false,
@@ -121,11 +138,11 @@ export class ClevelandMarkers {
121
138
  stroke: 'stroke',
122
139
  strokeWidth: 'strokeWidth',
123
140
  strokeOpacity: 'strokeOpacity',
124
- ...(dash ? { styles: { strokeDasharray: dash } } : {})
141
+ ...(Object.keys(pattern).length > 0 ? { styles: pattern } : {})
125
142
  }));
126
- // The arrows, one marker per dash pattern like the lines, each from its `from` dot to its `to` dot.
143
+ // The arrows, one marker per pattern like the lines, each from its `from` dot to its `to` dot.
127
144
  const head = connector.arrow;
128
- const arrows = [...byDash].flatMap(([dash, group]) => {
145
+ const arrows = [...byPattern.values()].flatMap(({ pattern, looks: group }) => {
129
146
  const rows = group.flatMap((look) => (look.arrow ? [look.arrow] : []));
130
147
  if (rows.length === 0 || !head)
131
148
  return [];
@@ -145,7 +162,7 @@ export class ClevelandMarkers {
145
162
  stroke: 'stroke',
146
163
  strokeWidth: 'strokeWidth',
147
164
  strokeOpacity: 'strokeOpacity',
148
- ...(dash ? { styles: { strokeDasharray: dash } } : {})
165
+ ...(Object.keys(pattern).length > 0 ? { styles: pattern } : {})
149
166
  }
150
167
  ];
151
168
  });
@@ -161,7 +178,7 @@ export class ClevelandMarkers {
161
178
  const disabled = interaction?.dotsFor(name);
162
179
  return group.visualSegments.map((segment) => {
163
180
  const stem = segment.styles.stroke;
164
- const dot = segment.styles.dots;
181
+ const dot = segment.styles.dot;
165
182
  return {
166
183
  type: 'dot',
167
184
  series: name,
@@ -188,6 +205,14 @@ export class ClevelandMarkers {
188
205
  ...this.#connectorLabels(state, ctx, connector, looks, isHorizontal)
189
206
  ];
190
207
  };
208
+ /** The dash, cap and join a connector is drawn with, leaving out what is not set. */
209
+ static #pattern(strokeDasharray, strokeLinecap, strokeLinejoin) {
210
+ return {
211
+ ...(strokeDasharray ? { strokeDasharray } : {}),
212
+ ...(strokeLinecap ? { strokeLinecap } : {}),
213
+ ...(strokeLinejoin ? { strokeLinejoin } : {})
214
+ };
215
+ }
191
216
  /** The caps of the connectors: a tick at each end of those that have a length, one marker per stroke look. */
192
217
  #caps(entries, looks, length, isHorizontal) {
193
218
  const byLook = new Map();
@@ -39,8 +39,10 @@ export function buildClevelandDefaults(options = { name: 'ClevelandPlot', config
39
39
  data: {
40
40
  discreteAxes: 1,
41
41
  segments: {
42
- color: (style) => style?.dots?.dotFill ?? style?.stroke?.stroke,
43
- connect: false
42
+ color: (style) => style?.dot?.dotFill ?? style?.stroke?.stroke,
43
+ connect: false,
44
+ // The stem's `styles.stroke` is not a segment's: a connector the author styles wins over it (see `ClevelandMarkers`).
45
+ base: { nested: ['dot'] }
44
46
  }
45
47
  },
46
48
  scales: {
@@ -24,7 +24,7 @@ export declare function matchesSegmentX<TStyle>(xVal: AxisValue, seg: Segment<TS
24
24
  * which it overrides in its own panels.
25
25
  */
26
26
  export declare function validateSegments<TStyle>(segments: SegmentsConfig<TStyle>): void;
27
- export declare function buildGroupedSeries<S extends Series<any>, TStyle = LineSegmentStyle>(seriesList: S[], segments?: SegmentsConfig<TStyle>, connect?: boolean): VisualGroup<S, TStyle>[];
27
+ export declare function buildGroupedSeries<S extends Series<any>, TStyle = LineSegmentStyle>(seriesList: S[], segments?: SegmentsConfig<TStyle>, connect?: boolean, baseFor?: (seriesName: string) => TStyle | undefined): VisualGroup<S, TStyle>[];
28
28
  /**
29
29
  * Builds the `ScopedMarkerGroup[]` a `Plot.svelte` hands to `GeometryLayout`
30
30
  * for rendering `series`-scoped `hover`/`component` markers - one group per
@@ -39,4 +39,6 @@ export declare function buildScopedMarkerGroups<TData extends Record<string, unk
39
39
  segments?: SegmentsConfig<TStyle>;
40
40
  colorFor: (series: S, seriesIndex: number) => string;
41
41
  segmentColorFor: (styles: TStyle | undefined, defaultColor: string) => string;
42
+ /** The style each series' segments are drawn over, as `buildGroupedSeries` takes it. */
43
+ baseFor?: (seriesName: string) => TStyle | undefined;
42
44
  }): ScopedMarkerGroup<TData>[];
@@ -75,9 +75,17 @@ export function validateSegments(segments) {
75
75
  }
76
76
  }
77
77
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
- function resolveSeriesSegments(series, segs, connect) {
78
+ function resolveSeriesSegments(series, segmentsOfSeries, connect, base) {
79
+ // The style every segment is drawn over: each one's own wins, field by field.
80
+ const baseStyle = (base ?? {});
81
+ const segs = base
82
+ ? segmentsOfSeries.map((seg) => ({
83
+ ...seg,
84
+ styles: merge(base, (seg.styles ?? {}))
85
+ }))
86
+ : segmentsOfSeries;
79
87
  if (!segs.length) {
80
- return [{ data: series.data, styles: {}, scales: undefined }];
88
+ return [{ data: series.data, styles: baseStyle, scales: undefined }];
81
89
  }
82
90
  const getX = readChannel(series.x);
83
91
  const getY = readChannel(series.y);
@@ -121,10 +129,8 @@ function resolveSeriesSegments(series, segs, connect) {
121
129
  // Dots are excluded to avoid duplicate marks at the shared boundary points.
122
130
  const catchAll = segs.find((s) => !s.areas || s.areas.length === 0);
123
131
  const connectorStyle = (() => {
124
- if (!catchAll?.styles)
125
- return {};
126
- const rest = { ...catchAll.styles };
127
- delete rest.dots;
132
+ const rest = { ...(catchAll?.styles ?? baseStyle) };
133
+ delete rest.dot;
128
134
  return rest;
129
135
  })();
130
136
  const defaultVisual = [];
@@ -133,19 +139,33 @@ function resolveSeriesSegments(series, segs, connect) {
133
139
  for (let r = 0; r < runs.length; r++) {
134
140
  const run = runs[r];
135
141
  if (run.segIdx === -1) {
136
- // Unmatched run: for a continuous mark (line), insert bridge points so
137
- // it connects to adjacent user segments without a visual gap. Discrete
138
- // marks (bar, pyramid) have no such gap to bridge - bridging would just
142
+ // Unmatched run: drawn with its own rows only. For a continuous mark
143
+ // (line), line-only connectors join it to the adjacent user segments
144
+ // without a visual gap, like those between two user segments, so no
145
+ // neighbor's boundary row is drawn - or dotted - twice. Discrete marks
146
+ // (bar, pyramid) have no such gap to bridge - bridging would just
139
147
  // duplicate the neighboring runs' boundary rows as an extra, wrongly
140
148
  // styled mark at that same category.
141
- const bridged = [...run.data];
149
+ defaultVisual.push({ data: run.data, styles: baseStyle, scales: undefined });
142
150
  if (connect) {
143
- if (r > 0)
144
- bridged.unshift(runs[r - 1].data[runs[r - 1].data.length - 1]);
145
- if (r < runs.length - 1)
146
- bridged.push(runs[r + 1].data[0]);
151
+ const first = run.data[0];
152
+ const last = run.data[run.data.length - 1];
153
+ if (r > 0) {
154
+ const before = runs[r - 1].data;
155
+ connectors.push({
156
+ data: [before[before.length - 1], first],
157
+ styles: connectorStyle,
158
+ scales: undefined
159
+ });
160
+ }
161
+ if (r < runs.length - 1) {
162
+ connectors.push({
163
+ data: [last, runs[r + 1].data[0]],
164
+ styles: connectorStyle,
165
+ scales: undefined
166
+ });
167
+ }
147
168
  }
148
- defaultVisual.push({ data: bridged, styles: {}, scales: undefined });
149
169
  }
150
170
  else {
151
171
  const seg = segs[run.segIdx];
@@ -196,10 +216,10 @@ function resolveSeriesSegments(series, segs, connect) {
196
216
  function readChannel(accessor) {
197
217
  return accessor === undefined ? () => undefined : resolveAccessor(accessor);
198
218
  }
199
- export function buildGroupedSeries(seriesList, segments = {}, connect = true) {
219
+ export function buildGroupedSeries(seriesList, segments = {}, connect = true, baseFor) {
200
220
  return seriesList.map((s) => ({
201
221
  series: s,
202
- visualSegments: resolveSeriesSegments(s, resolveSegmentsForSeries(segments, s), connect)
222
+ visualSegments: resolveSeriesSegments(s, resolveSegmentsForSeries(segments, s), connect, baseFor?.(s.name))
203
223
  }));
204
224
  }
205
225
  /**
@@ -212,11 +232,12 @@ export function buildGroupedSeries(seriesList, segments = {}, connect = true) {
212
232
  * that isn't shared across plot kinds.
213
233
  */
214
234
  export function buildScopedMarkerGroups(args) {
215
- const { groupedSeries, segments = {}, colorFor, segmentColorFor } = args;
235
+ const { groupedSeries, segments = {}, colorFor, segmentColorFor, baseFor } = args;
216
236
  return groupedSeries.map((group, seriesIndex) => {
217
237
  const defaultColor = colorFor(group.series, seriesIndex);
218
238
  const xFor = readChannel(group.series.x);
219
239
  const segs = resolveSegmentsForSeries(segments, group.series);
240
+ const base = baseFor?.(group.series.name);
220
241
  return {
221
242
  name: group.series.name,
222
243
  ...(group.series.id !== undefined ? { id: group.series.id } : {}),
@@ -225,7 +246,7 @@ export function buildScopedMarkerGroups(args) {
225
246
  color: defaultColor,
226
247
  segments: segs.map((seg) => ({
227
248
  match: (row) => matchesSegmentX(xFor(row), seg),
228
- color: segmentColorFor(seg.styles, defaultColor)
249
+ color: segmentColorFor(base ? merge(base, (seg.styles ?? {})) : seg.styles, defaultColor)
229
250
  }))
230
251
  };
231
252
  });
@@ -454,7 +454,7 @@ export type PlotStylesConfig = {
454
454
  */
455
455
  gap: {
456
456
  stroke: StrokeStyle;
457
- dots: DotStyle;
457
+ dot: DotStyle;
458
458
  };
459
459
  };
460
460
  /** Default lollipop-plot styling. */
@@ -11,7 +11,7 @@ export type LegendSectionType = 'discrete' | 'continuous' | 'size' | 'symbol';
11
11
  export type LegendDisabledStyle = {
12
12
  opacity?: number;
13
13
  stroke?: StrokeStyle;
14
- dots?: DotStyle;
14
+ dot?: DotStyle;
15
15
  fill?: string;
16
16
  fillOpacity?: number;
17
17
  };
@@ -1,5 +1,6 @@
1
1
  import type { FrameProp } from './frame';
2
2
  import type { ValuesStyle } from './values';
3
+ import type { StrokeStyle, DotStyle, AreaStyle } from '../../plots/styling';
3
4
  /**
4
5
  * The fields every plot kind's own `styles` shape shares - the common parent
5
6
  * of {@link AxisBasedStylesConfig} and {@link GeoStylesConfig}.
@@ -8,11 +9,36 @@ export type BasePlotStylesConfig = {
8
9
  /** Decorative border around the plot's content box - see {@link FrameStyle}. Available on every plot kind. */
9
10
  frame?: FrameProp;
10
11
  };
12
+ /** A style as it is for every series, or a function of the series' name - `undefined` leaves that series as it is. */
13
+ export type PerSeries<TStyle> = TStyle | ((series: string) => TStyle | undefined);
14
+ /**
15
+ * The basic styles any plot's marks are made of, as the style every series is
16
+ * drawn with, before its `segments` restyle a stretch of it. Each takes a style
17
+ * as it is, or a function of the series' name, to set one series or two apart.
18
+ * A plot uses the ones its own marks are made of and ignores the rest, so one
19
+ * `styles` can be shared between plots: `stroke` and `dot` style a line plot's
20
+ * lines and dots, `area` a bar's. What a segment sets wins over them, field by
21
+ * field; whatever a legend turned off wins over both.
22
+ */
23
+ export type BasicStylesConfig = {
24
+ /** The line of every series: its width, opacity, dashes... The color comes from the `color` scale unless set here. */
25
+ stroke?: PerSeries<StrokeStyle>;
26
+ /** The dots of every series. Unset, a series has none but those a segment gives it. */
27
+ dot?: PerSeries<DotStyle>;
28
+ /** The area of every series - a bar, a band: its fill, opacity, outline... */
29
+ area?: PerSeries<AreaStyle>;
30
+ };
31
+ /** The basic styles a plot kind's segment style is made of: nested in it under their own names (a line's `{ stroke, dot }`), or the style itself (an area's `AreaStyle`). */
32
+ export type BasicStylesShape = {
33
+ nested: (keyof BasicStylesConfig)[];
34
+ } | {
35
+ flat: keyof BasicStylesConfig;
36
+ };
11
37
  /**
12
38
  * Top-level rendering style overrides shared by the non-geo plot kinds.
13
39
  * Passed as the `styles` prop. Each plot reads only the fields it cares
14
40
  * about.
15
41
  */
16
- export type AxisBasedStylesConfig = BasePlotStylesConfig & {
42
+ export type AxisBasedStylesConfig = BasePlotStylesConfig & BasicStylesConfig & {
17
43
  values?: ValuesStyle;
18
44
  };
@@ -15,7 +15,7 @@ export type DeltaConfig = {
15
15
  from: DeltaTarget;
16
16
  to?: DeltaTarget;
17
17
  formatDiff?: (diff: number) => string;
18
- line?: StrokeStyle;
18
+ stroke?: StrokeStyle;
19
19
  text?: FontStyle;
20
20
  dot?: DotStyle;
21
21
  };
@@ -29,12 +29,12 @@ export type DeltaPointConfig = {
29
29
  export type PerPosition<T> = T | ((at: AxisValue) => T);
30
30
  /**
31
31
  * A delta annotation between two points on a line chart. `at` can be a list of positions: one delta is
32
- * drawn at each, with the same ends. `line`, `text` and `dot` can be functions of the position, to style
32
+ * drawn at each, with the same ends. `stroke`, `text` and `dot` can be functions of the position, to style
33
33
  * each delta on its own - a label to the left of the last one, say, so it stays inside the plot.
34
34
  */
35
- export type DeltaMarkerConfig = Omit<DeltaPointConfig, 'at' | 'line' | 'text' | 'dot'> & {
35
+ export type DeltaMarkerConfig = Omit<DeltaPointConfig, 'at' | 'stroke' | 'text' | 'dot'> & {
36
36
  at: AxisValue | AxisValue[];
37
- line?: PerPosition<StrokeStyle>;
37
+ stroke?: PerPosition<StrokeStyle>;
38
38
  text?: PerPosition<FontStyle>;
39
39
  dot?: PerPosition<DotStyle>;
40
40
  };
@@ -1,11 +1,14 @@
1
1
  import type { Series } from '../../plots/data/common';
2
2
  import type { DiscreteAxesCount } from './axes';
3
+ import type { BasicStylesShape } from '../../layout/styles';
3
4
  /** How a plot kind's own mark splits into visual segments. */
4
5
  export type SegmentsStageOptions = {
5
6
  /** The color a segment's own style sets, if any, for the markers scoped to its series. */
6
7
  color?: (style: any) => string | undefined;
7
8
  /** Whether adjacent visual segments share their boundary row - a continuous mark (line, area) does. Defaults to `true`. */
8
9
  connect?: boolean;
10
+ /** The basic styles (`styles.stroke`, `styles.dot`, `styles.area`) this kind's segment style is made of, which every segment of a series is drawn over. A segment's own style wins over them, field by field. Left out, the kind draws none of them. */
11
+ base?: BasicStylesShape;
9
12
  };
10
13
  /**
11
14
  * What differs between plots in the shared "data" stage. Everything else -
@@ -61,6 +61,8 @@ export type AxisPlotState<TData extends Record<string, unknown>, TStyle extends
61
61
  /** Every series name, before legend-omitted ones are dropped, so colors stay put. */
62
62
  allSeriesNames?: string[];
63
63
  groupedSeries?: VisualGroup<TSeries, TStyle>[];
64
+ /** The style every segment of a series is drawn over (see `SegmentsStageOptions.base`), which the "transforms" phase cuts its own groups with too. */
65
+ segmentBase?: (series: string) => TStyle | undefined;
64
66
  flat?: TRow[];
65
67
  scopedGroups?: ScopedMarkerGroup<TData>[];
66
68
  colorForSeries?: (name: string) => string;
@@ -26,6 +26,8 @@ export type ProcessedData = {
26
26
  /** What the shared "transforms" stage reads from and writes to a plot's state. */
27
27
  export type TransformsStageState = {
28
28
  resolvedSeries?: Series<any>[];
29
+ /** The style every segment of a series is drawn over, which the processed groups are cut with too. */
30
+ segmentBase?: (series: string) => any;
29
31
  axes?: PlotAxes;
30
32
  /** Each series' gap fills, by series id: the value each missing row is drawn at. */
31
33
  gapFillMaps?: Map<string, Map<unknown, {
@@ -1,11 +1,11 @@
1
1
  import type { StrokeStyle, DotStyle, AreaStyle } from '../styling';
2
2
  import type { CurveName } from '../../layout/styles/curve';
3
- /** Per-segment styling for a line: its curve, its stroke and/or its endpoint dots. */
3
+ /** Per-segment styling for a line: its curve, its stroke and/or its dots. */
4
4
  export type LineSegmentStyle = {
5
5
  /** How the segment joins its points; `'step'` and its kin draw stairs. Falls back to `styles.curve`, then `'linear'`. */
6
6
  curve?: CurveName;
7
7
  stroke?: StrokeStyle;
8
- dots?: DotStyle;
8
+ dot?: DotStyle;
9
9
  };
10
10
  /** Per-segment styling for a bar-shaped mark. Shared by `BarPlot` and `PyramidPlot`. */
11
11
  export type BarSegmentStyle = AreaStyle;
package/docs/markers.md CHANGED
@@ -102,7 +102,7 @@ markers={[
102
102
  ]}
103
103
  ```
104
104
 
105
- A delta's `line`, `text` and `dot` can be functions of the position, to style each one on its own - a label to the left of the last one, say, so it stays inside the plot:
105
+ A delta's `stroke`, `text` and `dot` can be functions of the position, to style each one on its own - a label to the left of the last one, say, so it stays inside the plot:
106
106
 
107
107
  ```ts
108
108
  {
@@ -35,5 +35,5 @@ Two dots per category, one for each of two series, joined by a connector: the ga
35
35
  - `r` sizes the dots by a field (`r="population"`; area follows the value, and the legend gets a size section) or fixes their radius in pixels (`r={8}`).
36
36
  - The connector is a neutral gray as thick as the dots are wide (`stemWidth: 'dots'`), a band; its default color and width, the dot radius and the padding come from `dumbbell` in the [configuration](../../theming.md). `styles.connector` restyles it per category (see [Connectors](cleveland.md#connectors)). `styles.connector.arrow` draws it as an arrow from one series to the other, pointing the way the value moved.
37
37
  - `styles.values` writes each value past its dot, away from the other one. A label past the lowest dot can reach the axis labels when that dot sits at the edge of the axis; pin `scales.y.domain` (or `x`) wide enough to leave it room.
38
- - `segments` restyle the dots of chosen categories (`styles: { dots: { dotRadius: 9 } }`, `areas: [{ x: { from: 'Salta', to: 'Salta' } }]`).
38
+ - `segments` restyle the dots of chosen categories (`styles: { dot: { dotRadius: 9 } }`, `areas: [{ x: { from: 'Salta', to: 'Salta' } }]`).
39
39
  - The tooltip reads one category at a time, listing its two values.
@@ -35,5 +35,5 @@ One value per category, drawn as a dot on a thin stem that rises from the baseli
35
35
  - The value axis reaches zero, where the stems start; pinning `scales.y.domain` moves the baseline to the nearest end of it, and a negative value hangs below zero.
36
36
  - `scales.x.paddingInner` / `paddingOuter` set the room around each lollipop; the defaults, with the dot radius and the stem width (a thin 2 px line), come from `lollipop` in the [configuration](../../theming.md).
37
37
  - `styles.values` writes each value just past its dot.
38
- - `segments` restyle whole categories: `styles: { stroke: {...}, dots: {...} }` restyle their stems and dots (`areas: [{ x: { from: 'Salta', to: 'Salta' } }]`).
38
+ - `segments` restyle whole categories: `styles: { stroke: {...}, dot: {...} }` restyle their stems and dots (`areas: [{ x: { from: 'Salta', to: 'Salta' } }]`).
39
39
  - The tooltip reads one lollipop at a time, along the category axis.