@fundar/data-chart-telling 1.9.0 → 1.10.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 (48) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/configuration/config.svelte.js +1 -1
  3. package/dist/layout/interaction/PlotInteraction.svelte.js +1 -1
  4. package/dist/layout/legend/DisabledStyle.d.ts +1 -1
  5. package/dist/layout/legend/DisabledStyle.js +9 -9
  6. package/dist/layout/styles/BasicStyles.d.ts +13 -0
  7. package/dist/layout/styles/BasicStyles.js +31 -0
  8. package/dist/markers/arrow/ArrowMarker.svelte +2 -0
  9. package/dist/markers/rule/RuleMarker.svelte +2 -0
  10. package/dist/phases/data/DataPhase.js +5 -1
  11. package/dist/phases/transforms/TransformsPhase.js +2 -2
  12. package/dist/plots/correlation/heatmap/defaults.js +1 -1
  13. package/dist/plots/correlation/heatmap/markers.js +5 -1
  14. package/dist/plots/correlation/scatter/defaults.js +2 -1
  15. package/dist/plots/distribution/box/defaults.js +5 -1
  16. package/dist/plots/distribution/box/markers.js +9 -2
  17. package/dist/plots/distribution/density/defaults.js +7 -1
  18. package/dist/plots/distribution/density/markers.js +4 -0
  19. package/dist/plots/distribution/histogram/defaults.js +7 -1
  20. package/dist/plots/distribution/histogram/markers.js +10 -2
  21. package/dist/plots/distribution/pyramid/defaults.js +5 -1
  22. package/dist/plots/distribution/pyramid/markers.js +10 -1
  23. package/dist/plots/distribution/violin/defaults.js +5 -1
  24. package/dist/plots/distribution/violin/markers.js +4 -0
  25. package/dist/plots/evolution/area/defaults.js +3 -1
  26. package/dist/plots/evolution/area/markers.js +18 -9
  27. package/dist/plots/evolution/line/defaults.js +6 -1
  28. package/dist/plots/evolution/line/markers.d.ts +2 -2
  29. package/dist/plots/evolution/line/markers.js +5 -5
  30. package/dist/plots/evolution/slope/markers.js +1 -1
  31. package/dist/plots/ranking/bar/defaults.js +5 -1
  32. package/dist/plots/ranking/bar/markers.js +10 -2
  33. package/dist/plots/ranking/cleveland/ClevelandMarkers.js +37 -12
  34. package/dist/plots/ranking/cleveland/defaults.js +4 -2
  35. package/dist/plots/utils/segments.d.ts +3 -1
  36. package/dist/plots/utils/segments.js +40 -19
  37. package/dist/types/configuration/styling.d.ts +1 -1
  38. package/dist/types/layout/legend.d.ts +1 -1
  39. package/dist/types/layout/styles/base.d.ts +27 -1
  40. package/dist/types/phases/data/phase.d.ts +3 -0
  41. package/dist/types/phases/state.d.ts +2 -0
  42. package/dist/types/phases/transforms/phase.d.ts +2 -0
  43. package/dist/types/plots/segments/common.d.ts +2 -2
  44. package/docs/plots/ranking/dumbbell.md +1 -1
  45. package/docs/plots/ranking/lollipop.md +1 -1
  46. package/docs/styles-and-segments.md +32 -2
  47. package/examples/SegmentsBySeries.svelte +1 -1
  48. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## [1.10.0] - 2026-11-03
6
+
7
+ ### Added
8
+
9
+ - `styles.stroke`, `styles.dot` and `styles.area` on every axis-based plot: the style every series is drawn with, each as it is or as a function of the series' name. A plot uses the ones its own marks are made of and ignores the rest: `stroke` and `dot` on `LinePlot`, `SlopePlot`, `SparklinePlot` and the plots with connectors (`LollipopPlot`, `DumbbellPlot`, `ClevelandPlot`, `ForestPlot`); `area` on `AreaPlot`, `BarPlot`, `BulletPlot`, `PyramidPlot`, `HistogramPlot`, `BoxPlot`, `DensityPlot`, `ViolinPlot` and `HeatmapPlot`; `dot` on `ScatterPlot`. A segment's own style wins over them field by field, a connector styled with `styles.connector` wins over `styles.stroke`, and the connectors of those plots - and their arrows - take its dashes and line caps and joins (`strokeDasharray`, `strokeLinecap`, `strokeLinejoin`) from it or from a segment, and a series a legend turned off dims over all of them. Before, restyling every series - or one of them - took a `segments` entry per series.
10
+
11
+ ### Fixed
12
+
13
+ - The outline an area style sets - `stroke`, `strokeWidth`, `strokeOpacity`, `strokeDasharray` and `borderRadius` - reaches the bars of `BarPlot`, `BulletPlot` and `PyramidPlot` and the bins of `HistogramPlot`, whether it comes from a segment or from `styles.area`. These plots read only the fill of a segment's style, so the rest was ignored; a series a legend dims fades its outline with its fill.
14
+
15
+ ### Changed
16
+
17
+ - **Breaking:** the dots of a line's style are `dot`, not `dots`, so they read like `styles.dot`: `LineSegmentStyle.dots` (in `segments` and in the `styles.gaps` zones), the legend's `LegendDisabledStyle.dots` and `line.gap.dots` in the configuration. Rename them where they are set.
18
+
5
19
  ## [1.9.0] - 2026-11-03
6
20
 
7
21
  ### Changed
@@ -303,7 +303,7 @@ export const DEFAULT_CONFIG = {
303
303
  dotRadius: 4,
304
304
  gap: {
305
305
  stroke: { strokeOpacity: 0.6, strokeDasharray: '4 4' },
306
- dots: {}
306
+ dot: {}
307
307
  }
308
308
  },
309
309
  lollipop: {
@@ -47,7 +47,7 @@ export class PlotInteraction {
47
47
  }
48
48
  /** `series`' dots while dimmed. */
49
49
  dotsFor(series) {
50
- return DisabledStyle.dots(this.dimmed(series));
50
+ return DisabledStyle.dot(this.dimmed(series));
51
51
  }
52
52
  /** Isolates `series` - every other one dims - or ends it with `null`. */
53
53
  isolate(series) {
@@ -11,7 +11,7 @@ export declare class DisabledStyle {
11
11
  /** The style a legend item draws with while off - it stays visible, so it can be turned back on. */
12
12
  static ofItem(action: LegendDisabledAction | undefined, fallbackOpacity: number): LegendDisabledStyle | undefined;
13
13
  static stroke(disabled: LegendDisabledStyle | undefined): StrokeStyle | undefined;
14
- static dots(disabled: LegendDisabledStyle | undefined): DotStyle | undefined;
14
+ static dot(disabled: LegendDisabledStyle | undefined): DotStyle | undefined;
15
15
  static fill(disabled: LegendDisabledStyle | undefined): {
16
16
  fill?: string;
17
17
  fillOpacity?: number;
@@ -32,18 +32,18 @@ export class DisabledStyle {
32
32
  strokeLinejoin: disabled.stroke?.strokeLinejoin
33
33
  };
34
34
  }
35
- static dots(disabled) {
35
+ static dot(disabled) {
36
36
  if (!disabled)
37
37
  return undefined;
38
38
  return {
39
- dotRadius: disabled.dots?.dotRadius,
40
- dotFill: disabled.dots?.dotFill,
41
- dotFillOpacity: disabled.dots?.dotFillOpacity ?? disabled.opacity,
42
- dotSymbol: disabled.dots?.dotSymbol,
43
- dotStroke: disabled.dots?.dotStroke,
44
- dotStrokeWidth: disabled.dots?.dotStrokeWidth,
45
- dotStrokeOpacity: disabled.dots?.dotStrokeOpacity ?? disabled.opacity,
46
- dotStrokeDasharray: disabled.dots?.dotStrokeDasharray
39
+ dotRadius: disabled.dot?.dotRadius,
40
+ dotFill: disabled.dot?.dotFill,
41
+ dotFillOpacity: disabled.dot?.dotFillOpacity ?? disabled.opacity,
42
+ dotSymbol: disabled.dot?.dotSymbol,
43
+ dotStroke: disabled.dot?.dotStroke,
44
+ dotStrokeWidth: disabled.dot?.dotStrokeWidth,
45
+ dotStrokeOpacity: disabled.dot?.dotStrokeOpacity ?? disabled.opacity,
46
+ dotStrokeDasharray: disabled.dot?.dotStrokeDasharray
47
47
  };
48
48
  }
49
49
  static fill(disabled) {
@@ -0,0 +1,13 @@
1
+ import type { BasicStylesConfig, BasicStylesShape, PerSeries } from '../../types/layout/styles';
2
+ /**
3
+ * The style a plot draws every series with, out of its `styles.stroke`,
4
+ * `styles.dot` and `styles.area`: the ones its kind's segment style is made of
5
+ * (`shape`), each as it is or as a function of the series' name. The segments
6
+ * of a series restyle a stretch of it over this.
7
+ */
8
+ export declare class BasicStyles {
9
+ /** The style of the series named `name`, or `undefined` when there is none to give. */
10
+ static of<TStyle>(styles: BasicStylesConfig | undefined, shape: BasicStylesShape | undefined): ((seriesName: string) => TStyle | undefined) | undefined;
11
+ /** A style as it is, or what its function says of the series named `name`; a function has nothing to say without a name. */
12
+ static read<TStyle extends object>(style: PerSeries<TStyle> | undefined, name: string | undefined): TStyle | undefined;
13
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The style a plot draws every series with, out of its `styles.stroke`,
3
+ * `styles.dot` and `styles.area`: the ones its kind's segment style is made of
4
+ * (`shape`), each as it is or as a function of the series' name. The segments
5
+ * of a series restyle a stretch of it over this.
6
+ */
7
+ export class BasicStyles {
8
+ /** The style of the series named `name`, or `undefined` when there is none to give. */
9
+ static of(styles, shape) {
10
+ if (!styles || !shape)
11
+ return undefined;
12
+ const keys = 'flat' in shape ? [shape.flat] : shape.nested;
13
+ if (keys.every((key) => styles[key] === undefined))
14
+ return undefined;
15
+ return (name) => {
16
+ const picked = Object.fromEntries(keys.flatMap((key) => {
17
+ const style = BasicStyles.read(styles[key], name);
18
+ return style ? [[key, style]] : [];
19
+ }));
20
+ if (Object.keys(picked).length === 0)
21
+ return undefined;
22
+ return ('flat' in shape ? picked[shape.flat] : picked);
23
+ };
24
+ }
25
+ /** A style as it is, or what its function says of the series named `name`; a function has nothing to say without a name. */
26
+ static read(style, name) {
27
+ if (typeof style !== 'function')
28
+ return style;
29
+ return name === undefined ? undefined : style(name);
30
+ }
31
+ }
@@ -67,4 +67,6 @@
67
67
  strokeWidth={row.strokeWidth}
68
68
  strokeOpacity={row.strokeOpacity}
69
69
  strokeDasharray={marker.styles?.strokeDasharray}
70
+ strokeLinecap={marker.styles?.strokeLinecap}
71
+ strokeLinejoin={marker.styles?.strokeLinejoin}
70
72
  />
@@ -53,4 +53,6 @@
53
53
  strokeWidth={row.strokeWidth}
54
54
  strokeOpacity={row.strokeOpacity}
55
55
  strokeDasharray={styles?.strokeDasharray}
56
+ strokeLinecap={styles?.strokeLinecap}
57
+ strokeLinejoin={styles?.strokeLinejoin}
56
58
  />
@@ -3,6 +3,7 @@ import { ColorRamp } from '../../utils/color';
3
3
  import { resolveAccessor } from '../../utils/accessors';
4
4
  import { buildGroupedSeries, buildScopedMarkerGroups } from '../../plots/utils/segments';
5
5
  import { buildGapFillMaps, isGapValue } from '../../plots/utils/gaps';
6
+ import { BasicStyles } from '../../layout/styles/BasicStyles';
6
7
  import { FacetPanels } from '../facet/FacetPanels';
7
8
  import { PlotAxes } from './PlotAxes';
8
9
  /**
@@ -39,10 +40,12 @@ export class DataPhase {
39
40
  ? FacetPanels.splitSeries(fullSeries, state.panels, state.panelOf)
40
41
  : fullSeries;
41
42
  const resolvedSeries = ctx.interaction?.legend?.withoutOmitted(panelSeries) ?? panelSeries;
42
- const groupedSeries = buildGroupedSeries(resolvedSeries, segments, connect);
43
+ const baseFor = BasicStyles.of(state.styles, options.segments?.base);
44
+ const groupedSeries = buildGroupedSeries(resolvedSeries, segments, connect, baseFor);
43
45
  const scopedGroups = buildScopedMarkerGroups({
44
46
  groupedSeries,
45
47
  segments,
48
+ baseFor,
46
49
  colorFor: (s) => colorForSeries(s.name),
47
50
  segmentColorFor: (style, defaultColor) => segmentColor?.(style) ?? defaultColor
48
51
  });
@@ -57,6 +60,7 @@ export class DataPhase {
57
60
  seriesGrouping,
58
61
  allSeriesNames: fullSeries.map((s) => s.name),
59
62
  groupedSeries,
63
+ segmentBase: baseFor,
60
64
  flat: resolvedSeries.flatMap((s) => s.data),
61
65
  scopedGroups,
62
66
  colorForSeries,
@@ -159,7 +159,7 @@ export class TransformsPhase {
159
159
  static #buildGroups(state, connect) {
160
160
  const { data } = state.processedData;
161
161
  const series = (state.resolvedSeries ?? []).map((s) => TransformsPhase.#processedSeries(state, s, data.filter((row) => row[SERIES_ID_KEY] === seriesIdOf(s))));
162
- return buildGroupedSeries(series, state.segments, connect);
162
+ return buildGroupedSeries(series, state.segments, connect, state.segmentBase);
163
163
  }
164
164
  /**
165
165
  * The processed rows in their own order, split into runs of consecutive rows
@@ -181,7 +181,7 @@ export class TransformsPhase {
181
181
  else
182
182
  runs.push(TransformsPhase.#processedSeries(state, source, [row]));
183
183
  }
184
- return buildGroupedSeries(runs, state.segments, false);
184
+ return buildGroupedSeries(runs, state.segments, false, state.segmentBase);
185
185
  }
186
186
  /**
187
187
  * One series accessor shared by every series as-is, so a field key keeps
@@ -57,7 +57,7 @@ function extendWithColorScales(state, ctx) {
57
57
  */
58
58
  export function buildHeatmapDefaults() {
59
59
  return {
60
- data: { discreteAxes: 2 },
60
+ data: { discreteAxes: 2, segments: { base: { flat: 'area' } } },
61
61
  mapping: {
62
62
  // A heatmap with no `color` still reads one per cell, as nothing.
63
63
  extend: (state) => ({
@@ -1,6 +1,8 @@
1
1
  import { LegendState } from '../../../layout/legend/LegendState.svelte';
2
2
  import { DisabledStyle } from '../../../layout/legend/DisabledStyle';
3
3
  import { resolveAccessor } from '../../../utils/accessors';
4
+ import { merge } from '../../../utils/merge';
5
+ import { SERIES_KEY } from '../../../phases/transforms/TransformsPhase';
4
6
  /** A processed channel as a per-row function. */
5
7
  function readChannel(channel) {
6
8
  return resolveAccessor(channel);
@@ -103,8 +105,10 @@ export function buildMarks(state, ctx) {
103
105
  return base;
104
106
  return { ...base, ...DisabledStyle.fill(range.disabledStyle) };
105
107
  }
108
+ // `styles.area`, as the cell's series sets it: what every cell is drawn with, under its segments.
106
109
  function getCellStyle(d) {
107
- return rangeFilteredStyle(segStyleMap.get(`${String(getX(d))}::${String(getY(d))}`) ?? {}, Number(getColor(d)));
110
+ const base = state.segmentBase?.(String(d[SERIES_KEY]));
111
+ return rangeFilteredStyle(merge(base ?? {}, segStyleMap.get(`${String(getX(d))}::${String(getY(d))}`)), Number(getColor(d)));
108
112
  }
109
113
  const styleGroups = (() => {
110
114
  const groups = new Map();
@@ -63,7 +63,8 @@ export function buildScatterDefaults() {
63
63
  data: {
64
64
  segments: {
65
65
  color: (style) => style?.dotFill,
66
- connect: false
66
+ connect: false,
67
+ base: { flat: 'dot' }
67
68
  }
68
69
  },
69
70
  transforms: { groups: 'runs' },
@@ -14,7 +14,11 @@ export function buildBoxDefaults() {
14
14
  return {
15
15
  data: {
16
16
  discreteAxes: 1,
17
- segments: { color: (style) => style?.fill, connect: false }
17
+ segments: {
18
+ color: (style) => style?.fill,
19
+ connect: false,
20
+ base: { flat: 'area' }
21
+ }
18
22
  },
19
23
  scales: {
20
24
  bandOrder: 'data',
@@ -15,7 +15,14 @@ export function buildMarks(state, ctx) {
15
15
  const disabled = ctx.interaction?.fillFor(name);
16
16
  return group.visualSegments.map((seg) => {
17
17
  const { stroke, strokeWidth, strokeOpacity, borderRadius } = seg.styles;
18
- const outline = { stroke, strokeWidth, strokeOpacity, borderRadius };
18
+ const fill = disabled?.fill ?? seg.styles.fill ?? state.colorForSeries(name);
19
+ // An outline asked for without a color is the fill's.
20
+ const outline = {
21
+ stroke: stroke ?? (strokeWidth !== undefined ? fill : undefined),
22
+ strokeWidth,
23
+ strokeOpacity,
24
+ borderRadius
25
+ };
19
26
  return {
20
27
  type: 'box',
21
28
  series: name,
@@ -25,7 +32,7 @@ export function buildMarks(state, ctx) {
25
32
  bar: {
26
33
  // The box's own outline, from its segment; unset fields stay svelteplot's.
27
34
  ...Object.fromEntries(Object.entries(outline).filter(([, v]) => v !== undefined)),
28
- fill: disabled?.fill ?? seg.styles.fill ?? state.colorForSeries(name),
35
+ fill,
29
36
  fillOpacity: disabled?.fillOpacity ?? seg.styles.fillOpacity ?? cfg.box.fillOpacity
30
37
  }
31
38
  };
@@ -19,7 +19,13 @@ function mergeDensity(own, horizontal) {
19
19
  */
20
20
  export function buildDensityDefaults() {
21
21
  return {
22
- data: { segments: { color: (style) => style?.fill, connect: false } },
22
+ data: {
23
+ segments: {
24
+ color: (style) => style?.fill,
25
+ connect: false,
26
+ base: { flat: 'area' }
27
+ }
28
+ },
23
29
  transforms: {
24
30
  transforms: (state) => mergeDensity(state.transforms ?? [], state.axes?.isHorizontal ?? false)
25
31
  },
@@ -24,6 +24,10 @@ export function buildMarks(state, ctx) {
24
24
  strokeDasharray: disabledStroke?.strokeDasharray ?? seg.styles.strokeDasharray
25
25
  }
26
26
  : {
27
+ // An outline asked for without a color is the fill's.
28
+ ...(seg.styles.strokeWidth !== undefined
29
+ ? { stroke: disabledFill?.fill ?? color }
30
+ : {}),
27
31
  ...seg.styles,
28
32
  fill: disabledFill?.fill ?? color,
29
33
  fillOpacity: disabledFill?.fillOpacity ?? seg.styles.fillOpacity ?? cfg.density.fillOpacity
@@ -32,7 +32,13 @@ function mergeBin(own, cfg, horizontal) {
32
32
  */
33
33
  export function buildHistogramDefaults() {
34
34
  return {
35
- data: { segments: { color: (style) => style?.fill, connect: false } },
35
+ data: {
36
+ segments: {
37
+ color: (style) => style?.fill,
38
+ connect: false,
39
+ base: { flat: 'area' }
40
+ }
41
+ },
36
42
  transforms: {
37
43
  transforms: (state, ctx) => mergeBin(state.transforms ?? [], ctx.cfg, state.axes?.isHorizontal ?? false)
38
44
  },
@@ -72,6 +72,8 @@ export function buildMarks(state, ctx) {
72
72
  const fillOpacity = ctx.cfg.histogram.fillOpacity;
73
73
  const rects = (state.processedGroups ?? []).flatMap((group) => {
74
74
  const disabled = ctx.interaction?.fillFor(group.series.name);
75
+ // A stroke the segment sets is not a reason to lose the series' fill, as the marker would take it for the color.
76
+ const color = state.colorForSeries(group.series.name);
75
77
  return group.visualSegments.map((seg) => Transforms.finish({
76
78
  type: 'rect',
77
79
  series: group.series.name,
@@ -79,9 +81,15 @@ export function buildMarks(state, ctx) {
79
81
  ...channels,
80
82
  ...options,
81
83
  styles: {
84
+ // An outline asked for without a color is the fill's.
85
+ ...(seg.styles.strokeWidth !== undefined
86
+ ? { stroke: disabled?.fill ?? seg.styles.fill ?? color }
87
+ : {}),
88
+ ...seg.styles,
89
+ fill: disabled?.fill ?? seg.styles.fill ?? color,
82
90
  fillOpacity: disabled?.fillOpacity ?? seg.styles.fillOpacity ?? fillOpacity,
83
- ...(seg.styles.fill !== undefined ? { fill: seg.styles.fill } : {}),
84
- ...(disabled?.fill !== undefined ? { fill: disabled.fill } : {})
91
+ // A dimmed series fades its outline with its fill.
92
+ ...(disabled?.fillOpacity !== undefined ? { strokeOpacity: disabled.fillOpacity } : {})
85
93
  }
86
94
  }, transforms));
87
95
  });
@@ -20,7 +20,11 @@ export function buildPyramidDefaults() {
20
20
  return {
21
21
  data: {
22
22
  discreteAxes: 1,
23
- segments: { color: (style) => style?.fill, connect: false },
23
+ segments: {
24
+ color: (style) => style?.fill,
25
+ connect: false,
26
+ base: { flat: 'area' }
27
+ },
24
28
  orderSeries: (series, state) => {
25
29
  const sorted = [...series].sort(byName).map((s) => s.name);
26
30
  const reversed = state.scales.grouping?.reverse ?? false;
@@ -119,8 +119,17 @@ export function buildMarks(state, ctx) {
119
119
  data: realData,
120
120
  ...span(processedCategory, signed),
121
121
  styles: {
122
+ // An outline asked for without a color is the fill's.
123
+ ...(seg.styles.strokeWidth !== undefined
124
+ ? { stroke: disabledFill?.fill ?? seg.styles.fill ?? defaultColor }
125
+ : {}),
126
+ ...seg.styles,
122
127
  fill: disabledFill?.fill ?? seg.styles.fill ?? defaultColor,
123
- fillOpacity: disabledFill?.fillOpacity ?? seg.styles.fillOpacity ?? 1
128
+ fillOpacity: disabledFill?.fillOpacity ?? seg.styles.fillOpacity ?? 1,
129
+ // A dimmed series fades its outline with its fill.
130
+ ...(disabledFill?.fillOpacity !== undefined
131
+ ? { strokeOpacity: disabledFill.fillOpacity }
132
+ : {})
124
133
  }
125
134
  });
126
135
  }
@@ -34,7 +34,11 @@ export function buildViolinDefaults() {
34
34
  return {
35
35
  data: {
36
36
  discreteAxes: 1,
37
- segments: { color: (style) => style?.fill, connect: false }
37
+ segments: {
38
+ color: (style) => style?.fill,
39
+ connect: false,
40
+ base: { flat: 'area' }
41
+ }
38
42
  },
39
43
  scales: {
40
44
  bandOrder: 'data',
@@ -45,6 +45,10 @@ export function buildMarks(state, ctx) {
45
45
  peak,
46
46
  slot: { index, count: groups.length },
47
47
  styles: {
48
+ // An outline asked for without a color is the fill's.
49
+ ...(seg.styles.strokeWidth !== undefined
50
+ ? { stroke: disabled?.fill ?? seg.styles.fill ?? state.colorForSeries(name) }
51
+ : {}),
48
52
  ...seg.styles,
49
53
  fill: disabled?.fill ?? seg.styles.fill ?? state.colorForSeries(name),
50
54
  fillOpacity: disabled?.fillOpacity ?? seg.styles.fillOpacity ?? cfg.violin.fillOpacity
@@ -3,7 +3,9 @@ import { ValueLabels } from '../../../layout/styles/ValueLabels';
3
3
  /** AreaPlot on the shared pipeline: areas from a zero baseline, author markers taking the plot's `x`/`y`, and its own markers. Its plot defaults to: hover along x, cumulative timeline tweening `y` per series. */
4
4
  export function buildAreaDefaults() {
5
5
  return {
6
- data: { segments: { color: (style) => style?.fill } },
6
+ data: {
7
+ segments: { color: (style) => style?.fill, base: { flat: 'area' } }
8
+ },
7
9
  statistics: { inheritPositions: true },
8
10
  scales: { zero: ['y'] },
9
11
  transforms: { labels: { select: 'last', skip: ValueLabels.skipGaps } },
@@ -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
  };
@@ -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;
@@ -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.
@@ -103,6 +103,36 @@ A row whose value is `null`, `undefined` or `NaN` breaks a line. `styles.gaps` b
103
103
  - On bar plots only the presence of `x` matters.
104
104
  - Unset styles fall back to `line.gap`, `bar.gap` and `pyramid.gap` in the [configuration](theming.md).
105
105
 
106
+ ### Basic styles
107
+
108
+ `styles.stroke`, `styles.dot` and `styles.area` set the style every series is drawn with, so `segments` are left for what only part of a series looks like. Each takes a style as it is, or a function of the series' name returning one (`undefined` leaves that series as it is):
109
+
110
+ ```svelte
111
+ <LinePlot
112
+ styles={{
113
+ stroke: (name) =>
114
+ name === 'male' ? { strokeWidth: 2, strokeOpacity: 0.5 } : { strokeWidth: 4 },
115
+ dot: { dotRadius: 3 }
116
+ }}
117
+ ...
118
+ />
119
+ ```
120
+
121
+ - A plot uses the ones its own marks are made of and ignores the rest, so one `styles` can be shared between plots:
122
+
123
+ | Plot | Uses |
124
+ | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
125
+ | `LinePlot`, `SlopePlot`, `SparklinePlot` | `stroke` (the line), `dot` (a dot on every point; unset, none) |
126
+ | `AreaPlot`, `BarPlot`, `BulletPlot`, `PyramidPlot`, `HistogramPlot`, `BoxPlot`, `DensityPlot`, `ViolinPlot`, `HeatmapPlot` | `area` |
127
+ | `ScatterPlot` | `dot` |
128
+ | `LollipopPlot`, `DumbbellPlot`, `ClevelandPlot`, `ForestPlot` | `stroke` (the stems and connectors), `dot` |
129
+
130
+ `GeoPlot` has styles of its own and uses none of them.
131
+
132
+ - On the plots with connectors, `styles.stroke` (or a segment's `stroke`) styles the stems and connectors: width, color, opacity, dashes and line caps and joins. What the author sets for a connector wins over it: a segment's `stroke` wins over `styles.connector`, which wins over `styles.stroke`. A function of the series styles only the connector of a lone dot, which has a series; the connector of several dots takes a style as it is.
133
+ - A series keeps the color of the `color` scale unless the style sets its own.
134
+ - A segment's own style wins over them, field by field, and what a legend turned off dims over both - so a `legend.off` fades a series without `segments` repeating its opacity.
135
+
106
136
  ## Segments
107
137
 
108
138
  `segments` separates data ranges from their styling. It is an object: each key is a series name (a feature id for `GeoPlot`), or `'default'`, mapped to a list of segments.
@@ -125,7 +155,7 @@ The style shape depends on the plot:
125
155
 
126
156
  | Plot | Style type | Fields |
127
157
  | --------------------------- | ---------------------------------- | ---------------------------------------------- |
128
- | `LinePlot` | `LineSegmentStyle` | `stroke` (line style), `dots` (dot style) |
158
+ | `LinePlot` | `LineSegmentStyle` | `stroke` (line style), `dot` (dot style) |
129
159
  | `AreaPlot`, `HistogramPlot` | `AreaStyle` | `fill`, `fillOpacity`, `stroke`, ... |
130
160
  | `BarPlot`, `PyramidPlot` | `BarSegmentStyle` (`AreaStyle`) | `fill`, `fillOpacity`, `stroke`, ... |
131
161
  | `HeatmapPlot` | `CellSegmentStyle` (`AreaStyle`) | `fill`, `fillOpacity`, `stroke`, `strokeWidth` |
@@ -192,7 +222,7 @@ Per-series colors, plus dots over a range shared by every series:
192
222
  default: [
193
223
  {
194
224
  areas: [{ x: { from: 1965, to: 1980 } }],
195
- styles: { dots: { dotRadius: 5, dotSymbol: 'circle' } }
225
+ styles: { dot: { dotRadius: 5, dotSymbol: 'circle' } }
196
226
  }
197
227
  ]
198
228
  }}
@@ -18,7 +18,7 @@
18
18
  default: [
19
19
  {
20
20
  areas: [{ x: { from: 1965, to: 1980 } }],
21
- styles: { dots: { dotRadius: 5, dotSymbol: 'circle' } }
21
+ styles: { dot: { dotRadius: 5, dotSymbol: 'circle' } }
22
22
  }
23
23
  ]
24
24
  }}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fundar/data-chart-telling",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Declarative charts, maps and small multiples for Svelte, built on svelteplot.",
5
5
  "license": "MIT",
6
6
  "author": "Ezequiel Puerta",