@fundar/data-chart-telling 0.0.49 → 0.0.51

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 (40) hide show
  1. package/dist/configuration/config.svelte.js +7 -2
  2. package/dist/configuration/themes/index.d.ts +12 -0
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +1 -1
  5. package/dist/layout/legend/DiscreteBadgeItem.svelte +21 -8
  6. package/dist/layout/legend/DiscreteBadgeItem.svelte.d.ts +1 -0
  7. package/dist/layout/legend/DiscreteSection.svelte +22 -9
  8. package/dist/layout/legend/DiscreteSection.svelte.d.ts +2 -1
  9. package/dist/layout/legend/LegendLayout.svelte +7 -2
  10. package/dist/layout/scales/ScalesLayout.svelte +87 -10
  11. package/dist/layout/scales/buildScale.d.ts +18 -1
  12. package/dist/layout/scales/buildScale.js +49 -2
  13. package/dist/layout/scales/niceDomain.d.ts +6 -0
  14. package/dist/layout/scales/niceDomain.js +34 -0
  15. package/dist/layout/tooltip/controller.svelte.js +23 -11
  16. package/dist/layout/tooltip/utils.js +3 -0
  17. package/dist/markers/BarMarker.svelte +145 -84
  18. package/dist/markers/HoverMarker.svelte +41 -1
  19. package/dist/markers/HoverMarker.svelte.d.ts +7 -1
  20. package/dist/plots/bar/Plot.svelte +46 -20
  21. package/dist/plots/bar/buildBarMarkers.d.ts +4 -14
  22. package/dist/plots/bar/buildBarMarkers.js +73 -53
  23. package/dist/plots/bar/hoverPoints.d.ts +1 -1
  24. package/dist/plots/bar/hoverPoints.js +2 -3
  25. package/dist/plots/bar/layout.svelte.d.ts +4 -0
  26. package/dist/plots/bar/layout.svelte.js +62 -17
  27. package/dist/plots/line/Plot.svelte +29 -14
  28. package/dist/plots/line/buildLineMarkers.d.ts +1 -0
  29. package/dist/plots/line/buildLineMarkers.js +3 -4
  30. package/dist/plots/pyramid/Plot.svelte +10 -6
  31. package/dist/plots/scatter/Plot.svelte +25 -11
  32. package/dist/plots/scatter/buildScatterMarkers.d.ts +1 -0
  33. package/dist/plots/scatter/buildScatterMarkers.js +3 -4
  34. package/dist/plots/utils/legendDisabled.d.ts +13 -1
  35. package/dist/plots/utils/legendDisabled.js +27 -0
  36. package/dist/types/configuration/styling.d.ts +12 -2
  37. package/dist/types/layout/legend.d.ts +15 -10
  38. package/dist/types/layout/tooltip.d.ts +2 -1
  39. package/dist/types/markers/common.d.ts +14 -2
  40. package/package.json +1 -1
@@ -1,92 +1,153 @@
1
1
  <script lang="ts">
2
- import { BarX, BarY, usePlot } from 'svelteplot';
3
- import type { BarMarkerConfig } from '../types/markers/common';
2
+ import { BarX, BarY, usePlot } from 'svelteplot';
3
+ import type { BarMarkerConfig } from '../types/markers/common';
4
4
 
5
- /**
6
- * Renders one `'bar'` marker `role: 'segment'` is a visible bar
7
- * (`<BarX>`/`<BarY>`, per `isHorizontal`); `role: 'hitArea'` is an
8
- * invisible hit region for one category, resolved against the live scale
9
- * via `usePlot()`.
10
- */
11
- let { marker }: { marker: BarMarkerConfig } = $props();
5
+ /**
6
+ * Renders one `'bar'` marker: `role: 'segment'` (visible bar via
7
+ * `<BarX>`/`<BarY>`), `role: 'hitArea'` (invisible hover hit region for
8
+ * one segment), or `role: 'highlight'` (visible area highlight).
9
+ */
10
+ let { marker }: { marker: BarMarkerConfig } = $props();
12
11
 
13
- const plot = usePlot();
14
- function categoryScaleFor(isHorizontal: boolean) {
15
- return isHorizontal ? plot.scales.y : plot.scales.x;
16
- }
17
- function valueScaleFor(isHorizontal: boolean) {
18
- return isHorizontal ? plot.scales.x : plot.scales.y;
19
- }
20
- function valueExtentFor(isHorizontal: boolean): [number, number] {
21
- const range = valueScaleFor(isHorizontal)?.fn?.range?.();
22
- if (!range || range.length < 2) return [0, 0];
23
- const a = Number(range[0]);
24
- const b = Number(range[range.length - 1]);
25
- return [Math.min(a, b), Math.max(a, b)];
26
- }
12
+ const plot = usePlot();
13
+ function categoryScaleFor(isHorizontal: boolean) {
14
+ return isHorizontal ? plot.scales.y : plot.scales.x;
15
+ }
16
+ function valueScaleFor(isHorizontal: boolean) {
17
+ return isHorizontal ? plot.scales.x : plot.scales.y;
18
+ }
19
+ function valueExtentFor(isHorizontal: boolean): [number, number] {
20
+ const range = valueScaleFor(isHorizontal)?.fn?.range?.();
21
+ if (!range || range.length < 2) return [0, 0];
22
+ const a = Number(range[0]);
23
+ const b = Number(range[range.length - 1]);
24
+ return [Math.min(a, b), Math.max(a, b)];
25
+ }
26
+ // A `'hitArea'` marker's own value-axis slice, mapped to pixels.
27
+ function hitAreaValueRangeFor(
28
+ isHorizontal: boolean,
29
+ from: unknown,
30
+ to: unknown
31
+ ): [number, number] {
32
+ if (typeof from !== 'number' || typeof to !== 'number') return valueExtentFor(isHorizontal);
33
+ const scale = valueScaleFor(isHorizontal);
34
+ const a = Number(scale?.fn?.(from) ?? 0);
35
+ const b = Number(scale?.fn?.(to) ?? 0);
36
+ return [Math.min(a, b), Math.max(a, b)];
37
+ }
27
38
  </script>
28
39
 
29
40
  {#if marker.role === 'segment'}
30
- {#if marker.isHorizontal}
31
- <BarX
32
- data={marker.data}
33
- y={marker.y}
34
- x1={marker.x1}
35
- x2={marker.x2}
36
- insetTop={marker.insetTop}
37
- insetBottom={marker.insetBottom}
38
- fill={marker.style?.fill}
39
- fillOpacity={marker.style?.fillOpacity}
40
- stroke={marker.style?.stroke}
41
- strokeWidth={marker.style?.strokeWidth}
42
- strokeOpacity={marker.style?.strokeOpacity}
43
- strokeDasharray={marker.style?.strokeDasharray}
44
- borderRadius={marker.style?.borderRadius}
45
- />
46
- {:else}
47
- <BarY
48
- data={marker.data}
49
- x={marker.x}
50
- y1={marker.y1}
51
- y2={marker.y2}
52
- insetLeft={marker.insetLeft}
53
- insetRight={marker.insetRight}
54
- fill={marker.style?.fill}
55
- fillOpacity={marker.style?.fillOpacity}
56
- stroke={marker.style?.stroke}
57
- strokeWidth={marker.style?.strokeWidth}
58
- strokeOpacity={marker.style?.strokeOpacity}
59
- strokeDasharray={marker.style?.strokeDasharray}
60
- borderRadius={marker.style?.borderRadius}
61
- />
62
- {/if}
41
+ {@const categoryScale = categoryScaleFor(marker.isHorizontal)}
42
+ {@const bandwidth = categoryScale?.type === 'band' ? categoryScale.fn.bandwidth() : 0}
43
+ {#if marker.isHorizontal}
44
+ <BarX
45
+ data={marker.data}
46
+ y={marker.y}
47
+ x1={marker.x1}
48
+ x2={marker.x2}
49
+ insetTop={marker.insetTop != null ? marker.insetTop * bandwidth : undefined}
50
+ insetBottom={marker.insetBottom != null ? marker.insetBottom * bandwidth : undefined}
51
+ fill={marker.style?.fill}
52
+ fillOpacity={marker.style?.fillOpacity}
53
+ stroke={marker.style?.stroke}
54
+ strokeWidth={marker.style?.strokeWidth}
55
+ strokeOpacity={marker.style?.strokeOpacity}
56
+ strokeDasharray={marker.style?.strokeDasharray}
57
+ borderRadius={marker.style?.borderRadius}
58
+ />
59
+ {:else}
60
+ <BarY
61
+ data={marker.data}
62
+ x={marker.x}
63
+ y1={marker.y1}
64
+ y2={marker.y2}
65
+ insetLeft={marker.insetLeft != null ? marker.insetLeft * bandwidth : undefined}
66
+ insetRight={marker.insetRight != null ? marker.insetRight * bandwidth : undefined}
67
+ fill={marker.style?.fill}
68
+ fillOpacity={marker.style?.fillOpacity}
69
+ stroke={marker.style?.stroke}
70
+ strokeWidth={marker.style?.strokeWidth}
71
+ strokeOpacity={marker.style?.strokeOpacity}
72
+ strokeDasharray={marker.style?.strokeDasharray}
73
+ borderRadius={marker.style?.borderRadius}
74
+ />
75
+ {/if}
63
76
  {:else}
64
- {@const categoryScale = categoryScaleFor(marker.isHorizontal)}
65
- {#if categoryScale?.type === 'band'}
66
- {@const bandwidth = categoryScale.fn.bandwidth()}
67
- {@const bandStart = Number(categoryScale.fn(marker.category as never) ?? 0)}
68
- {@const [valueMin, valueMax] = valueExtentFor(marker.isHorizontal)}
69
- <!-- svelte-ignore a11y_no_static_element_interactions -->
70
- {#if marker.isHorizontal}
71
- <rect
72
- x={valueMin}
73
- y={bandStart}
74
- width={Math.max(0, valueMax - valueMin)}
75
- height={bandwidth}
76
- fill="transparent"
77
- onpointerenter={marker.onpointerenter}
78
- onpointerleave={marker.onpointerleave}
79
- />
80
- {:else}
81
- <rect
82
- x={bandStart}
83
- y={valueMin}
84
- width={bandwidth}
85
- height={Math.max(0, valueMax - valueMin)}
86
- fill="transparent"
87
- onpointerenter={marker.onpointerenter}
88
- onpointerleave={marker.onpointerleave}
89
- />
90
- {/if}
91
- {/if}
77
+ {@const categoryScale = categoryScaleFor(marker.isHorizontal)}
78
+ {#if categoryScale?.type === 'band'}
79
+ {@const bandwidth = categoryScale.fn.bandwidth()}
80
+ {@const bandStart = Number(categoryScale.fn(marker.category as never) ?? 0)}
81
+ {#if marker.role === 'hitArea'}
82
+ {@const [rangeMin, rangeMax] = marker.isHorizontal
83
+ ? hitAreaValueRangeFor(marker.isHorizontal, marker.x1, marker.x2)
84
+ : hitAreaValueRangeFor(marker.isHorizontal, marker.y1, marker.y2)}
85
+ {@const insetLeft = (marker.insetLeft ?? 0) * bandwidth}
86
+ {@const insetRight = (marker.insetRight ?? 0) * bandwidth}
87
+ {@const insetTop = (marker.insetTop ?? 0) * bandwidth}
88
+ {@const insetBottom = (marker.insetBottom ?? 0) * bandwidth}
89
+ {#if marker.isHorizontal}
90
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
91
+ <rect
92
+ class="dct-hit-area"
93
+ x={rangeMin + insetLeft}
94
+ y={bandStart + insetBottom}
95
+ width={Math.max(0, rangeMax - rangeMin - insetLeft - insetRight)}
96
+ height={Math.max(0, bandwidth - insetTop - insetBottom)}
97
+ fill="transparent"
98
+ onpointerenter={marker.onpointerenter}
99
+ onpointerleave={marker.onpointerleave}
100
+ />
101
+ {:else}
102
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
103
+ <rect
104
+ class="dct-hit-area"
105
+ x={bandStart + insetLeft}
106
+ y={rangeMin + insetBottom}
107
+ width={Math.max(0, bandwidth - insetLeft - insetRight)}
108
+ height={Math.max(0, rangeMax - rangeMin - insetTop - insetBottom)}
109
+ fill="transparent"
110
+ onpointerenter={marker.onpointerenter}
111
+ onpointerleave={marker.onpointerleave}
112
+ />
113
+ {/if}
114
+ {:else}
115
+ {@const [valueMin, valueMax] = valueExtentFor(marker.isHorizontal)}
116
+ {@const thickness = bandwidth + 2}
117
+ {@const center = bandStart + bandwidth / 2}
118
+ {#if marker.isHorizontal}
119
+ <rect
120
+ class="hover-area"
121
+ x={valueMin}
122
+ y={center - thickness / 2}
123
+ width={Math.max(0, valueMax - valueMin)}
124
+ height={thickness}
125
+ fill={marker.style?.fill}
126
+ fill-opacity={marker.style?.fillOpacity}
127
+ stroke={marker.style?.stroke}
128
+ stroke-width={marker.style?.strokeWidth}
129
+ stroke-opacity={marker.style?.strokeOpacity}
130
+ stroke-dasharray={marker.style?.strokeDasharray}
131
+ rx={marker.style?.borderRadius}
132
+ pointer-events="none"
133
+ />
134
+ {:else}
135
+ <rect
136
+ class="hover-area"
137
+ x={center - thickness / 2}
138
+ y={valueMin}
139
+ width={thickness}
140
+ height={Math.max(0, valueMax - valueMin)}
141
+ fill={marker.style?.fill}
142
+ fill-opacity={marker.style?.fillOpacity}
143
+ stroke={marker.style?.stroke}
144
+ stroke-width={marker.style?.strokeWidth}
145
+ stroke-opacity={marker.style?.strokeOpacity}
146
+ stroke-dasharray={marker.style?.strokeDasharray}
147
+ ry={marker.style?.borderRadius}
148
+ pointer-events="none"
149
+ />
150
+ {/if}
151
+ {/if}
152
+ {/if}
92
153
  {/if}
@@ -2,6 +2,7 @@
2
2
  import DotMarker from './DotMarker.svelte';
3
3
  import TextMarker from './TextMarker.svelte';
4
4
  import RuleMarker from './RuleMarker.svelte';
5
+ import BarMarker from './BarMarker.svelte';
5
6
  import { getConfiguration } from '../configuration/config.svelte';
6
7
  import type { MarkerProps } from '../types/markers/props';
7
8
  import type { HoverMarkerConfig } from '../types/markers/common';
@@ -22,10 +23,19 @@
22
23
  * pattern `BarMarker`/`RuleMarker` follow. `activePoint`/
23
24
  * `impliesCrosshairX`/`impliesCrosshairY` are all optional and default to
24
25
  * "no crosshair".
26
+ *
27
+ * `areaX`/`areaY` are a separate, always-explicit opt-in (no implied
28
+ * default) for a thickened area highlight in place of the thin crosshair —
29
+ * see `HoverMarkerConfig`'s own doc comment. Rendered via `BarMarker`'s
30
+ * `role: 'highlight'`, so it renders nothing on an axis without a band
31
+ * scale (e.g. a line/scatter plot's continuous axis).
25
32
  */
26
33
  interface Props extends MarkerProps<HoverDisplayPoint> {
27
34
  /** Narrowed to just the fields this component reads — `HoverMarkerConfig` also carries routing/scoping fields (`series`/`scope`/`strategy`/`sync`) it has no use for. */
28
- marker: Pick<HoverMarkerConfig, 'ruleX' | 'ruleY' | 'showLabels' | 'format' | 'ruleStyle' | 'dotStyle' | 'fontStyle'>;
35
+ marker: Pick<
36
+ HoverMarkerConfig,
37
+ 'ruleX' | 'ruleY' | 'areaX' | 'areaY' | 'showLabels' | 'format' | 'ruleStyle' | 'areaStyle' | 'dotStyle' | 'fontStyle'
38
+ >;
29
39
  activePoint?: HoverDisplayPoint | null;
30
40
  impliesCrosshairX?: boolean;
31
41
  impliesCrosshairY?: boolean;
@@ -46,9 +56,13 @@
46
56
  const showCrosshairY = $derived(marker.ruleY !== false && (impliesCrosshairY || marker.ruleY === true));
47
57
  const ruleX = $derived(showCrosshairX ? (activePoint?.x ?? null) : null);
48
58
  const ruleY = $derived(showCrosshairY ? (activePoint?.y ?? null) : null);
59
+ // areaX/areaY have no implied default — always off unless explicitly set.
60
+ const areaX = $derived(marker.areaX === true ? (activePoint?.x ?? null) : null);
61
+ const areaY = $derived(marker.areaY === true ? (activePoint?.y ?? null) : null);
49
62
  const showLabels = $derived(marker.showLabels ?? true);
50
63
  const format = $derived(marker.format);
51
64
  const ruleStyle = $derived(marker.ruleStyle);
65
+ const areaStyle = $derived({ ...cfg.hover.area, ...marker.areaStyle });
52
66
  const dotStyle = $derived(marker.dotStyle);
53
67
  const fontStyle = $derived(marker.fontStyle);
54
68
 
@@ -93,6 +107,32 @@
93
107
  }
94
108
  </script>
95
109
 
110
+ {#if areaX != null}
111
+ <BarMarker
112
+ marker={{
113
+ type: 'bar',
114
+ role: 'highlight',
115
+ isHorizontal: false,
116
+ data: [],
117
+ category: areaX,
118
+ style: areaStyle,
119
+ }}
120
+ />
121
+ {/if}
122
+
123
+ {#if areaY != null}
124
+ <BarMarker
125
+ marker={{
126
+ type: 'bar',
127
+ role: 'highlight',
128
+ isHorizontal: true,
129
+ data: [],
130
+ category: areaY,
131
+ style: areaStyle,
132
+ }}
133
+ />
134
+ {/if}
135
+
96
136
  {#if ruleX != null}
97
137
  <RuleMarker axis="x" value={ruleX} style={{ ...cfg.hover.rule, ...ruleStyle }} />
98
138
  {/if}
@@ -16,10 +16,16 @@ import type { HoverDisplayPoint } from '../types/layout/tooltip';
16
16
  * pattern `BarMarker`/`RuleMarker` follow. `activePoint`/
17
17
  * `impliesCrosshairX`/`impliesCrosshairY` are all optional and default to
18
18
  * "no crosshair".
19
+ *
20
+ * `areaX`/`areaY` are a separate, always-explicit opt-in (no implied
21
+ * default) for a thickened area highlight in place of the thin crosshair —
22
+ * see `HoverMarkerConfig`'s own doc comment. Rendered via `BarMarker`'s
23
+ * `role: 'highlight'`, so it renders nothing on an axis without a band
24
+ * scale (e.g. a line/scatter plot's continuous axis).
19
25
  */
20
26
  interface Props extends MarkerProps<HoverDisplayPoint> {
21
27
  /** Narrowed to just the fields this component reads — `HoverMarkerConfig` also carries routing/scoping fields (`series`/`scope`/`strategy`/`sync`) it has no use for. */
22
- marker: Pick<HoverMarkerConfig, 'ruleX' | 'ruleY' | 'showLabels' | 'format' | 'ruleStyle' | 'dotStyle' | 'fontStyle'>;
28
+ marker: Pick<HoverMarkerConfig, 'ruleX' | 'ruleY' | 'areaX' | 'areaY' | 'showLabels' | 'format' | 'ruleStyle' | 'areaStyle' | 'dotStyle' | 'fontStyle'>;
23
29
  activePoint?: HoverDisplayPoint | null;
24
30
  impliesCrosshairX?: boolean;
25
31
  impliesCrosshairY?: boolean;
@@ -2,7 +2,12 @@
2
2
  import { paletteColor } from '../../utils/color';
3
3
  import { buildSeries } from '../../utils/grouping';
4
4
  import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
5
- import { disabledFillOverride } from '../utils/legendDisabled';
5
+ import {
6
+ disabledFillOverride,
7
+ resolveEffectiveDisabledStyle,
8
+ omitDisabledSeries,
9
+ buildSeriesColorIndex
10
+ } from '../utils/legendDisabled';
6
11
  import { resolveAccessor } from '../utils/accessors';
7
12
  import { getConfiguration } from '../../configuration/config.svelte';
8
13
  import {
@@ -71,7 +76,15 @@
71
76
  });
72
77
 
73
78
  const cfg = $derived(getConfiguration());
74
- const resolvedSeries = $derived(series ?? buildSeries(data!, x!, y!, z));
79
+ const legendInteraction = $derived(getLegendInteraction());
80
+ // Unfiltered series list, for stable per-name color indexing.
81
+ const fullSeries = $derived(series ?? buildSeries(data!, x!, y!, z));
82
+ const colorIndex = $derived(buildSeriesColorIndex(fullSeries));
83
+ function colorForSeries(name: string): string {
84
+ return paletteColor(colorIndex.get(name) ?? 0, cfg.palette);
85
+ }
86
+ // Drops series toggled off with `disabled: { mode: 'omit' }`.
87
+ const resolvedSeries = $derived(omitDisabledSeries(fullSeries, legendInteraction));
75
88
  const groupedSeries = $derived(
76
89
  // connect: false — bars are discrete; there's no visual gap between
77
90
  // adjacent categories to bridge (see buildGroupedSeries's own doc).
@@ -104,7 +117,7 @@
104
117
  buildScopedMarkerGroups<TData, Series<TData>, BarSegmentStyle>({
105
118
  groupedSeries,
106
119
  segments,
107
- colorFor: (_series, seriesIndex) => paletteColor(seriesIndex, cfg.palette),
120
+ colorFor: (series) => colorForSeries(series.name),
108
121
  segmentColorFor: (style, defaultColor) => style?.fill ?? defaultColor
109
122
  })
110
123
  );
@@ -145,14 +158,27 @@
145
158
  delete rest.type;
146
159
  return rest;
147
160
  }
148
- // The single, fully-resolved scales object BasePlotLayout reads for
149
- // everything (ticks, grid, sort, and the real svelteplot scale) — avoids
150
- // keeping a second, separately-patched copy of `scales.x`/`scales.y` in
151
- // sync with the one actually forwarded.
161
+ // Backs the category axis's `paddingInner`/`paddingOuter` the gap
162
+ // *between* category groups with the theme's `bar.paddingInner`/
163
+ // `paddingOuter` default, when the plot doesn't set its own (or the
164
+ // `padding` shorthand). BarPlot-only, unlike svelteplot's own hardcoded
165
+ // 0.15 fallback for every band scale.
166
+ function withDefaultPadding(scale?: AxisScale<TData>): AxisScale<TData> {
167
+ return {
168
+ ...scale,
169
+ paddingInner: scale?.paddingInner ?? scale?.padding ?? cfg.bar.paddingInner,
170
+ paddingOuter: scale?.paddingOuter ?? scale?.padding ?? cfg.bar.paddingOuter
171
+ };
172
+ }
173
+ // The single, fully-resolved scales object both BasePlotLayout (ticks,
174
+ // grid, sort, the real svelteplot scale) and `createBarLayout` (the
175
+ // grouped-bar-width estimate) read for everything — avoids keeping a
176
+ // second, separately-patched copy of `scales.x`/`scales.y` in sync with
177
+ // the one actually forwarded.
152
178
  const resolvedScales = $derived({
153
179
  ...scales,
154
- x: stripCategoricalType(scales.x),
155
- y: stripCategoricalType(scales.y)
180
+ x: stripCategoricalType(categoricalAxis === 'x' ? withDefaultPadding(scales.x) : scales.x),
181
+ y: stripCategoricalType(categoricalAxis === 'y' ? withDefaultPadding(scales.y) : scales.y)
156
182
  });
157
183
 
158
184
  // The accessor feeding each *visual* channel — swapped from the
@@ -166,7 +192,7 @@
166
192
  * rendering, value labels, and hover markers below. See `layout.svelte.ts`.
167
193
  */
168
194
  const barLayout = createBarLayout<TData>({
169
- scales: () => scales,
195
+ scales: () => resolvedScales,
170
196
  isHorizontal: () => isHorizontal,
171
197
  width: () => width,
172
198
  height: () => height,
@@ -181,7 +207,6 @@
181
207
  // which drive tick/domain computation and the Pointer's hit-testing) —
182
208
  // `row` (see BasePlotLayout/HoverPoint) is what lets the template recover the
183
209
  // semantic category regardless of which axis it landed on.
184
- const legendInteraction = $derived(getLegendInteraction());
185
210
  const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
186
211
 
187
212
  // A value label hovered while `styles.values.hoverIsolate` is on dims
@@ -191,18 +216,19 @@
191
216
  let hoverIsolatedSeries = $state<string | null>(null);
192
217
 
193
218
  function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
194
- return (
195
- legendInteraction?.disabledSeries.get(name) ??
196
- (hoverIsolate && hoverIsolatedSeries && hoverIsolatedSeries !== name
197
- ? { opacity: cfg.legend.disabledOpacity }
198
- : undefined)
219
+ return resolveEffectiveDisabledStyle(
220
+ name,
221
+ legendInteraction,
222
+ cfg.legend.disabledOpacity,
223
+ hoverIsolate,
224
+ hoverIsolatedSeries
199
225
  );
200
226
  }
201
227
 
202
228
  // A legend-disabled series is dimmed on the bar itself, but never surfaces
203
229
  // in hover/tooltip — its points are simply absent from the candidate list.
204
230
  const hoverPoints = $derived(
205
- buildHoverPoints(resolvedSeries, isHorizontal, barLayout, cfg.palette).filter(
231
+ buildHoverPoints(resolvedSeries, isHorizontal, barLayout, colorForSeries).filter(
206
232
  (p) => !legendInteraction?.disabledSeries.has(p.series!)
207
233
  )
208
234
  );
@@ -220,8 +246,8 @@
220
246
  // (via svelteplot's `RectPath`) actually render that band, which is
221
247
  // enough to mismatch the neighbouring category once bands get thin
222
248
  // (many categories). The `role: 'hitArea'` bar markers `buildBarMarkers`
223
- // emits hit-test the category bands directly instead — see its own doc
224
- // comment.
249
+ // emits hit-test each series' own segment directly instead — see its
250
+ // own doc comment.
225
251
  pointMatching: 'contains' as const
226
252
  });
227
253
 
@@ -294,7 +320,7 @@
294
320
  barLayout,
295
321
  isHorizontal,
296
322
  hoverPoints,
297
- palette: cfg.palette,
323
+ colorFor: colorForSeries,
298
324
  disabledFillFor: (name) => disabledFillOverride(effectiveDisabledFor(name)),
299
325
  active: hover.active,
300
326
  setHoverMatch,
@@ -6,26 +6,16 @@ import type { BarMarkerConfig } from '../../types/markers/common';
6
6
  import type { BarLayout } from './layout.svelte';
7
7
  import type { DiscreteGapFill } from '../utils/gaps';
8
8
  /**
9
- * Every `'bar'` marker for one BarPlot render: one `role: 'segment'` marker
10
- * per series (stacked) or per visual segment (grouped/overlap), one
11
- * additional `role: 'segment'` marker per series per gap zone kind for any
12
- * row `styles.gaps` fills (distinctly styled, drawn at its interpolated
13
- * value — see `resolveDiscreteGapFills`), plus one `role: 'hitArea'` marker
14
- * per category, so a chart never gets segment markers without their hit
15
- * areas. A row whose value is missing and *not* filled by a gap zone is
16
- * skipped entirely — no bar drawn, rather than the broken zero/`NaN`
17
- * geometry a raw missing value would otherwise produce.
18
- *
19
- * `'hitArea'` geometry is left unresolved (just `category` + `data`) —
20
- * resolving it to pixels needs svelteplot's live scale, only available
21
- * where the marker actually renders.
9
+ * Every `'bar'` marker for one BarPlot render: visible `role: 'segment'`
10
+ * bars (real data + gap fills) plus one `role: 'hitArea'` hit region per row,
11
+ * sized to that row's own segment.
22
12
  */
23
13
  export declare function buildBarMarkers<TData extends Record<string, unknown>>(args: {
24
14
  groupedSeries: VisualGroup<Series<TData>, BarSegmentStyle>[];
25
15
  barLayout: BarLayout<TData>;
26
16
  isHorizontal: boolean;
27
17
  hoverPoints: HoverPoint<TData>[];
28
- palette: string[];
18
+ colorFor: (seriesName: string) => string;
29
19
  disabledFillFor: (seriesName: string) => {
30
20
  fill?: string;
31
21
  fillOpacity?: number;