@fundar/data-chart-telling 0.0.33 → 0.0.34
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 +31 -15
- package/dist/configuration/themes/index.d.ts +28 -0
- package/dist/index.d.ts +13 -13
- package/dist/index.js +2 -2
- package/dist/layout/legend/DiscreteSection.svelte +348 -133
- package/dist/types/configuration/styling.d.ts +8 -0
- package/dist/types/layout/legend.d.ts +25 -4
- package/package.json +1 -1
|
@@ -22,7 +22,7 @@ const text = (over) => ({
|
|
|
22
22
|
color: 'inherit',
|
|
23
23
|
leading: '1.3',
|
|
24
24
|
spacing: 'normal',
|
|
25
|
-
...over
|
|
25
|
+
...over
|
|
26
26
|
});
|
|
27
27
|
export const DEFAULT_CONFIG = {
|
|
28
28
|
font: 'inherit',
|
|
@@ -32,10 +32,14 @@ export const DEFAULT_CONFIG = {
|
|
|
32
32
|
spacing: {
|
|
33
33
|
headerPadding: '0.75rem 0 1rem',
|
|
34
34
|
legendPadding: '0.75rem 0',
|
|
35
|
-
captionPadding: '0.5rem 0'
|
|
35
|
+
captionPadding: '0.5rem 0'
|
|
36
36
|
},
|
|
37
37
|
title: text({ size: '1.5rem', weight: 500, leading: '1.2' }),
|
|
38
|
-
subtitle: text({
|
|
38
|
+
subtitle: text({
|
|
39
|
+
size: '1rem',
|
|
40
|
+
weight: 400,
|
|
41
|
+
color: 'color-mix(in srgb, currentColor 85%, transparent)'
|
|
42
|
+
}),
|
|
39
43
|
caption: text({ size: '0.75rem', color: 'color-mix(in srgb, currentColor 75%, transparent)' }),
|
|
40
44
|
legend: {
|
|
41
45
|
font: 'inherit',
|
|
@@ -47,6 +51,13 @@ export const DEFAULT_CONFIG = {
|
|
|
47
51
|
gap: '0.5rem 2rem',
|
|
48
52
|
swatchSize: '1.25rem',
|
|
49
53
|
disabledOpacity: 0.12,
|
|
54
|
+
navButton: {
|
|
55
|
+
background: 'transparent',
|
|
56
|
+
color: 'currentColor',
|
|
57
|
+
size: '1.5rem',
|
|
58
|
+
fontSize: '1rem',
|
|
59
|
+
disabledOpacity: 0.3
|
|
60
|
+
}
|
|
50
61
|
},
|
|
51
62
|
axis: {
|
|
52
63
|
color: 'currentColor',
|
|
@@ -57,7 +68,7 @@ export const DEFAULT_CONFIG = {
|
|
|
57
68
|
yTickSize: 5,
|
|
58
69
|
xTickPadding: 10,
|
|
59
70
|
yTickPadding: 10,
|
|
60
|
-
grid: false
|
|
71
|
+
grid: false
|
|
61
72
|
},
|
|
62
73
|
tooltip: {
|
|
63
74
|
background: '#111827',
|
|
@@ -65,7 +76,7 @@ export const DEFAULT_CONFIG = {
|
|
|
65
76
|
fontSize: '0.8125rem',
|
|
66
77
|
padding: '0.5rem 0.75rem',
|
|
67
78
|
radius: '0.375rem',
|
|
68
|
-
border: 'none'
|
|
79
|
+
border: 'none'
|
|
69
80
|
},
|
|
70
81
|
margins: { top: 'auto', right: 'auto', bottom: 'auto', left: 'auto' },
|
|
71
82
|
palette: ['#4f46e5', '#e6580d', '#16a34a', '#dc2626', '#0891b2', '#9333ea', '#ca8a04', '#db2777'],
|
|
@@ -75,15 +86,15 @@ export const DEFAULT_CONFIG = {
|
|
|
75
86
|
dotRadius: 4,
|
|
76
87
|
gap: {
|
|
77
88
|
stroke: { strokeOpacity: 0.6, strokeDasharray: '4 4' },
|
|
78
|
-
dots: {}
|
|
89
|
+
dots: {}
|
|
79
90
|
},
|
|
80
91
|
valueLabels: {
|
|
81
92
|
elbowGap: 10,
|
|
82
93
|
laneStep: 7,
|
|
83
94
|
labelGap: 6,
|
|
84
95
|
connectorColor: '#94a3b8',
|
|
85
|
-
connectorWidth: 0.5
|
|
86
|
-
}
|
|
96
|
+
connectorWidth: 0.5
|
|
97
|
+
}
|
|
87
98
|
},
|
|
88
99
|
bar: { gap: { fillOpacity: 0.5 } },
|
|
89
100
|
pyramid: { gap: { fillOpacity: 0.5 } },
|
|
@@ -93,7 +104,7 @@ export const DEFAULT_CONFIG = {
|
|
|
93
104
|
rule: { stroke: '#e6580d', strokeOpacity: 0.35, strokeWidth: 1.5, strokeDasharray: '4 4' },
|
|
94
105
|
dot: { dotRadius: 5, dotFill: 'currentColor' },
|
|
95
106
|
font: { fill: 'currentColor', fontSize: 12, dy: -18 },
|
|
96
|
-
area: { stroke: '#e6580d', strokeWidth: 2, fillOpacity: 0 }
|
|
107
|
+
area: { stroke: '#e6580d', strokeWidth: 2, fillOpacity: 0 }
|
|
97
108
|
},
|
|
98
109
|
facet: {
|
|
99
110
|
indicator: {
|
|
@@ -101,16 +112,16 @@ export const DEFAULT_CONFIG = {
|
|
|
101
112
|
color: '#ffffff',
|
|
102
113
|
fontSize: '0.8125rem',
|
|
103
114
|
padding: '0.125rem 0.5rem',
|
|
104
|
-
radius: '999px'
|
|
115
|
+
radius: '999px'
|
|
105
116
|
},
|
|
106
117
|
navButton: {
|
|
107
118
|
background: '#111827',
|
|
108
119
|
color: '#ffffff',
|
|
109
120
|
size: '2rem',
|
|
110
121
|
fontSize: '1.1rem',
|
|
111
|
-
disabledOpacity: 0.3
|
|
112
|
-
}
|
|
113
|
-
}
|
|
122
|
+
disabledOpacity: 0.3
|
|
123
|
+
}
|
|
124
|
+
}
|
|
114
125
|
};
|
|
115
126
|
// ── Merge ─────────────────────────────────────────────────────────────────────
|
|
116
127
|
function isPlainObject(v) {
|
|
@@ -151,7 +162,7 @@ export function configToCssVars(c) {
|
|
|
151
162
|
[`--dct-${prefix}-weight`]: String(s.weight),
|
|
152
163
|
[`--dct-${prefix}-color`]: s.color,
|
|
153
164
|
[`--dct-${prefix}-leading`]: s.leading,
|
|
154
|
-
[`--dct-${prefix}-spacing`]: s.spacing
|
|
165
|
+
[`--dct-${prefix}-spacing`]: s.spacing
|
|
155
166
|
});
|
|
156
167
|
return {
|
|
157
168
|
'--dct-font': c.font,
|
|
@@ -172,6 +183,11 @@ export function configToCssVars(c) {
|
|
|
172
183
|
'--dct-legend-title-weight': String(c.legend.titleWeight),
|
|
173
184
|
'--dct-legend-gap': c.legend.gap,
|
|
174
185
|
'--dct-legend-swatch': c.legend.swatchSize,
|
|
186
|
+
'--dct-legend-nav-bg': c.legend.navButton.background,
|
|
187
|
+
'--dct-legend-nav-color': c.legend.navButton.color,
|
|
188
|
+
'--dct-legend-nav-size': c.legend.navButton.size,
|
|
189
|
+
'--dct-legend-nav-font-size': c.legend.navButton.fontSize,
|
|
190
|
+
'--dct-legend-nav-disabled-opacity': String(c.legend.navButton.disabledOpacity),
|
|
175
191
|
'--dct-axis-color': c.axis.color,
|
|
176
192
|
'--dct-tooltip-bg': c.tooltip.background,
|
|
177
193
|
'--dct-tooltip-color': c.tooltip.color,
|
|
@@ -193,7 +209,7 @@ export function configToCssVars(c) {
|
|
|
193
209
|
'--dct-facet-nav-color': c.facet.navButton.color,
|
|
194
210
|
'--dct-facet-nav-btn-size': c.facet.navButton.size,
|
|
195
211
|
'--dct-facet-nav-font-size': c.facet.navButton.fontSize,
|
|
196
|
-
'--dct-facet-nav-disabled-opacity': String(c.facet.navButton.disabledOpacity)
|
|
212
|
+
'--dct-facet-nav-disabled-opacity': String(c.facet.navButton.disabledOpacity)
|
|
197
213
|
};
|
|
198
214
|
}
|
|
199
215
|
/** Serialize a CSS-vars record into a `style` attribute string. */
|
|
@@ -49,6 +49,13 @@ export declare const THEMES: {
|
|
|
49
49
|
gap?: string | undefined;
|
|
50
50
|
swatchSize?: string | undefined;
|
|
51
51
|
disabledOpacity?: number | undefined;
|
|
52
|
+
navButton?: {
|
|
53
|
+
background?: string | undefined;
|
|
54
|
+
color?: string | undefined;
|
|
55
|
+
size?: string | undefined;
|
|
56
|
+
fontSize?: string | undefined;
|
|
57
|
+
disabledOpacity?: number | undefined;
|
|
58
|
+
} | undefined;
|
|
52
59
|
} | undefined;
|
|
53
60
|
axis?: {
|
|
54
61
|
color?: string | undefined;
|
|
@@ -249,6 +256,13 @@ export declare const THEMES: {
|
|
|
249
256
|
gap?: string | undefined;
|
|
250
257
|
swatchSize?: string | undefined;
|
|
251
258
|
disabledOpacity?: number | undefined;
|
|
259
|
+
navButton?: {
|
|
260
|
+
background?: string | undefined;
|
|
261
|
+
color?: string | undefined;
|
|
262
|
+
size?: string | undefined;
|
|
263
|
+
fontSize?: string | undefined;
|
|
264
|
+
disabledOpacity?: number | undefined;
|
|
265
|
+
} | undefined;
|
|
252
266
|
} | undefined;
|
|
253
267
|
axis?: {
|
|
254
268
|
color?: string | undefined;
|
|
@@ -449,6 +463,13 @@ export declare const THEMES: {
|
|
|
449
463
|
gap?: string | undefined;
|
|
450
464
|
swatchSize?: string | undefined;
|
|
451
465
|
disabledOpacity?: number | undefined;
|
|
466
|
+
navButton?: {
|
|
467
|
+
background?: string | undefined;
|
|
468
|
+
color?: string | undefined;
|
|
469
|
+
size?: string | undefined;
|
|
470
|
+
fontSize?: string | undefined;
|
|
471
|
+
disabledOpacity?: number | undefined;
|
|
472
|
+
} | undefined;
|
|
452
473
|
} | undefined;
|
|
453
474
|
axis?: {
|
|
454
475
|
color?: string | undefined;
|
|
@@ -649,6 +670,13 @@ export declare const THEMES: {
|
|
|
649
670
|
gap?: string | undefined;
|
|
650
671
|
swatchSize?: string | undefined;
|
|
651
672
|
disabledOpacity?: number | undefined;
|
|
673
|
+
navButton?: {
|
|
674
|
+
background?: string | undefined;
|
|
675
|
+
color?: string | undefined;
|
|
676
|
+
size?: string | undefined;
|
|
677
|
+
fontSize?: string | undefined;
|
|
678
|
+
disabledOpacity?: number | undefined;
|
|
679
|
+
} | undefined;
|
|
652
680
|
} | undefined;
|
|
653
681
|
axis?: {
|
|
654
682
|
color?: string | undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { default as FacetLayout } from './layout/facet/FacetLayout.svelte';
|
|
|
21
21
|
export { default as FacetIndicator } from './layout/facet/FacetIndicator.svelte';
|
|
22
22
|
export { default as LegendLayout } from './layout/legend/LegendLayout.svelte';
|
|
23
23
|
export { default as Tooltip } from './layout/tooltip/Tooltip.svelte';
|
|
24
|
-
export { getLegendInteraction, provideLegendInteraction, createLegendInteraction
|
|
24
|
+
export { getLegendInteraction, provideLegendInteraction, createLegendInteraction } from './layout/legend/interaction.svelte';
|
|
25
25
|
export { default as BarMarker } from './markers/BarMarker.svelte';
|
|
26
26
|
export { default as CellMarker } from './markers/CellMarker.svelte';
|
|
27
27
|
export { default as DeltaMarker } from './markers/DeltaMarker.svelte';
|
|
@@ -31,31 +31,31 @@ export { default as LineMarker } from './markers/LineMarker.svelte';
|
|
|
31
31
|
export { default as PeakMarker } from './markers/PeakMarker.svelte';
|
|
32
32
|
export { default as RuleMarker } from './markers/RuleMarker.svelte';
|
|
33
33
|
export { default as TextMarker } from './markers/TextMarker.svelte';
|
|
34
|
-
export { setConfiguration, getConfiguration, resetConfiguration, configToCssVars, cssVarsStyle, DEFAULT_CONFIG
|
|
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
37
|
export { groupBy, buildSeries } from './utils/grouping';
|
|
38
38
|
export { makeColorScale, normalize, resolveCssColor, paletteColor } from './utils/color';
|
|
39
39
|
export type { AxisValue, AxisScale, AxisBasedScalesConfig } from './types/layout/scales';
|
|
40
40
|
export type { Accessor, Series, SeriesProps, DataProps } from './types/plots/data/common';
|
|
41
|
-
export type { BasePlotProps, StylesConfig, MarkersConfig, SegmentsConfig, ScalesConfig, MarginConfig, TooltipConfig, HoverConfig, PointMatchingStrategy
|
|
42
|
-
export type { ValuesStyle, ColorsStyle, FrameStyle, FrameProp, BasePlotStylesConfig, AxisBasedStylesConfig, GapsAwarePlotStylesConfig, GeoStylesConfig, GeoTileLayerConfig
|
|
43
|
-
export type { GapsConfig, GapZonesConfig, GapZoneConfig, OpenGapZoneConfig
|
|
41
|
+
export type { BasePlotProps, StylesConfig, MarkersConfig, SegmentsConfig, ScalesConfig, MarginConfig, TooltipConfig, HoverConfig, PointMatchingStrategy } from './types/plots/props';
|
|
42
|
+
export type { ValuesStyle, ColorsStyle, FrameStyle, FrameProp, BasePlotStylesConfig, AxisBasedStylesConfig, GapsAwarePlotStylesConfig, GeoStylesConfig, GeoTileLayerConfig } from './types/layout/styles';
|
|
43
|
+
export type { GapsConfig, GapZonesConfig, GapZoneConfig, OpenGapZoneConfig } from './types/plots/gaps';
|
|
44
44
|
export type { LineSegmentStyle, BarSegmentStyle, CellSegmentStyle, GeoSegmentStyle, ScatterSegmentStyle } from './types/plots/segments/common';
|
|
45
45
|
export type { Range, Area, Segment } from './types/plots/segments/config';
|
|
46
|
-
export type { ValueAnchor, SymbolType, LineStyle, LineCap, LineJoin, FontWeight, FontStyleKeyword, TextAnchor, LineAnchor
|
|
46
|
+
export type { ValueAnchor, SymbolType, LineStyle, LineCap, LineJoin, FontWeight, FontStyleKeyword, TextAnchor, LineAnchor } from './types/plots/constants';
|
|
47
47
|
export type { FontStyle, StrokeStyle, DotStyle, AreaStyle } from './types/plots/styling';
|
|
48
48
|
export type { AxisBasedMarkersConfig } from './types/markers/common';
|
|
49
49
|
export type { LineMarkersConfig, DeltaMarkerConfig, DeltaConfig, DeltaTarget } from './types/markers/line';
|
|
50
50
|
export type { MarkerProps } from './types/markers/props';
|
|
51
51
|
export type { GeoFeature, GeoJsonInput, GeoDataProps, Topology } from './types/plots/data/geo';
|
|
52
52
|
export type { ScatterDataProps } from './types/plots/data/scatter';
|
|
53
|
-
export type { GeoProjectionName, GeoProjectionConfig, GeoCustomProjection, GeoCustomProjectionFactory, GeoScalesConfig, GeoZoomConfig, CoordinatesConfig
|
|
54
|
-
export type { GeoMarkersConfig, GeoInsetMarkerConfig, GeoInsetTooltipConfig, GeoInsetLocation, GeoInsetProjectionName, GeoInsetProjectionConfig
|
|
55
|
-
export type { ChartPlotContext, ChartPlotSnippet
|
|
56
|
-
export type { TimeValue, Orientation, TimelineConfig
|
|
57
|
-
export type { Responsive, FacetColumns, FacetMode, FacetNavLayout, FacetConfig, FacetIndicatorContext, FacetIndicatorSnippet, FacetNavDirection, FacetNavContext, FacetNavSnippet
|
|
58
|
-
export type { LegendSection, DiscreteLegendSection, ContinuousLegendSection, LegendItem, LegendDisabledStyle, LegendInteractionStore, LegendPlotContext
|
|
59
|
-
export type { HoverStrategy, TooltipOptions, TooltipProp, TooltipAnchorX, TooltipAnchorY
|
|
53
|
+
export type { GeoProjectionName, GeoProjectionConfig, GeoCustomProjection, GeoCustomProjectionFactory, GeoScalesConfig, GeoZoomConfig, CoordinatesConfig } from './types/layout/coordinates';
|
|
54
|
+
export type { GeoMarkersConfig, GeoInsetMarkerConfig, GeoInsetTooltipConfig, GeoInsetLocation, GeoInsetProjectionName, GeoInsetProjectionConfig } from './types/markers/geo';
|
|
55
|
+
export type { ChartPlotContext, ChartPlotSnippet } from './types/charts/common';
|
|
56
|
+
export type { TimeValue, Orientation, TimelineConfig } from './types/layout/timeline';
|
|
57
|
+
export type { Responsive, FacetColumns, FacetMode, FacetNavLayout, FacetConfig, FacetIndicatorContext, FacetIndicatorSnippet, FacetNavDirection, FacetNavContext, FacetNavSnippet } from './types/layout/facet';
|
|
58
|
+
export type { LegendSection, DiscreteLegendSection, ContinuousLegendSection, LegendItem, LegendDisabledStyle, LegendItemsLayout, LegendNavDirection, LegendNavContext, LegendNavSnippet, LegendInteractionStore, LegendPlotContext } from './types/layout/legend';
|
|
59
|
+
export type { HoverStrategy, TooltipOptions, TooltipProp, TooltipAnchorX, TooltipAnchorY } from './types/layout/tooltip';
|
|
60
60
|
export type { ChartConfig, ChartConfigInput, TextStyle } from './types/configuration/styling';
|
|
61
61
|
export type { ChartProps, GapsAwareChartProps } from './types/charts/props';
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,7 @@ export { default as FacetLayout } from './layout/facet/FacetLayout.svelte';
|
|
|
27
27
|
export { default as FacetIndicator } from './layout/facet/FacetIndicator.svelte';
|
|
28
28
|
export { default as LegendLayout } from './layout/legend/LegendLayout.svelte';
|
|
29
29
|
export { default as Tooltip } from './layout/tooltip/Tooltip.svelte';
|
|
30
|
-
export { getLegendInteraction, provideLegendInteraction, createLegendInteraction
|
|
30
|
+
export { getLegendInteraction, provideLegendInteraction, createLegendInteraction } from './layout/legend/interaction.svelte';
|
|
31
31
|
// Markers
|
|
32
32
|
export { default as BarMarker } from './markers/BarMarker.svelte';
|
|
33
33
|
export { default as CellMarker } from './markers/CellMarker.svelte';
|
|
@@ -38,7 +38,7 @@ export { default as LineMarker } from './markers/LineMarker.svelte';
|
|
|
38
38
|
export { default as PeakMarker } from './markers/PeakMarker.svelte';
|
|
39
39
|
export { default as RuleMarker } from './markers/RuleMarker.svelte';
|
|
40
40
|
export { default as TextMarker } from './markers/TextMarker.svelte';
|
|
41
|
-
export { setConfiguration, getConfiguration, resetConfiguration, configToCssVars, cssVarsStyle, DEFAULT_CONFIG
|
|
41
|
+
export { setConfiguration, getConfiguration, resetConfiguration, configToCssVars, cssVarsStyle, DEFAULT_CONFIG } from './configuration/config.svelte';
|
|
42
42
|
export { THEMES } from './configuration/themes';
|
|
43
43
|
// ── Utils ─────────────────────────────────────────────────────────────────────
|
|
44
44
|
export { groupBy, buildSeries } from './utils/grouping';
|
|
@@ -1,139 +1,354 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
2
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
3
|
+
import { getConfiguration } from '../../configuration/config.svelte';
|
|
4
|
+
import { paletteColor } from '../../utils/color';
|
|
5
|
+
import {
|
|
6
|
+
getLegendInteraction,
|
|
7
|
+
createLegendInteraction
|
|
8
|
+
} from './interaction.svelte';
|
|
9
|
+
import type {
|
|
10
|
+
DiscreteLegendSection,
|
|
11
|
+
LegendItem,
|
|
12
|
+
LegendNavDirection
|
|
13
|
+
} from '../../types/layout/legend';
|
|
14
|
+
import type { LineStyle } from '../../types/plots/constants';
|
|
15
|
+
|
|
16
|
+
let { section }: { section: DiscreteLegendSection } = $props();
|
|
17
|
+
|
|
18
|
+
const cfg = $derived(getConfiguration());
|
|
19
|
+
const layout = $derived(section.layout ?? { type: 'vertical' as const });
|
|
20
|
+
const isHorizontal = $derived(layout.type === 'horizontal');
|
|
21
|
+
const maxRows = $derived(layout.type === 'horizontal' ? Math.max(1, layout.maxRows ?? 1) : 1);
|
|
22
|
+
const columns = $derived(section.columns ?? 1);
|
|
23
|
+
const interaction = getLegendInteraction() ?? createLegendInteraction();
|
|
24
|
+
|
|
25
|
+
const seeded = new SvelteSet<string>();
|
|
26
|
+
$effect(() => {
|
|
27
|
+
for (const item of section.items) {
|
|
28
|
+
if (seeded.has(item.name)) continue;
|
|
29
|
+
seeded.add(item.name);
|
|
30
|
+
if (item.disabled) {
|
|
31
|
+
interaction.disabledSeries.set(
|
|
32
|
+
item.name,
|
|
33
|
+
section.disabledStyle ?? { opacity: cfg.legend.disabledOpacity }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function toggle(item: LegendItem): void {
|
|
40
|
+
if (!section.interactive) return;
|
|
41
|
+
if (interaction.disabledSeries.has(item.name)) {
|
|
42
|
+
interaction.disabledSeries.delete(item.name);
|
|
43
|
+
} else {
|
|
44
|
+
interaction.disabledSeries.set(
|
|
45
|
+
item.name,
|
|
46
|
+
section.disabledStyle ?? { opacity: cfg.legend.disabledOpacity }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function onKeydown(event: KeyboardEvent, item: LegendItem): void {
|
|
52
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
toggle(item);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function dashArray(style?: LineStyle): string | undefined {
|
|
58
|
+
switch (style) {
|
|
59
|
+
case 'dashed':
|
|
60
|
+
return '2,5';
|
|
61
|
+
case 'dotted':
|
|
62
|
+
return '1,4';
|
|
63
|
+
case 'dashdot':
|
|
64
|
+
return '5,3,1,3';
|
|
65
|
+
default:
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Horizontal pagination ──────────────────────────────────────────────
|
|
71
|
+
// Rows are discovered by measuring where each item actually lands (its
|
|
72
|
+
// `offsetTop`) in a full, hidden copy of the list, laid out *in normal
|
|
73
|
+
// flow* right alongside the visible (paginated) one rather than
|
|
74
|
+
// `position: absolute`. That in-flow placement is deliberate, not
|
|
75
|
+
// incidental: an absolutely-positioned element is excluded from its
|
|
76
|
+
// ancestors' own width calculations, so once pagination narrowed the
|
|
77
|
+
// visible box down to one or two items, every ancestor up to the legend
|
|
78
|
+
// section — which sizes itself to fit its content, not the other way
|
|
79
|
+
// around — shrank down to hug that now-narrow content instead of staying
|
|
80
|
+
// pinned at the section's actual available width. Each further
|
|
81
|
+
// remeasurement then ran at that smaller width, wrapping even fewer
|
|
82
|
+
// items per row and shrinking again — a runaway feedback loop that read
|
|
83
|
+
// as items "disappearing" into ever-smaller pages. Keeping the full
|
|
84
|
+
// (wide) measurer in flow means its width, not the narrow visible page's,
|
|
85
|
+
// is what ancestors size against — `height: 0; overflow: hidden` below
|
|
86
|
+
// then clips away the vertical space it would otherwise take, so only
|
|
87
|
+
// its width — never its height — affects the layout, and the legend
|
|
88
|
+
// still visually shrinks the moment pagination reduces what's shown.
|
|
89
|
+
let measurerEl: HTMLDivElement | undefined = $state();
|
|
90
|
+
// Resize trigger only, so a later container resize re-runs the effect —
|
|
91
|
+
// its value isn't otherwise used (see the effect body).
|
|
92
|
+
let containerWidth = $state(0);
|
|
93
|
+
let rows = $state<number[][]>([]);
|
|
94
|
+
|
|
95
|
+
$effect(() => {
|
|
96
|
+
if (!isHorizontal || !measurerEl) {
|
|
97
|
+
rows = [];
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
void section.items;
|
|
101
|
+
void containerWidth;
|
|
102
|
+
const children = Array.from(measurerEl.children) as HTMLElement[];
|
|
103
|
+
const grouped: number[][] = [];
|
|
104
|
+
let lastTop = Number.NaN;
|
|
105
|
+
for (let i = 0; i < children.length; i++) {
|
|
106
|
+
const top = children[i].offsetTop;
|
|
107
|
+
if (top !== lastTop) {
|
|
108
|
+
grouped.push([i]);
|
|
109
|
+
lastTop = top;
|
|
110
|
+
} else {
|
|
111
|
+
grouped[grouped.length - 1].push(i);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
rows = grouped;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const totalPages = $derived(isHorizontal ? Math.max(1, Math.ceil(rows.length / maxRows)) : 1);
|
|
118
|
+
let page = $state(0);
|
|
119
|
+
$effect(() => {
|
|
120
|
+
if (page > totalPages - 1) page = Math.max(0, totalPages - 1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const showNav = $derived(isHorizontal && totalPages > 1);
|
|
124
|
+
|
|
125
|
+
const visibleItems = $derived.by((): { item: LegendItem; i: number }[] => {
|
|
126
|
+
const all = section.items.map((item, i) => ({ item, i }));
|
|
127
|
+
if (!isHorizontal || rows.length === 0) return all;
|
|
128
|
+
const start = page * maxRows;
|
|
129
|
+
const end = Math.min(rows.length, start + maxRows);
|
|
130
|
+
// `rows` entries are already index-ascending, so flattening the page's
|
|
131
|
+
// row slice preserves item order without needing a lookup set.
|
|
132
|
+
return rows
|
|
133
|
+
.slice(start, end)
|
|
134
|
+
.flat()
|
|
135
|
+
.map((i) => all[i]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
function prevPage(): void {
|
|
139
|
+
page = Math.max(0, page - 1);
|
|
140
|
+
}
|
|
141
|
+
function nextPage(): void {
|
|
142
|
+
page = Math.min(totalPages - 1, page + 1);
|
|
143
|
+
}
|
|
53
144
|
</script>
|
|
54
145
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
146
|
+
{#snippet discreteItem(item: LegendItem, i: number)}
|
|
147
|
+
{@const disabledStyle = interaction.disabledSeries.get(item.name)}
|
|
148
|
+
{@const color =
|
|
149
|
+
disabledStyle?.fill ??
|
|
150
|
+
disabledStyle?.stroke?.stroke ??
|
|
151
|
+
item.color ??
|
|
152
|
+
paletteColor(i, cfg.palette)}
|
|
153
|
+
{@const itemOpacity = disabledStyle
|
|
154
|
+
? (disabledStyle.opacity ?? cfg.legend.disabledOpacity)
|
|
155
|
+
: (item.opacity ?? 1)}
|
|
156
|
+
<div
|
|
157
|
+
class="dct-discrete__item"
|
|
158
|
+
class:dct-discrete__item--interactive={section.interactive}
|
|
159
|
+
style:opacity={itemOpacity}
|
|
160
|
+
{...section.interactive
|
|
161
|
+
? {
|
|
162
|
+
role: 'button',
|
|
163
|
+
tabindex: 0,
|
|
164
|
+
'aria-pressed': disabledStyle != null,
|
|
165
|
+
onclick: () => toggle(item),
|
|
166
|
+
onkeydown: (e: KeyboardEvent) => onKeydown(e, item)
|
|
167
|
+
}
|
|
168
|
+
: {}}
|
|
169
|
+
>
|
|
170
|
+
<svg class="dct-discrete__swatch" viewBox="0 0 15 10">
|
|
171
|
+
{#if item.symbol === 'circle'}
|
|
172
|
+
<circle cx="5" cy="5" r="4" fill={color} />
|
|
173
|
+
{:else if item.symbol === 'square'}
|
|
174
|
+
<rect x="1" y="1" width="8" height="8" rx="2" fill={color} />
|
|
175
|
+
{:else if item.symbol === 'triangle'}
|
|
176
|
+
<path
|
|
177
|
+
d="M 6 2 L 9 7.5 A 1.5 1.5 0 0 1 7.8 10 L 2.2 10 A 1.5 1.5 0 0 1 1 7.5 L 4 2 A 1.5 1.5 0 0 1 6 2"
|
|
178
|
+
fill={color}
|
|
179
|
+
transform="translate(0,-1)"
|
|
180
|
+
/>
|
|
181
|
+
{:else if item.symbol === 'line'}
|
|
182
|
+
<line
|
|
183
|
+
x1="2"
|
|
184
|
+
y1="5"
|
|
185
|
+
x2="13"
|
|
186
|
+
y2="5"
|
|
187
|
+
stroke={color}
|
|
188
|
+
stroke-width="3"
|
|
189
|
+
stroke-linecap="round"
|
|
190
|
+
stroke-dasharray={dashArray(item.lineStyle)}
|
|
191
|
+
/>
|
|
192
|
+
{:else}
|
|
193
|
+
<rect x="1" y="1" width="8" height="8" fill={color} />
|
|
194
|
+
{/if}
|
|
195
|
+
</svg>
|
|
196
|
+
<span class="dct-discrete__label">{item.label || item.name}</span>
|
|
197
|
+
</div>
|
|
198
|
+
{/snippet}
|
|
199
|
+
|
|
200
|
+
{#snippet navButtonSlot(
|
|
201
|
+
direction: LegendNavDirection,
|
|
202
|
+
disabled: boolean,
|
|
203
|
+
onclick: () => void,
|
|
204
|
+
label: string
|
|
205
|
+
)}
|
|
206
|
+
{#if section.navButton}
|
|
207
|
+
{@render section.navButton({ direction, disabled, onclick })}
|
|
208
|
+
{:else}
|
|
209
|
+
<button
|
|
210
|
+
type="button"
|
|
211
|
+
class="dct-discrete-paginated__btn"
|
|
212
|
+
{disabled}
|
|
213
|
+
{onclick}
|
|
214
|
+
aria-label={label}
|
|
215
|
+
>
|
|
216
|
+
{direction === 'prev' ? '‹' : '›'}
|
|
217
|
+
</button>
|
|
218
|
+
{/if}
|
|
219
|
+
{/snippet}
|
|
220
|
+
|
|
221
|
+
{#if isHorizontal}
|
|
222
|
+
<div class="dct-discrete-paginated">
|
|
223
|
+
{#if showNav}
|
|
224
|
+
{@render navButtonSlot('prev', page === 0, prevPage, 'Página anterior de referencias')}
|
|
225
|
+
{/if}
|
|
226
|
+
<div class="dct-discrete-horizontal" bind:clientWidth={containerWidth}>
|
|
227
|
+
<div class="dct-discrete dct-discrete--horizontal">
|
|
228
|
+
{#each visibleItems as { item, i } (item.name)}
|
|
229
|
+
{@render discreteItem(item, i)}
|
|
230
|
+
{/each}
|
|
231
|
+
</div>
|
|
232
|
+
<div
|
|
233
|
+
class="dct-discrete dct-discrete--horizontal dct-discrete--measure"
|
|
234
|
+
bind:this={measurerEl}
|
|
235
|
+
aria-hidden="true"
|
|
236
|
+
>
|
|
237
|
+
{#each section.items as item, i (item.name)}
|
|
238
|
+
{@render discreteItem(item, i)}
|
|
239
|
+
{/each}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
{#if showNav}
|
|
243
|
+
{@render navButtonSlot(
|
|
244
|
+
'next',
|
|
245
|
+
page >= totalPages - 1,
|
|
246
|
+
nextPage,
|
|
247
|
+
'Página siguiente de referencias'
|
|
248
|
+
)}
|
|
249
|
+
{/if}
|
|
250
|
+
</div>
|
|
251
|
+
{:else}
|
|
252
|
+
<div class="dct-discrete" style:grid-template-columns="repeat({columns}, max-content)">
|
|
253
|
+
{#each section.items as item, i (item.name)}
|
|
254
|
+
{@render discreteItem(item, i)}
|
|
255
|
+
{/each}
|
|
256
|
+
</div>
|
|
257
|
+
{/if}
|
|
104
258
|
|
|
105
259
|
<style>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
260
|
+
.dct-discrete {
|
|
261
|
+
display: grid;
|
|
262
|
+
column-gap: 1.25rem;
|
|
263
|
+
row-gap: 0.4rem;
|
|
264
|
+
max-width: 100%;
|
|
265
|
+
}
|
|
266
|
+
.dct-discrete--horizontal {
|
|
267
|
+
display: flex;
|
|
268
|
+
flex-wrap: wrap;
|
|
269
|
+
}
|
|
270
|
+
/* Items keep their natural (max-content) width and wrap to a new row
|
|
271
|
+
instead of shrinking/truncating — matching the grid variant, where
|
|
272
|
+
`grid-template-columns: repeat(n, max-content)` never shrinks a column
|
|
273
|
+
either. Without this, flex's default `flex-shrink: 1` would cram every
|
|
274
|
+
item onto one line by squeezing labels down via their own
|
|
275
|
+
`overflow: hidden` before ever wrapping. */
|
|
276
|
+
.dct-discrete--horizontal .dct-discrete__item {
|
|
277
|
+
flex-shrink: 0;
|
|
278
|
+
}
|
|
279
|
+
/* Flanks the rows with the prev/next buttons (when present) instead of
|
|
280
|
+
stacking them below — `align-items: center` keeps each button centered
|
|
281
|
+
against the full block of rows, not pinned to its first/last row. */
|
|
282
|
+
.dct-discrete-paginated {
|
|
283
|
+
display: flex;
|
|
284
|
+
align-items: center;
|
|
285
|
+
gap: var(--dct-legend-nav-gap, 0.35rem);
|
|
286
|
+
}
|
|
287
|
+
.dct-discrete-horizontal {
|
|
288
|
+
flex: 1 1 auto;
|
|
289
|
+
min-width: 0;
|
|
290
|
+
}
|
|
291
|
+
/* Full, untruncated copy of the list used only to measure row breaks — kept
|
|
292
|
+
in normal flow (see the long comment on the measurement effect for why
|
|
293
|
+
it must NOT be `position: absolute` here), with its height collapsed to
|
|
294
|
+
0 and clipped so it never adds any visible space of its own. Reading
|
|
295
|
+
`offsetTop` off its children still works: `overflow: hidden` only clips
|
|
296
|
+
what's *painted*, not each child's actual computed layout position. */
|
|
297
|
+
.dct-discrete--measure {
|
|
298
|
+
height: 0;
|
|
299
|
+
overflow: hidden;
|
|
300
|
+
visibility: hidden;
|
|
301
|
+
pointer-events: none;
|
|
302
|
+
}
|
|
303
|
+
.dct-discrete__item {
|
|
304
|
+
display: flex;
|
|
305
|
+
align-items: center;
|
|
306
|
+
gap: 0.35rem;
|
|
307
|
+
min-width: 0;
|
|
308
|
+
transition: opacity 0.15s ease;
|
|
309
|
+
}
|
|
310
|
+
.dct-discrete__item--interactive {
|
|
311
|
+
cursor: pointer;
|
|
312
|
+
}
|
|
313
|
+
.dct-discrete__item--interactive:focus-visible {
|
|
314
|
+
outline: 2px solid currentColor;
|
|
315
|
+
outline-offset: 2px;
|
|
316
|
+
}
|
|
317
|
+
.dct-discrete__swatch {
|
|
318
|
+
width: var(--dct-legend-swatch, 1.25rem);
|
|
319
|
+
height: var(--dct-legend-swatch, 1.25rem);
|
|
320
|
+
flex-shrink: 0;
|
|
321
|
+
}
|
|
322
|
+
.dct-discrete__label {
|
|
323
|
+
font-size: var(--dct-legend-size, 0.875rem);
|
|
324
|
+
font-weight: var(--dct-legend-weight, 400);
|
|
325
|
+
white-space: nowrap;
|
|
326
|
+
overflow: hidden;
|
|
327
|
+
text-overflow: ellipsis;
|
|
328
|
+
min-width: 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.dct-discrete-paginated__btn {
|
|
332
|
+
display: flex;
|
|
333
|
+
align-items: center;
|
|
334
|
+
justify-content: center;
|
|
335
|
+
flex-shrink: 0;
|
|
336
|
+
border: none;
|
|
337
|
+
border-radius: var(--dct-legend-nav-radius, 999px);
|
|
338
|
+
width: var(--dct-legend-nav-size, 1.5rem);
|
|
339
|
+
height: var(--dct-legend-nav-size, 1.5rem);
|
|
340
|
+
padding: 0;
|
|
341
|
+
font-size: var(--dct-legend-nav-font-size, 1rem);
|
|
342
|
+
line-height: 1;
|
|
343
|
+
background: var(--dct-legend-nav-bg, transparent);
|
|
344
|
+
color: var(--dct-legend-nav-color, currentColor);
|
|
345
|
+
cursor: pointer;
|
|
346
|
+
}
|
|
347
|
+
.dct-discrete-paginated__btn:hover:not(:disabled) {
|
|
348
|
+
background: var(--dct-legend-nav-hover-bg, color-mix(in srgb, currentColor 10%, transparent));
|
|
349
|
+
}
|
|
350
|
+
.dct-discrete-paginated__btn:disabled {
|
|
351
|
+
opacity: var(--dct-legend-nav-disabled-opacity, 0.3);
|
|
352
|
+
cursor: default;
|
|
353
|
+
}
|
|
139
354
|
</style>
|
|
@@ -39,6 +39,14 @@ export type ChartConfig = {
|
|
|
39
39
|
swatchSize: string;
|
|
40
40
|
/** Fallback opacity for a legend-disabled series/value when a section doesn't set its own `disabledStyle`. */
|
|
41
41
|
disabledOpacity: number;
|
|
42
|
+
/** Prev/next arrows shown when a `layout: { type: 'horizontal' }` discrete section overflows `maxRows`. */
|
|
43
|
+
navButton: {
|
|
44
|
+
background: string;
|
|
45
|
+
color: string;
|
|
46
|
+
size: string;
|
|
47
|
+
fontSize: string;
|
|
48
|
+
disabledOpacity: number;
|
|
49
|
+
};
|
|
42
50
|
};
|
|
43
51
|
axis: {
|
|
44
52
|
color: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
import type { SvelteMap } from 'svelte/reactivity';
|
|
2
3
|
import type { SymbolType, LineStyle } from '../plots/constants';
|
|
3
4
|
import type { StrokeStyle, DotStyle } from '../plots/styling';
|
|
@@ -26,15 +27,37 @@ export type LegendItem = {
|
|
|
26
27
|
/** Initial toggled-off state (uncontrolled — the user can still click it back on). */
|
|
27
28
|
disabled?: boolean;
|
|
28
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* How a discrete section's items flow. `'vertical'` (the default) stacks them
|
|
32
|
+
* in a `columns`-wide grid, as it always has. `'horizontal'` flows them left
|
|
33
|
+
* to right, wrapping to new rows as needed — and once wrapped rows exceed
|
|
34
|
+
* `maxRows` (default 1), the section paginates instead of growing taller,
|
|
35
|
+
* surfacing prev/next arrows to page through the rest.
|
|
36
|
+
*/
|
|
37
|
+
export type LegendItemsLayout = {
|
|
38
|
+
type: 'vertical';
|
|
39
|
+
} | {
|
|
40
|
+
type: 'horizontal';
|
|
41
|
+
maxRows?: number;
|
|
42
|
+
};
|
|
43
|
+
export type LegendNavDirection = 'prev' | 'next';
|
|
44
|
+
export type LegendNavContext = {
|
|
45
|
+
direction: LegendNavDirection;
|
|
46
|
+
disabled: boolean;
|
|
47
|
+
onclick: () => void;
|
|
48
|
+
};
|
|
49
|
+
/** Custom prev/next button renderer for a paginated horizontal section. Falls back to a small default arrow button. */
|
|
50
|
+
export type LegendNavSnippet = Snippet<[LegendNavContext]>;
|
|
29
51
|
export type DiscreteLegendSection = {
|
|
30
52
|
type: 'discrete';
|
|
31
53
|
title?: string;
|
|
32
54
|
items: LegendItem[];
|
|
33
55
|
columns?: number;
|
|
34
|
-
/**
|
|
56
|
+
/** Item flow direction. Defaults to `{ type: 'vertical' }` (unchanged prior behaviour). */
|
|
57
|
+
layout?: LegendItemsLayout;
|
|
35
58
|
interactive?: boolean;
|
|
36
|
-
/** Style applied to a toggled-off item's series. Falls back to `cfg.legend.disabledOpacity`. */
|
|
37
59
|
disabledStyle?: LegendDisabledStyle;
|
|
60
|
+
navButton?: LegendNavSnippet;
|
|
38
61
|
};
|
|
39
62
|
export type ContinuousLegendSection = {
|
|
40
63
|
type: 'continuous';
|
|
@@ -47,7 +70,6 @@ export type ContinuousLegendSection = {
|
|
|
47
70
|
ticks?: number[];
|
|
48
71
|
width?: number;
|
|
49
72
|
format?: (v: number) => string;
|
|
50
|
-
/** Enables drag handles on the bar's edges to narrow the visible range. */
|
|
51
73
|
interactive?: boolean;
|
|
52
74
|
/**
|
|
53
75
|
* How the color ramp responds to a narrowed range: `'rescale'` reprojects
|
|
@@ -55,7 +77,6 @@ export type ContinuousLegendSection = {
|
|
|
55
77
|
* excludes out-of-range values. Defaults to `'filter'`.
|
|
56
78
|
*/
|
|
57
79
|
mode?: 'rescale' | 'filter';
|
|
58
|
-
/** Style applied to values outside the active range. Falls back to `cfg.legend.disabledOpacity`. */
|
|
59
80
|
disabledStyle?: LegendDisabledStyle;
|
|
60
81
|
};
|
|
61
82
|
export type LegendSection = DiscreteLegendSection | ContinuousLegendSection;
|