@fundar/data-chart-telling 0.0.49 → 0.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/configuration/config.svelte.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/layout/legend/DiscreteBadgeItem.svelte +21 -8
- package/dist/layout/legend/DiscreteBadgeItem.svelte.d.ts +1 -0
- package/dist/layout/legend/DiscreteSection.svelte +22 -9
- package/dist/layout/legend/DiscreteSection.svelte.d.ts +2 -1
- package/dist/layout/legend/LegendLayout.svelte +7 -2
- package/dist/markers/BarMarker.svelte +61 -22
- package/dist/markers/HoverMarker.svelte +41 -1
- package/dist/markers/HoverMarker.svelte.d.ts +7 -1
- package/dist/plots/bar/Plot.svelte +24 -11
- package/dist/plots/bar/buildBarMarkers.d.ts +1 -1
- package/dist/plots/bar/buildBarMarkers.js +2 -3
- package/dist/plots/bar/hoverPoints.d.ts +1 -1
- package/dist/plots/bar/hoverPoints.js +2 -3
- package/dist/plots/line/Plot.svelte +29 -14
- package/dist/plots/line/buildLineMarkers.d.ts +1 -0
- package/dist/plots/line/buildLineMarkers.js +3 -4
- package/dist/plots/pyramid/Plot.svelte +10 -6
- package/dist/plots/scatter/Plot.svelte +25 -11
- package/dist/plots/scatter/buildScatterMarkers.d.ts +1 -0
- package/dist/plots/scatter/buildScatterMarkers.js +3 -4
- package/dist/plots/utils/legendDisabled.d.ts +13 -1
- package/dist/plots/utils/legendDisabled.js +27 -0
- package/dist/types/configuration/styling.d.ts +6 -2
- package/dist/types/layout/legend.d.ts +15 -10
- package/dist/types/markers/common.d.ts +14 -2
- package/package.json +1 -1
|
@@ -123,7 +123,7 @@ export const DEFAULT_CONFIG = {
|
|
|
123
123
|
rule: { stroke: '#e6580d', strokeOpacity: 0.35, strokeWidth: 1.5, strokeDasharray: '4 4' },
|
|
124
124
|
dot: { dotRadius: 5, dotFill: 'currentColor' },
|
|
125
125
|
font: { fill: 'currentColor', fontSize: 12, dy: -18 },
|
|
126
|
-
area: {
|
|
126
|
+
area: { fill: '#94a3b8', fillOpacity: 0.25 }
|
|
127
127
|
},
|
|
128
128
|
facet: {
|
|
129
129
|
indicator: {
|
package/dist/index.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export { default as TextMarker } from './markers/TextMarker.svelte';
|
|
|
34
34
|
export { setConfiguration, getConfiguration, resetConfiguration, configToCssVars, cssVarsStyle, DEFAULT_CONFIG } from './configuration/config.svelte';
|
|
35
35
|
export { THEMES } from './configuration/themes';
|
|
36
36
|
export type { ThemeName } from './configuration/themes';
|
|
37
|
-
export { groupBy
|
|
37
|
+
export { groupBy } from './utils/grouping';
|
|
38
38
|
export { makeColorScale, normalize, resolveCssColor, paletteColor, contrastTextColor, contrastRatio } from './utils/color';
|
|
39
39
|
export { estimateCategoricalAxisMargin } from './plots/utils/categoricalAxisMargin';
|
|
40
40
|
export { estimateLineValueLabelMargin } from './plots/line/valueLabelMargin';
|
|
@@ -58,7 +58,7 @@ export type { GeoMarkersConfig, GeoInsetMarkerConfig, GeoInsetTooltipConfig, Geo
|
|
|
58
58
|
export type { ChartPlotContext, ChartPlotSnippet } from './types/charts/common';
|
|
59
59
|
export type { TimeValue, Orientation, TimelineConfig } from './types/layout/timeline';
|
|
60
60
|
export type { Responsive, FacetColumns, FacetMode, FacetNavLayout, FacetConfig, FacetIndicatorContext, FacetIndicatorSnippet, FacetNavDirection, FacetNavContext, FacetNavSnippet } from './types/layout/facet';
|
|
61
|
-
export type { LegendSection, DiscreteLegendSection, ContinuousLegendSection, LegendItem, LegendDisabledStyle, LegendItemsLayout, LegendInteractiveStyle, DiscreteInteractiveConfig, ContinuousInteractiveConfig, LegendNavDirection, LegendNavContext, LegendNavSnippet, LegendInteractionStore, LegendPlotContext } from './types/layout/legend';
|
|
61
|
+
export type { LegendSection, DiscreteLegendSection, ContinuousLegendSection, LegendItem, LegendDisabledStyle, LegendDisabledAction, LegendItemsLayout, LegendInteractiveStyle, DiscreteInteractiveConfig, ContinuousInteractiveConfig, LegendNavDirection, LegendNavContext, LegendNavSnippet, LegendInteractionStore, LegendPlotContext } from './types/layout/legend';
|
|
62
62
|
export type { HoverStrategy, TooltipOptions, TooltipProp, TooltipAnchorX, TooltipAnchorY } from './types/layout/tooltip';
|
|
63
63
|
export type { ChartConfig, ChartConfigInput, TextStyle } from './types/configuration/styling';
|
|
64
64
|
export type { ChartProps, GapsAwareChartProps } from './types/charts/props';
|
package/dist/index.js
CHANGED
|
@@ -41,7 +41,7 @@ export { default as TextMarker } from './markers/TextMarker.svelte';
|
|
|
41
41
|
export { setConfiguration, getConfiguration, resetConfiguration, configToCssVars, cssVarsStyle, DEFAULT_CONFIG } from './configuration/config.svelte';
|
|
42
42
|
export { THEMES } from './configuration/themes';
|
|
43
43
|
// ── Utils ─────────────────────────────────────────────────────────────────────
|
|
44
|
-
export { groupBy
|
|
44
|
+
export { groupBy } from './utils/grouping';
|
|
45
45
|
export { makeColorScale, normalize, resolveCssColor, paletteColor, contrastTextColor, contrastRatio } from './utils/color';
|
|
46
46
|
export { estimateCategoricalAxisMargin } from './plots/utils/categoricalAxisMargin';
|
|
47
47
|
export { estimateLineValueLabelMargin } from './plots/line/valueLabelMargin';
|
|
@@ -4,18 +4,23 @@
|
|
|
4
4
|
import type { LegendItem } from '../../types/layout/legend';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* The `interactive.legendStyle: 'badge'` item look: a pill
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* while `disabled`. The toggled-off dim itself is applied by the parent
|
|
11
|
-
* `.dct-discrete__item`.
|
|
7
|
+
* The `interactive.legendStyle: 'badge'` item look: a pill with the series
|
|
8
|
+
* color, symbol icon, label, and a toggle icon — an eye (crossed out when
|
|
9
|
+
* `disabled`) for `'style'` mode, or a plus/cross pair for `'omit'`.
|
|
12
10
|
*/
|
|
13
11
|
let {
|
|
14
12
|
item,
|
|
15
13
|
color,
|
|
16
14
|
disabled,
|
|
17
|
-
showIcon = true
|
|
18
|
-
|
|
15
|
+
showIcon = true,
|
|
16
|
+
omitMode = false
|
|
17
|
+
}: {
|
|
18
|
+
item: LegendItem;
|
|
19
|
+
color: string;
|
|
20
|
+
disabled: boolean;
|
|
21
|
+
showIcon?: boolean;
|
|
22
|
+
omitMode?: boolean;
|
|
23
|
+
} = $props();
|
|
19
24
|
const textColor = $derived(contrastTextColor(color));
|
|
20
25
|
</script>
|
|
21
26
|
|
|
@@ -25,7 +30,15 @@
|
|
|
25
30
|
{/if}
|
|
26
31
|
<span class="dct-discrete__label">{item.label || item.name}</span>
|
|
27
32
|
<svg class="dct-discrete__eye" viewBox="0 0 24 24" aria-hidden="true">
|
|
28
|
-
{#if
|
|
33
|
+
{#if omitMode}
|
|
34
|
+
{#if disabled}
|
|
35
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
36
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
37
|
+
{:else}
|
|
38
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
39
|
+
<line x1="6" y1="18" x2="18" y2="6" />
|
|
40
|
+
{/if}
|
|
41
|
+
{:else if disabled}
|
|
29
42
|
<path
|
|
30
43
|
d="M17.94 17.94A10.94 10.94 0 0 1 12 20c-7 0-11-8-11-8a18.5 18.5 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
|
|
31
44
|
/>
|
|
@@ -6,18 +6,25 @@
|
|
|
6
6
|
getLegendInteraction,
|
|
7
7
|
createLegendInteraction
|
|
8
8
|
} from './interaction.svelte';
|
|
9
|
+
import { resolveDisabledStyle } from '../../plots/utils/legendDisabled';
|
|
9
10
|
import DiscreteBadgeItem from './DiscreteBadgeItem.svelte';
|
|
10
11
|
import DiscreteSwatchItem from './DiscreteSwatchItem.svelte';
|
|
11
12
|
import type {
|
|
12
13
|
DiscreteLegendSection,
|
|
14
|
+
LegendDisabledAction,
|
|
13
15
|
LegendItem,
|
|
16
|
+
LegendItemsLayout,
|
|
14
17
|
LegendNavDirection
|
|
15
18
|
} from '../../types/layout/legend';
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
/** `defaultItemsLayout` is the fallback used when `section.layout` isn't set. */
|
|
21
|
+
let {
|
|
22
|
+
section,
|
|
23
|
+
defaultItemsLayout = { type: 'vertical' }
|
|
24
|
+
}: { section: DiscreteLegendSection; defaultItemsLayout?: LegendItemsLayout } = $props();
|
|
18
25
|
|
|
19
26
|
const cfg = $derived(getConfiguration());
|
|
20
|
-
const layout = $derived(section.layout ??
|
|
27
|
+
const layout = $derived(section.layout ?? defaultItemsLayout);
|
|
21
28
|
const isHorizontal = $derived(layout.type === 'horizontal');
|
|
22
29
|
const maxRows = $derived(layout.type === 'horizontal' ? Math.max(1, layout.maxRows ?? 1) : 1);
|
|
23
30
|
const columns = $derived(section.columns ?? 1);
|
|
@@ -26,8 +33,8 @@
|
|
|
26
33
|
isInteractive && (section.interactive?.legendStyle ?? 'badge') === 'badge'
|
|
27
34
|
);
|
|
28
35
|
const showIcon = $derived(section.interactive?.showIcon ?? true);
|
|
29
|
-
const
|
|
30
|
-
section.interactive?.
|
|
36
|
+
const disabledAction = $derived<LegendDisabledAction>(
|
|
37
|
+
section.interactive?.disabled ?? { mode: 'style' }
|
|
31
38
|
);
|
|
32
39
|
const interaction = getLegendInteraction() ?? createLegendInteraction();
|
|
33
40
|
|
|
@@ -37,7 +44,7 @@
|
|
|
37
44
|
if (seeded.has(item.name)) continue;
|
|
38
45
|
seeded.add(item.name);
|
|
39
46
|
if (item.disabled) {
|
|
40
|
-
interaction.disabledSeries.set(item.name,
|
|
47
|
+
interaction.disabledSeries.set(item.name, disabledAction);
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
50
|
});
|
|
@@ -47,7 +54,7 @@
|
|
|
47
54
|
if (interaction.disabledSeries.has(item.name)) {
|
|
48
55
|
interaction.disabledSeries.delete(item.name);
|
|
49
56
|
} else {
|
|
50
|
-
interaction.disabledSeries.set(item.name,
|
|
57
|
+
interaction.disabledSeries.set(item.name, disabledAction);
|
|
51
58
|
}
|
|
52
59
|
}
|
|
53
60
|
|
|
@@ -142,8 +149,14 @@
|
|
|
142
149
|
</script>
|
|
143
150
|
|
|
144
151
|
{#snippet discreteItem(item: LegendItem, i: number)}
|
|
145
|
-
{@const
|
|
146
|
-
{@const isDisabled =
|
|
152
|
+
{@const action = interaction.disabledSeries.get(item.name)}
|
|
153
|
+
{@const isDisabled = action != null}
|
|
154
|
+
{@const omitMode = disabledAction.mode === 'omit'}
|
|
155
|
+
{@const disabledStyle = isDisabled
|
|
156
|
+
? omitMode
|
|
157
|
+
? { opacity: cfg.legend.disabledOpacity }
|
|
158
|
+
: resolveDisabledStyle(action, cfg.legend.disabledOpacity)
|
|
159
|
+
: undefined}
|
|
147
160
|
{@const color =
|
|
148
161
|
disabledStyle?.fill ??
|
|
149
162
|
disabledStyle?.stroke?.stroke ??
|
|
@@ -167,7 +180,7 @@
|
|
|
167
180
|
: {}}
|
|
168
181
|
>
|
|
169
182
|
{#if useBadge}
|
|
170
|
-
<DiscreteBadgeItem {item} {color} disabled={isDisabled} {showIcon} />
|
|
183
|
+
<DiscreteBadgeItem {item} {color} disabled={isDisabled} {showIcon} {omitMode} />
|
|
171
184
|
{:else}
|
|
172
185
|
<DiscreteSwatchItem {item} {color} />
|
|
173
186
|
{/if}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { DiscreteLegendSection } from '../../types/layout/legend';
|
|
1
|
+
import type { DiscreteLegendSection, LegendItemsLayout } from '../../types/layout/legend';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
section: DiscreteLegendSection;
|
|
4
|
+
defaultItemsLayout?: LegendItemsLayout;
|
|
4
5
|
};
|
|
5
6
|
declare const DiscreteSection: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
7
|
type DiscreteSection = ReturnType<typeof DiscreteSection>;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { LegendSection } from '../../types/layout/legend';
|
|
2
|
+
import type { LegendItemsLayout, LegendSection } from '../../types/layout/legend';
|
|
3
3
|
import DiscreteSection from './DiscreteSection.svelte';
|
|
4
4
|
import ContinuousSection from './ContinuousSection.svelte';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Renders one or more legend sections side by side, dispatching each to the
|
|
8
8
|
* matching section renderer. Discrete and continuous sections can be mixed.
|
|
9
|
+
* A lone discrete section's items default to horizontal flow; vertical once
|
|
10
|
+
* a second section joins. A section's own `layout` always wins.
|
|
9
11
|
*/
|
|
10
12
|
let { sections }: { sections: LegendSection[] } = $props();
|
|
13
|
+
const defaultItemsLayout = $derived<LegendItemsLayout>(
|
|
14
|
+
sections.length === 1 ? { type: 'horizontal' } : { type: 'vertical' }
|
|
15
|
+
);
|
|
11
16
|
</script>
|
|
12
17
|
|
|
13
18
|
<div class="dct-legend">
|
|
@@ -17,7 +22,7 @@
|
|
|
17
22
|
<div class="dct-legend__title">{section.title}</div>
|
|
18
23
|
{/if}
|
|
19
24
|
{#if section.type === 'discrete'}
|
|
20
|
-
<DiscreteSection {section} />
|
|
25
|
+
<DiscreteSection {section} {defaultItemsLayout} />
|
|
21
26
|
{:else if section.type === 'continuous'}
|
|
22
27
|
<ContinuousSection {section} />
|
|
23
28
|
{/if}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* Renders one `'bar'` marker — `role: 'segment'` is a visible bar
|
|
7
7
|
* (`<BarX>`/`<BarY>`, per `isHorizontal`); `role: 'hitArea'` is an
|
|
8
|
-
* invisible hit region for one category
|
|
9
|
-
*
|
|
8
|
+
* invisible hit region for one category; `role: 'highlight'` is a visible,
|
|
9
|
+
* non-interactive area highlight. The latter two resolve their geometry
|
|
10
|
+
* against the live scale via `usePlot()`.
|
|
10
11
|
*/
|
|
11
12
|
let { marker }: { marker: BarMarkerConfig } = $props();
|
|
12
13
|
|
|
@@ -66,27 +67,65 @@
|
|
|
66
67
|
{@const bandwidth = categoryScale.fn.bandwidth()}
|
|
67
68
|
{@const bandStart = Number(categoryScale.fn(marker.category as never) ?? 0)}
|
|
68
69
|
{@const [valueMin, valueMax] = valueExtentFor(marker.isHorizontal)}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
70
|
+
{#if marker.role === 'hitArea'}
|
|
71
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
72
|
+
{#if marker.isHorizontal}
|
|
73
|
+
<rect
|
|
74
|
+
x={valueMin}
|
|
75
|
+
y={bandStart}
|
|
76
|
+
width={Math.max(0, valueMax - valueMin)}
|
|
77
|
+
height={bandwidth}
|
|
78
|
+
fill="transparent"
|
|
79
|
+
onpointerenter={marker.onpointerenter}
|
|
80
|
+
onpointerleave={marker.onpointerleave}
|
|
81
|
+
/>
|
|
82
|
+
{:else}
|
|
83
|
+
<rect
|
|
84
|
+
x={bandStart}
|
|
85
|
+
y={valueMin}
|
|
86
|
+
width={bandwidth}
|
|
87
|
+
height={Math.max(0, valueMax - valueMin)}
|
|
88
|
+
fill="transparent"
|
|
89
|
+
onpointerenter={marker.onpointerenter}
|
|
90
|
+
onpointerleave={marker.onpointerleave}
|
|
91
|
+
/>
|
|
92
|
+
{/if}
|
|
80
93
|
{:else}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
{@const thickness = bandwidth + 2}
|
|
95
|
+
{@const center = bandStart + bandwidth / 2}
|
|
96
|
+
{#if marker.isHorizontal}
|
|
97
|
+
<rect
|
|
98
|
+
class="hover-area"
|
|
99
|
+
x={valueMin}
|
|
100
|
+
y={center - thickness / 2}
|
|
101
|
+
width={Math.max(0, valueMax - valueMin)}
|
|
102
|
+
height={thickness}
|
|
103
|
+
fill={marker.style?.fill}
|
|
104
|
+
fill-opacity={marker.style?.fillOpacity}
|
|
105
|
+
stroke={marker.style?.stroke}
|
|
106
|
+
stroke-width={marker.style?.strokeWidth}
|
|
107
|
+
stroke-opacity={marker.style?.strokeOpacity}
|
|
108
|
+
stroke-dasharray={marker.style?.strokeDasharray}
|
|
109
|
+
rx={marker.style?.borderRadius}
|
|
110
|
+
pointer-events="none"
|
|
111
|
+
/>
|
|
112
|
+
{:else}
|
|
113
|
+
<rect
|
|
114
|
+
class="hover-area"
|
|
115
|
+
x={center - thickness / 2}
|
|
116
|
+
y={valueMin}
|
|
117
|
+
width={thickness}
|
|
118
|
+
height={Math.max(0, valueMax - valueMin)}
|
|
119
|
+
fill={marker.style?.fill}
|
|
120
|
+
fill-opacity={marker.style?.fillOpacity}
|
|
121
|
+
stroke={marker.style?.stroke}
|
|
122
|
+
stroke-width={marker.style?.strokeWidth}
|
|
123
|
+
stroke-opacity={marker.style?.strokeOpacity}
|
|
124
|
+
stroke-dasharray={marker.style?.strokeDasharray}
|
|
125
|
+
ry={marker.style?.borderRadius}
|
|
126
|
+
pointer-events="none"
|
|
127
|
+
/>
|
|
128
|
+
{/if}
|
|
90
129
|
{/if}
|
|
91
130
|
{/if}
|
|
92
131
|
{/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<
|
|
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 {
|
|
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
|
|
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: (
|
|
120
|
+
colorFor: (series) => colorForSeries(series.name),
|
|
108
121
|
segmentColorFor: (style, defaultColor) => style?.fill ?? defaultColor
|
|
109
122
|
})
|
|
110
123
|
);
|
|
@@ -181,7 +194,6 @@
|
|
|
181
194
|
// which drive tick/domain computation and the Pointer's hit-testing) —
|
|
182
195
|
// `row` (see BasePlotLayout/HoverPoint) is what lets the template recover the
|
|
183
196
|
// semantic category regardless of which axis it landed on.
|
|
184
|
-
const legendInteraction = $derived(getLegendInteraction());
|
|
185
197
|
const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
|
|
186
198
|
|
|
187
199
|
// A value label hovered while `styles.values.hoverIsolate` is on dims
|
|
@@ -191,18 +203,19 @@
|
|
|
191
203
|
let hoverIsolatedSeries = $state<string | null>(null);
|
|
192
204
|
|
|
193
205
|
function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
|
|
194
|
-
return (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
206
|
+
return resolveEffectiveDisabledStyle(
|
|
207
|
+
name,
|
|
208
|
+
legendInteraction,
|
|
209
|
+
cfg.legend.disabledOpacity,
|
|
210
|
+
hoverIsolate,
|
|
211
|
+
hoverIsolatedSeries
|
|
199
212
|
);
|
|
200
213
|
}
|
|
201
214
|
|
|
202
215
|
// A legend-disabled series is dimmed on the bar itself, but never surfaces
|
|
203
216
|
// in hover/tooltip — its points are simply absent from the candidate list.
|
|
204
217
|
const hoverPoints = $derived(
|
|
205
|
-
buildHoverPoints(resolvedSeries, isHorizontal, barLayout,
|
|
218
|
+
buildHoverPoints(resolvedSeries, isHorizontal, barLayout, colorForSeries).filter(
|
|
206
219
|
(p) => !legendInteraction?.disabledSeries.has(p.series!)
|
|
207
220
|
)
|
|
208
221
|
);
|
|
@@ -294,7 +307,7 @@
|
|
|
294
307
|
barLayout,
|
|
295
308
|
isHorizontal,
|
|
296
309
|
hoverPoints,
|
|
297
|
-
|
|
310
|
+
colorFor: colorForSeries,
|
|
298
311
|
disabledFillFor: (name) => disabledFillOverride(effectiveDisabledFor(name)),
|
|
299
312
|
active: hover.active,
|
|
300
313
|
setHoverMatch,
|
|
@@ -25,7 +25,7 @@ export declare function buildBarMarkers<TData extends Record<string, unknown>>(a
|
|
|
25
25
|
barLayout: BarLayout<TData>;
|
|
26
26
|
isHorizontal: boolean;
|
|
27
27
|
hoverPoints: HoverPoint<TData>[];
|
|
28
|
-
|
|
28
|
+
colorFor: (seriesName: string) => string;
|
|
29
29
|
disabledFillFor: (seriesName: string) => {
|
|
30
30
|
fill?: string;
|
|
31
31
|
fillOpacity?: number;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { resolveAccessor } from '../utils/accessors';
|
|
2
|
-
import { paletteColor } from '../../utils/color';
|
|
3
2
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
4
3
|
import { isGapValue, resolveGapAreaStyle } from '../utils/gaps';
|
|
5
4
|
const GAP_KINDS = ['start', 'middle', 'end'];
|
|
@@ -19,13 +18,13 @@ const GAP_KINDS = ['start', 'middle', 'end'];
|
|
|
19
18
|
* where the marker actually renders.
|
|
20
19
|
*/
|
|
21
20
|
export function buildBarMarkers(args) {
|
|
22
|
-
const { groupedSeries, barLayout, isHorizontal, hoverPoints,
|
|
21
|
+
const { groupedSeries, barLayout, isHorizontal, hoverPoints, colorFor, disabledFillFor, active, setHoverMatch, clearHoverMatch, gapFillMaps } = args;
|
|
23
22
|
const gapDefaults = getConfiguration().bar.gap;
|
|
24
23
|
const segmentMarkers = [];
|
|
25
24
|
groupedSeries.forEach((group, seriesIndex) => {
|
|
26
25
|
const xFn = resolveAccessor(group.series.x);
|
|
27
26
|
const yFn = resolveAccessor(group.series.y);
|
|
28
|
-
const defaultColor =
|
|
27
|
+
const defaultColor = colorFor(group.series.name);
|
|
29
28
|
const disabledFill = disabledFillFor(group.series.name);
|
|
30
29
|
// Places one series' bar(s) — real or gap-fill — using whichever
|
|
31
30
|
// layout mode (stacked baseline, or flat baseline + grouped insets) is
|
|
@@ -12,4 +12,4 @@ import type { BarLayout } from './layout.svelte';
|
|
|
12
12
|
* height instead of `dx`/`dy`, since that offset is representable as a
|
|
13
13
|
* regular data value (unlike 'grouped''s pixel-only slice offset).
|
|
14
14
|
*/
|
|
15
|
-
export declare function buildHoverPoints<TData extends Record<string, unknown>>(resolvedSeries: Series<TData>[], isHorizontal: boolean, geometry: Pick<BarLayout<TData>, 'layout' | 'stackedBaseline' | 'groupOffset'>,
|
|
15
|
+
export declare function buildHoverPoints<TData extends Record<string, unknown>>(resolvedSeries: Series<TData>[], isHorizontal: boolean, geometry: Pick<BarLayout<TData>, 'layout' | 'stackedBaseline' | 'groupOffset'>, colorFor: (seriesName: string) => string): HoverPoint<TData>[];
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { resolveAccessor } from '../utils/accessors';
|
|
2
|
-
import { paletteColor } from '../../utils/color';
|
|
3
2
|
import { isGapValue } from '../utils/gaps';
|
|
4
3
|
/**
|
|
5
4
|
* Builds hover-marker candidate points for every row across every series.
|
|
@@ -12,7 +11,7 @@ import { isGapValue } from '../utils/gaps';
|
|
|
12
11
|
* height instead of `dx`/`dy`, since that offset is representable as a
|
|
13
12
|
* regular data value (unlike 'grouped''s pixel-only slice offset).
|
|
14
13
|
*/
|
|
15
|
-
export function buildHoverPoints(resolvedSeries, isHorizontal, geometry,
|
|
14
|
+
export function buildHoverPoints(resolvedSeries, isHorizontal, geometry, colorFor) {
|
|
16
15
|
return resolvedSeries.flatMap((s, idx) => {
|
|
17
16
|
const sx = resolveAccessor(s.x);
|
|
18
17
|
const sy = resolveAccessor(s.y);
|
|
@@ -29,7 +28,7 @@ export function buildHoverPoints(resolvedSeries, isHorizontal, geometry, palette
|
|
|
29
28
|
row: d,
|
|
30
29
|
label: String(sy(d)),
|
|
31
30
|
series: s.name,
|
|
32
|
-
color:
|
|
31
|
+
color: colorFor(s.name),
|
|
33
32
|
};
|
|
34
33
|
});
|
|
35
34
|
});
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
import { paletteColor } from '../../utils/color';
|
|
5
5
|
import { buildSeries } from '../../utils/grouping';
|
|
6
6
|
import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
disabledStrokeOverride,
|
|
9
|
+
disabledDotsOverride,
|
|
10
|
+
resolveEffectiveDisabledStyle,
|
|
11
|
+
omitDisabledSeries,
|
|
12
|
+
buildSeriesColorIndex
|
|
13
|
+
} from '../utils/legendDisabled';
|
|
8
14
|
import { buildLineMarkers } from './buildLineMarkers';
|
|
9
15
|
import { buildGapMarkers } from './buildGapMarkers';
|
|
10
16
|
import { isGapValue } from '../utils/gaps';
|
|
@@ -64,8 +70,16 @@
|
|
|
64
70
|
// resolved per series.
|
|
65
71
|
const gaps = $derived(styles.gaps ?? {});
|
|
66
72
|
|
|
67
|
-
const
|
|
73
|
+
const cfg = $derived(getConfiguration());
|
|
68
74
|
const legendInteraction = $derived(getLegendInteraction());
|
|
75
|
+
// Unfiltered series list, for stable per-name color indexing.
|
|
76
|
+
const fullSeries = $derived(series ?? buildSeries(data!, x!, y!, z));
|
|
77
|
+
const colorIndex = $derived(buildSeriesColorIndex(fullSeries));
|
|
78
|
+
function colorForSeries(name: string): string {
|
|
79
|
+
return paletteColor(colorIndex.get(name) ?? 0, cfg.palette);
|
|
80
|
+
}
|
|
81
|
+
// Drops series toggled off with `disabled: { mode: 'omit' }`.
|
|
82
|
+
const resolvedSeries = $derived(omitDisabledSeries(fullSeries, legendInteraction));
|
|
69
83
|
|
|
70
84
|
$effect(() => {
|
|
71
85
|
validateSegments(segments);
|
|
@@ -75,7 +89,6 @@
|
|
|
75
89
|
buildGroupedSeries<Series<TData>, LineSegmentStyle>(resolvedSeries, segments)
|
|
76
90
|
);
|
|
77
91
|
|
|
78
|
-
const cfg = $derived(getConfiguration());
|
|
79
92
|
const showVals = $derived(styles.values?.show ?? false);
|
|
80
93
|
const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
|
|
81
94
|
const valAnchor = $derived<ValueAnchor>(styles.values?.anchor ?? 'start');
|
|
@@ -86,11 +99,12 @@
|
|
|
86
99
|
let hoverIsolatedSeries = $state<string | null>(null);
|
|
87
100
|
|
|
88
101
|
function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
|
|
89
|
-
return (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
return resolveEffectiveDisabledStyle(
|
|
103
|
+
name,
|
|
104
|
+
legendInteraction,
|
|
105
|
+
cfg.legend.disabledOpacity,
|
|
106
|
+
hoverIsolate,
|
|
107
|
+
hoverIsolatedSeries
|
|
94
108
|
);
|
|
95
109
|
}
|
|
96
110
|
|
|
@@ -99,7 +113,7 @@
|
|
|
99
113
|
const flat = $derived(resolvedSeries.flatMap((s) => s.data));
|
|
100
114
|
// Legend-disabled series and gap rows are excluded from hover candidates.
|
|
101
115
|
const hoverPoints = $derived(
|
|
102
|
-
resolvedSeries.flatMap((s
|
|
116
|
+
resolvedSeries.flatMap((s) => {
|
|
103
117
|
if (legendInteraction?.disabledSeries.has(s.name)) return [];
|
|
104
118
|
const sx = resolveAccessor(s.x);
|
|
105
119
|
const sy = resolveAccessor(s.y);
|
|
@@ -111,7 +125,7 @@
|
|
|
111
125
|
row: d,
|
|
112
126
|
label: String(sy(d)),
|
|
113
127
|
series: s.name,
|
|
114
|
-
color:
|
|
128
|
+
color: colorForSeries(s.name)
|
|
115
129
|
}));
|
|
116
130
|
})
|
|
117
131
|
);
|
|
@@ -128,6 +142,7 @@
|
|
|
128
142
|
const lineMarkers = $derived(
|
|
129
143
|
buildLineMarkers({
|
|
130
144
|
groupedSeries,
|
|
145
|
+
colorFor: colorForSeries,
|
|
131
146
|
disabledStrokeFor: (name) => disabledStrokeOverride(effectiveDisabledFor(name)),
|
|
132
147
|
disabledDotsFor: (name) => disabledDotsOverride(effectiveDisabledFor(name))
|
|
133
148
|
})
|
|
@@ -137,7 +152,7 @@
|
|
|
137
152
|
buildGapMarkers<TData>({
|
|
138
153
|
seriesList: resolvedSeries,
|
|
139
154
|
gaps,
|
|
140
|
-
colorFor: (seriesIndex) =>
|
|
155
|
+
colorFor: (seriesIndex) => colorForSeries(resolvedSeries[seriesIndex].name),
|
|
141
156
|
disabledStrokeFor: (name) => disabledStrokeOverride(effectiveDisabledFor(name)),
|
|
142
157
|
disabledDotsFor: (name) => disabledDotsOverride(effectiveDisabledFor(name))
|
|
143
158
|
})
|
|
@@ -147,14 +162,14 @@
|
|
|
147
162
|
buildScopedMarkerGroups<TData, Series<TData>, LineSegmentStyle>({
|
|
148
163
|
groupedSeries,
|
|
149
164
|
segments,
|
|
150
|
-
colorFor: (
|
|
165
|
+
colorFor: (series) => colorForSeries(series.name),
|
|
151
166
|
segmentColorFor: (style, defaultColor) => style?.stroke?.stroke ?? defaultColor
|
|
152
167
|
})
|
|
153
168
|
);
|
|
154
169
|
|
|
155
170
|
const lastPoints = $derived(
|
|
156
171
|
resolvedSeries
|
|
157
|
-
.map((s
|
|
172
|
+
.map((s) => {
|
|
158
173
|
const sy = resolveAccessor(s.y);
|
|
159
174
|
const lastRow = [...s.data].reverse().find((d) => !isGapValue(sy(d)));
|
|
160
175
|
if (lastRow === undefined) return undefined;
|
|
@@ -164,7 +179,7 @@
|
|
|
164
179
|
matchedSeg?.color ??
|
|
165
180
|
group?.segments[0]?.color ??
|
|
166
181
|
group?.color ??
|
|
167
|
-
|
|
182
|
+
colorForSeries(s.name);
|
|
168
183
|
return { series: s, lastRow, color };
|
|
169
184
|
})
|
|
170
185
|
.filter((p): p is { series: Series<TData>; lastRow: TData; color: string } => p !== undefined)
|
|
@@ -10,6 +10,7 @@ import type { LineMarkerConfig, DotMarkerConfig } from '../../types/markers/comm
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function buildLineMarkers<TData extends Record<string, unknown>>(args: {
|
|
12
12
|
groupedSeries: VisualGroup<Series<TData>, LineSegmentStyle>[];
|
|
13
|
+
colorFor: (seriesName: string) => string;
|
|
13
14
|
disabledStrokeFor: (seriesName: string) => StrokeStyle | undefined;
|
|
14
15
|
disabledDotsFor: (seriesName: string) => DotStyle | undefined;
|
|
15
16
|
}): (LineMarkerConfig | DotMarkerConfig)[];
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { resolveAccessor } from '../utils/accessors';
|
|
2
|
-
import { paletteColor } from '../../utils/color';
|
|
3
2
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
4
3
|
/**
|
|
5
4
|
* Every `'line'` (+ `'dot'`, where a segment's style asks for them) marker
|
|
@@ -7,11 +6,11 @@ import { getConfiguration } from '../../configuration/config.svelte';
|
|
|
7
6
|
* `'dot'` marker right after it when that segment sets `style.dots`.
|
|
8
7
|
*/
|
|
9
8
|
export function buildLineMarkers(args) {
|
|
10
|
-
const { groupedSeries, disabledStrokeFor, disabledDotsFor } = args;
|
|
9
|
+
const { groupedSeries, colorFor, disabledStrokeFor, disabledDotsFor } = args;
|
|
11
10
|
const cfg = getConfiguration();
|
|
12
11
|
const markers = [];
|
|
13
|
-
groupedSeries.forEach((group
|
|
14
|
-
const defaultColor =
|
|
12
|
+
groupedSeries.forEach((group) => {
|
|
13
|
+
const defaultColor = colorFor(group.series.name);
|
|
15
14
|
const disabledStroke = disabledStrokeFor(group.series.name);
|
|
16
15
|
const disabledDots = disabledDotsFor(group.series.name);
|
|
17
16
|
const xFn = resolveAccessor(group.series.x);
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
import { paletteColor } from '../../utils/color';
|
|
5
5
|
import { buildSeries } from '../../utils/grouping';
|
|
6
6
|
import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
disabledFillOverride,
|
|
9
|
+
resolveEffectiveDisabledStyle
|
|
10
|
+
} from '../utils/legendDisabled';
|
|
8
11
|
import { buildPyramidBarMarkers } from './buildPyramidBarMarkers';
|
|
9
12
|
import {
|
|
10
13
|
createLabelMarginTracker,
|
|
@@ -90,11 +93,12 @@
|
|
|
90
93
|
let hoverIsolatedSeries = $state<string | null>(null);
|
|
91
94
|
|
|
92
95
|
function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
|
|
93
|
-
return (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
return resolveEffectiveDisabledStyle(
|
|
97
|
+
name,
|
|
98
|
+
legendInteraction,
|
|
99
|
+
cfg.legend.disabledOpacity,
|
|
100
|
+
hoverIsolate,
|
|
101
|
+
hoverIsolatedSeries
|
|
98
102
|
);
|
|
99
103
|
}
|
|
100
104
|
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
import { paletteColor } from '../../utils/color';
|
|
5
5
|
import { buildSeries } from '../../utils/grouping';
|
|
6
6
|
import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
disabledDotsOverride,
|
|
9
|
+
resolveEffectiveDisabledStyle,
|
|
10
|
+
omitDisabledSeries,
|
|
11
|
+
buildSeriesColorIndex
|
|
12
|
+
} from '../utils/legendDisabled';
|
|
8
13
|
import { buildScatterMarkers } from './buildScatterMarkers';
|
|
9
14
|
import { resolveRadiusPaddedDomain } from './resolveRadiusPaddedDomain';
|
|
10
15
|
import {
|
|
@@ -67,8 +72,16 @@
|
|
|
67
72
|
tooltip = undefined
|
|
68
73
|
}: Props = $props();
|
|
69
74
|
|
|
70
|
-
const
|
|
75
|
+
const cfg = $derived(getConfiguration());
|
|
71
76
|
const legendInteraction = $derived(getLegendInteraction());
|
|
77
|
+
// Unfiltered series list, for stable per-name color indexing.
|
|
78
|
+
const fullSeries = $derived(series ?? buildSeries(data!, x!, y!, z));
|
|
79
|
+
const colorIndex = $derived(buildSeriesColorIndex(fullSeries));
|
|
80
|
+
function colorForSeries(name: string): string {
|
|
81
|
+
return paletteColor(colorIndex.get(name) ?? 0, cfg.palette);
|
|
82
|
+
}
|
|
83
|
+
// Drops series toggled off with `disabled: { mode: 'omit' }`.
|
|
84
|
+
const resolvedSeries = $derived(omitDisabledSeries(fullSeries, legendInteraction));
|
|
72
85
|
|
|
73
86
|
$effect(() => {
|
|
74
87
|
validateSegments(segments);
|
|
@@ -81,7 +94,6 @@
|
|
|
81
94
|
buildGroupedSeries<Series<TData>, ScatterSegmentStyle>(resolvedSeries, segments, false)
|
|
82
95
|
);
|
|
83
96
|
|
|
84
|
-
const cfg = $derived(getConfiguration());
|
|
85
97
|
const showVals = $derived(styles.values?.show ?? false);
|
|
86
98
|
const formatValue = $derived(styles.values?.format ?? ((v: number) => `${v}`));
|
|
87
99
|
const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
|
|
@@ -93,11 +105,12 @@
|
|
|
93
105
|
let hoverIsolatedSeries = $state<string | null>(null);
|
|
94
106
|
|
|
95
107
|
function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
|
|
96
|
-
return (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
return resolveEffectiveDisabledStyle(
|
|
109
|
+
name,
|
|
110
|
+
legendInteraction,
|
|
111
|
+
cfg.legend.disabledOpacity,
|
|
112
|
+
hoverIsolate,
|
|
113
|
+
hoverIsolatedSeries
|
|
101
114
|
);
|
|
102
115
|
}
|
|
103
116
|
|
|
@@ -108,7 +121,7 @@
|
|
|
108
121
|
// A legend-disabled series is dimmed on the mark itself, but never surfaces
|
|
109
122
|
// in hover/tooltip — its points are simply absent from the candidate list.
|
|
110
123
|
const hoverPoints = $derived(
|
|
111
|
-
resolvedSeries.flatMap((s
|
|
124
|
+
resolvedSeries.flatMap((s) => {
|
|
112
125
|
if (legendInteraction?.disabledSeries.has(s.name)) return [];
|
|
113
126
|
const sx = resolveAccessor(s.x);
|
|
114
127
|
const sy = resolveAccessor(s.y);
|
|
@@ -118,7 +131,7 @@
|
|
|
118
131
|
row: d,
|
|
119
132
|
label: `${sx(d)}, ${sy(d)}`,
|
|
120
133
|
series: s.name,
|
|
121
|
-
color:
|
|
134
|
+
color: colorForSeries(s.name)
|
|
122
135
|
}));
|
|
123
136
|
})
|
|
124
137
|
);
|
|
@@ -138,6 +151,7 @@
|
|
|
138
151
|
buildScatterMarkers({
|
|
139
152
|
groupedSeries,
|
|
140
153
|
r,
|
|
154
|
+
colorFor: colorForSeries,
|
|
141
155
|
disabledDotsFor: (name) => disabledDotsOverride(effectiveDisabledFor(name))
|
|
142
156
|
})
|
|
143
157
|
);
|
|
@@ -146,7 +160,7 @@
|
|
|
146
160
|
buildScopedMarkerGroups<TData, Series<TData>, ScatterSegmentStyle>({
|
|
147
161
|
groupedSeries,
|
|
148
162
|
segments,
|
|
149
|
-
colorFor: (
|
|
163
|
+
colorFor: (series) => colorForSeries(series.name),
|
|
150
164
|
segmentColorFor: (style, defaultColor) => style?.dotFill ?? defaultColor
|
|
151
165
|
})
|
|
152
166
|
);
|
|
@@ -7,5 +7,6 @@ import type { DotMarkerConfig } from '../../types/markers/common';
|
|
|
7
7
|
export declare function buildScatterMarkers<TData extends Record<string, unknown>>(args: {
|
|
8
8
|
groupedSeries: VisualGroup<Series<TData>, ScatterSegmentStyle>[];
|
|
9
9
|
r?: Accessor<TData, number> | number;
|
|
10
|
+
colorFor: (seriesName: string) => string;
|
|
10
11
|
disabledDotsFor: (seriesName: string) => DotStyle | undefined;
|
|
11
12
|
}): DotMarkerConfig[];
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { resolveAccessor } from '../utils/accessors';
|
|
2
|
-
import { paletteColor } from '../../utils/color';
|
|
3
2
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
4
3
|
/** Every `'dot'` marker for one ScatterPlot render: one per visual segment. */
|
|
5
4
|
export function buildScatterMarkers(args) {
|
|
6
|
-
const { groupedSeries, r, disabledDotsFor } = args;
|
|
5
|
+
const { groupedSeries, r, colorFor, disabledDotsFor } = args;
|
|
7
6
|
const cfg = getConfiguration();
|
|
8
7
|
const rFn = r === undefined || typeof r === 'number' ? r : resolveAccessor(r);
|
|
9
8
|
const markers = [];
|
|
10
|
-
groupedSeries.forEach((group
|
|
11
|
-
const defaultColor =
|
|
9
|
+
groupedSeries.forEach((group) => {
|
|
10
|
+
const defaultColor = colorFor(group.series.name);
|
|
12
11
|
const disabledDots = disabledDotsFor(group.series.name);
|
|
13
12
|
const xFn = resolveAccessor(group.series.x);
|
|
14
13
|
const yFn = resolveAccessor(group.series.y);
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
1
|
+
import type { LegendDisabledAction, LegendDisabledStyle, LegendInteractionStore } from '../../types/layout/legend';
|
|
2
2
|
import type { StrokeStyle, DotStyle } from '../../types/plots/styling';
|
|
3
|
+
/** Resolves a {@link LegendDisabledAction} into the style a mark should render with — `'omit'` resolves to fully transparent. */
|
|
4
|
+
export declare function resolveDisabledStyle(action: LegendDisabledAction | undefined, fallbackOpacity: number): LegendDisabledStyle | undefined;
|
|
5
|
+
/** Drops series toggled off with `disabled: { mode: 'omit' }` from a series list. */
|
|
6
|
+
export declare function omitDisabledSeries<S extends {
|
|
7
|
+
name: string;
|
|
8
|
+
}>(series: S[], legendInteraction: LegendInteractionStore | null | undefined): S[];
|
|
9
|
+
/** Builds a per-series-name → palette index from the full series list, before {@link omitDisabledSeries} filters it. */
|
|
10
|
+
export declare function buildSeriesColorIndex<S extends {
|
|
11
|
+
name: string;
|
|
12
|
+
}>(series: S[]): Map<string, number>;
|
|
13
|
+
/** Resolves the disabled style for a series by name: legend-off state, falling back to hover-isolate dim. */
|
|
14
|
+
export declare function resolveEffectiveDisabledStyle(name: string, legendInteraction: LegendInteractionStore | null | undefined, disabledOpacity: number, hoverIsolate: boolean, hoverIsolatedSeries: string | null): LegendDisabledStyle | undefined;
|
|
3
15
|
/**
|
|
4
16
|
* The three per-mark override layers a legend "disabled" state can apply,
|
|
5
17
|
* derived once from a {@link LegendDisabledStyle}. Each is `undefined` when
|
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
/** Resolves a {@link LegendDisabledAction} into the style a mark should render with — `'omit'` resolves to fully transparent. */
|
|
2
|
+
export function resolveDisabledStyle(action, fallbackOpacity) {
|
|
3
|
+
if (!action)
|
|
4
|
+
return undefined;
|
|
5
|
+
if (action.mode === 'omit')
|
|
6
|
+
return { opacity: 0 };
|
|
7
|
+
return action.style ?? { opacity: fallbackOpacity };
|
|
8
|
+
}
|
|
9
|
+
/** Drops series toggled off with `disabled: { mode: 'omit' }` from a series list. */
|
|
10
|
+
export function omitDisabledSeries(series, legendInteraction) {
|
|
11
|
+
if (!legendInteraction)
|
|
12
|
+
return series;
|
|
13
|
+
return series.filter((s) => legendInteraction.disabledSeries.get(s.name)?.mode !== 'omit');
|
|
14
|
+
}
|
|
15
|
+
/** Builds a per-series-name → palette index from the full series list, before {@link omitDisabledSeries} filters it. */
|
|
16
|
+
export function buildSeriesColorIndex(series) {
|
|
17
|
+
const index = new Map();
|
|
18
|
+
series.forEach((s, i) => index.set(s.name, i));
|
|
19
|
+
return index;
|
|
20
|
+
}
|
|
21
|
+
/** Resolves the disabled style for a series by name: legend-off state, falling back to hover-isolate dim. */
|
|
22
|
+
export function resolveEffectiveDisabledStyle(name, legendInteraction, disabledOpacity, hoverIsolate, hoverIsolatedSeries) {
|
|
23
|
+
return (resolveDisabledStyle(legendInteraction?.disabledSeries.get(name), disabledOpacity) ??
|
|
24
|
+
(hoverIsolate && hoverIsolatedSeries && hoverIsolatedSeries !== name
|
|
25
|
+
? { opacity: disabledOpacity }
|
|
26
|
+
: undefined));
|
|
27
|
+
}
|
|
1
28
|
/**
|
|
2
29
|
* The three per-mark override layers a legend "disabled" state can apply,
|
|
3
30
|
* derived once from a {@link LegendDisabledStyle}. Each is `undefined` when
|
|
@@ -37,7 +37,7 @@ export type ChartConfig = {
|
|
|
37
37
|
gap: string;
|
|
38
38
|
/** swatch square size */
|
|
39
39
|
swatchSize: string;
|
|
40
|
-
/** Fallback opacity for a legend-disabled series/value when a section doesn't set its own
|
|
40
|
+
/** Fallback opacity for a legend-disabled series/value when a section doesn't set its own style. */
|
|
41
41
|
disabledOpacity: number;
|
|
42
42
|
/** Prev/next arrows shown when a `layout: { type: 'horizontal' }` discrete section overflows `maxRows`. */
|
|
43
43
|
navButton: {
|
|
@@ -160,7 +160,11 @@ export type ChartConfig = {
|
|
|
160
160
|
activeColor: string;
|
|
161
161
|
thickness: number;
|
|
162
162
|
};
|
|
163
|
-
/**
|
|
163
|
+
/**
|
|
164
|
+
* In-SVG hover highlight: crosshair rule, dot + value label, and the area
|
|
165
|
+
* highlight — a restyled feature on `GeoPlot`, or an `areaX`/`areaY` band
|
|
166
|
+
* on an axis-based plot's band scale.
|
|
167
|
+
*/
|
|
164
168
|
hover: {
|
|
165
169
|
rule: StrokeStyle;
|
|
166
170
|
dot: DotStyle;
|
|
@@ -17,6 +17,13 @@ export type LegendDisabledStyle = {
|
|
|
17
17
|
fill?: string;
|
|
18
18
|
fillOpacity?: number;
|
|
19
19
|
};
|
|
20
|
+
/** What a legend toggle does to a series once it's off: `'style'` dims it in place, `'omit'` removes it from the plot entirely. */
|
|
21
|
+
export type LegendDisabledAction = {
|
|
22
|
+
mode: 'style';
|
|
23
|
+
style?: LegendDisabledStyle;
|
|
24
|
+
} | {
|
|
25
|
+
mode: 'omit';
|
|
26
|
+
};
|
|
20
27
|
export type LegendItem = {
|
|
21
28
|
name: string;
|
|
22
29
|
label?: string;
|
|
@@ -28,11 +35,9 @@ export type LegendItem = {
|
|
|
28
35
|
disabled?: boolean;
|
|
29
36
|
};
|
|
30
37
|
/**
|
|
31
|
-
* How a discrete section's items flow. `'vertical'`
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* `maxRows` (default 1), the section paginates instead of growing taller,
|
|
35
|
-
* surfacing prev/next arrows to page through the rest.
|
|
38
|
+
* How a discrete section's items flow. `'vertical'` stacks them in a
|
|
39
|
+
* `columns`-wide grid; `'horizontal'` flows them left to right, paginating
|
|
40
|
+
* past `maxRows` (default 1) with prev/next arrows.
|
|
36
41
|
*/
|
|
37
42
|
export type LegendItemsLayout = {
|
|
38
43
|
type: 'vertical';
|
|
@@ -75,8 +80,8 @@ export type DiscreteInteractiveConfig = {
|
|
|
75
80
|
enabled?: boolean;
|
|
76
81
|
/** Item look while `enabled`. Defaults to `'badge'`. */
|
|
77
82
|
legendStyle?: LegendInteractiveStyle;
|
|
78
|
-
/**
|
|
79
|
-
|
|
83
|
+
/** What happens to a toggled-off item. Defaults to `{ mode: 'style' }`. */
|
|
84
|
+
disabled?: LegendDisabledAction;
|
|
80
85
|
/** Shows the symbol icon inside a `'badge'` pill. Defaults to `true`; ignored by `'basic'`, which always shows it. */
|
|
81
86
|
showIcon?: boolean;
|
|
82
87
|
};
|
|
@@ -85,7 +90,7 @@ export type DiscreteLegendSection = {
|
|
|
85
90
|
title?: string;
|
|
86
91
|
items: LegendItem[];
|
|
87
92
|
columns?: number;
|
|
88
|
-
/** Item flow direction. Defaults to
|
|
93
|
+
/** Item flow direction. Defaults to horizontal for a lone section, vertical once there's more than one. */
|
|
89
94
|
layout?: LegendItemsLayout;
|
|
90
95
|
interactive?: DiscreteInteractiveConfig;
|
|
91
96
|
navButton?: LegendNavSnippet;
|
|
@@ -129,8 +134,8 @@ export type LegendSection = DiscreteLegendSection | ContinuousLegendSection;
|
|
|
129
134
|
* own private store via `createLegendInteraction`.
|
|
130
135
|
*/
|
|
131
136
|
export type LegendInteractionStore = {
|
|
132
|
-
/** Legend item name →
|
|
133
|
-
disabledSeries: SvelteMap<string,
|
|
137
|
+
/** Legend item name → action to apply. Presence in the map means "disabled". */
|
|
138
|
+
disabledSeries: SvelteMap<string, LegendDisabledAction>;
|
|
134
139
|
/**
|
|
135
140
|
* The active drag state of the (first) interactive continuous section, or
|
|
136
141
|
* `null` when none is interactive or the user hasn't narrowed it yet (full
|
|
@@ -22,9 +22,18 @@ export type HoverMarkerConfig = {
|
|
|
22
22
|
sync?: boolean;
|
|
23
23
|
ruleX?: boolean;
|
|
24
24
|
ruleY?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Thickened area in place of a thin crosshair — sized to the hovered
|
|
27
|
+
* bar/cell's own width, spanning the full opposite axis. Off by default;
|
|
28
|
+
* meaningful only on a band (categorical) scale, so it renders nothing on
|
|
29
|
+
* a line/scatter plot's continuous axis.
|
|
30
|
+
*/
|
|
31
|
+
areaX?: boolean;
|
|
32
|
+
areaY?: boolean;
|
|
25
33
|
showLabels?: boolean;
|
|
26
34
|
format?: (point: HoverDisplayPoint) => string;
|
|
27
35
|
ruleStyle?: StrokeStyle;
|
|
36
|
+
areaStyle?: AreaStyle;
|
|
28
37
|
dotStyle?: DotStyle;
|
|
29
38
|
fontStyle?: FontStyle;
|
|
30
39
|
};
|
|
@@ -40,11 +49,14 @@ export type LineMarkerConfig = {
|
|
|
40
49
|
* A single bar mark. `role: 'segment'` is a visible bar
|
|
41
50
|
* (`x`/`y`/`x1`/`x2`/`y1`/`y2` map to svelteplot's `<BarX>`/`<BarY>` per
|
|
42
51
|
* `isHorizontal`); `role: 'hitArea'` is an invisible per-category hit region
|
|
43
|
-
* for hover, resolved against the live scale at render time
|
|
52
|
+
* for hover, resolved against the live scale at render time; `role:
|
|
53
|
+
* 'highlight'` is `HoverMarkerConfig`'s `areaX`/`areaY` highlight — a
|
|
54
|
+
* visible, non-interactive rect centered on `category`, spanning the full
|
|
55
|
+
* opposite axis, sized to the category band's own width plus 2px.
|
|
44
56
|
*/
|
|
45
57
|
export type BarMarkerConfig = {
|
|
46
58
|
type: 'bar';
|
|
47
|
-
role: 'segment' | 'hitArea';
|
|
59
|
+
role: 'segment' | 'hitArea' | 'highlight';
|
|
48
60
|
isHorizontal: boolean;
|
|
49
61
|
data: Record<string, unknown>[];
|
|
50
62
|
x?: (d: any) => AxisValue;
|