@fundar/data-chart-telling 0.0.26 → 0.0.28
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/charts/BaseChart.svelte +18 -10
- package/dist/charts/BaseChart.svelte.d.ts +4 -2
- package/dist/charts/heatmap/Chart.svelte +2 -2
- package/dist/charts/pyramid/Chart.svelte +2 -2
- package/dist/configuration/config.svelte.js +26 -0
- package/dist/configuration/themes/dark.js +4 -0
- package/dist/configuration/themes/index.d.ts +64 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +2 -0
- package/dist/layout/coordinates/CoordinatesLayout.svelte +1 -1
- package/dist/layout/facet/FacetIndicator.svelte +31 -0
- package/dist/layout/facet/FacetIndicator.svelte.d.ts +8 -0
- package/dist/layout/facet/FacetLayout.svelte +249 -24
- package/dist/layout/facet/FacetLayout.svelte.d.ts +7 -1
- package/dist/layout/legend/ContinuousSection.svelte +1 -1
- package/dist/layout/legend/ContinuousSection.svelte.d.ts +1 -1
- package/dist/layout/legend/DiscreteSection.svelte +1 -1
- package/dist/layout/legend/DiscreteSection.svelte.d.ts +1 -1
- package/dist/layout/legend/LegendLayout.svelte +1 -1
- package/dist/layout/legend/LegendLayout.svelte.d.ts +1 -1
- package/dist/layout/legend/interaction.svelte.d.ts +9 -1
- package/dist/layout/legend/interaction.svelte.js +9 -1
- package/dist/layout/timeline/Timeline.svelte +1 -1
- package/dist/layout/timeline/Timeline.svelte.d.ts +1 -1
- package/dist/layout/timeline/TimelineLayout.svelte +1 -1
- package/dist/layout/timeline/TimelineLayout.svelte.d.ts +1 -1
- package/dist/layout/tooltip/controller.svelte.js +16 -2
- package/dist/markers/HoverMarker.svelte +2 -2
- package/dist/plots/bar/Plot.svelte +43 -4
- package/dist/plots/bar/ValueLabels.svelte +69 -23
- package/dist/plots/bar/ValueLabels.svelte.d.ts +10 -1
- package/dist/plots/heatmap/Plot.svelte +4 -4
- package/dist/plots/heatmap/ValueLabels.svelte +3 -3
- package/dist/plots/heatmap/ValueLabels.svelte.d.ts +1 -1
- package/dist/plots/line/Plot.svelte +34 -29
- package/dist/plots/line/ValueLabels.svelte +73 -51
- package/dist/plots/line/ValueLabels.svelte.d.ts +8 -4
- package/dist/plots/pyramid/Plot.svelte +41 -6
- package/dist/plots/pyramid/ValueLabels.svelte +70 -24
- package/dist/plots/pyramid/ValueLabels.svelte.d.ts +10 -1
- package/dist/plots/scatter/Plot.svelte +40 -4
- package/dist/plots/scatter/ValueLabels.svelte +90 -47
- package/dist/plots/scatter/ValueLabels.svelte.d.ts +14 -5
- package/dist/plots/utils/labelOverflow.svelte.d.ts +51 -0
- package/dist/plots/utils/labelOverflow.svelte.js +116 -0
- package/dist/plots/utils/legendDisabled.d.ts +1 -1
- package/dist/types/charts/common.d.ts +3 -50
- package/dist/types/charts/props.d.ts +3 -2
- package/dist/types/configuration/styling.d.ts +19 -0
- package/dist/types/layout/facet.d.ts +41 -0
- package/dist/types/layout/legend.d.ts +65 -1
- package/dist/types/layout/styles.d.ts +1 -12
- package/dist/types/layout/timeline.d.ts +32 -0
- package/dist/types/layout/timeline.js +1 -0
- package/dist/utils/interpolate.d.ts +1 -1
- package/package.json +1 -1
- package/dist/types/charts/legend.d.ts +0 -60
- /package/dist/types/{charts/legend.js → layout/facet.js} +0 -0
|
@@ -7,14 +7,11 @@
|
|
|
7
7
|
import { provideHover } from '../layout/tooltip/hover.svelte';
|
|
8
8
|
import { provideLegendInteraction } from '../layout/legend/interaction.svelte';
|
|
9
9
|
import type { Snippet } from 'svelte';
|
|
10
|
-
import type { LegendSection } from '../types/
|
|
10
|
+
import type { LegendSection } from '../types/layout/legend';
|
|
11
11
|
import type { HoverStrategy } from '../types/layout/tooltip';
|
|
12
|
-
import type {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
TimeValue,
|
|
16
|
-
TimelineConfig,
|
|
17
|
-
} from '../types/charts/common';
|
|
12
|
+
import type { ChartPlotSnippet } from '../types/charts/common';
|
|
13
|
+
import type { FacetConfig } from '../types/layout/facet';
|
|
14
|
+
import type { TimeValue, TimelineConfig } from '../types/layout/timeline';
|
|
18
15
|
|
|
19
16
|
/**
|
|
20
17
|
* The orchestrator. It wires together the four interchangeable layout
|
|
@@ -60,7 +57,11 @@
|
|
|
60
57
|
const cfg = $derived(getConfiguration());
|
|
61
58
|
|
|
62
59
|
const hoverStore = provideHover();
|
|
63
|
-
provideLegendInteraction();
|
|
60
|
+
const legendInteraction = provideLegendInteraction();
|
|
61
|
+
const legendPlotContext = $derived({
|
|
62
|
+
disabledSeries: new Set(legendInteraction.disabledSeries.keys()),
|
|
63
|
+
isDisabled: (name: string) => legendInteraction.disabledSeries.has(name),
|
|
64
|
+
});
|
|
64
65
|
$effect(() => {
|
|
65
66
|
hoverStore.strategy = hover?.strategy ?? 'x';
|
|
66
67
|
hoverStore.sync = hover?.sync ?? false;
|
|
@@ -75,6 +76,12 @@
|
|
|
75
76
|
columns={facet.columns}
|
|
76
77
|
values={facet.values}
|
|
77
78
|
gap={facet.gap}
|
|
79
|
+
mode={facet.mode}
|
|
80
|
+
visible={facet.visible}
|
|
81
|
+
pageSize={facet.pageSize}
|
|
82
|
+
formatIndicator={facet.formatIndicator}
|
|
83
|
+
indicator={facet.indicator}
|
|
84
|
+
navButton={facet.navButton}
|
|
78
85
|
width={w}
|
|
79
86
|
height={h}
|
|
80
87
|
>
|
|
@@ -84,12 +91,13 @@
|
|
|
84
91
|
width: cw,
|
|
85
92
|
height: ch,
|
|
86
93
|
facetValue,
|
|
87
|
-
selectedValue
|
|
94
|
+
selectedValue,
|
|
95
|
+
legend: legendPlotContext
|
|
88
96
|
})}
|
|
89
97
|
{/snippet}
|
|
90
98
|
</FacetLayout>
|
|
91
99
|
{:else}
|
|
92
|
-
{@render plot({ data: rows, width: w, height: h, selectedValue })}
|
|
100
|
+
{@render plot({ data: rows, width: w, height: h, selectedValue, legend: legendPlotContext })}
|
|
93
101
|
{/if}
|
|
94
102
|
{/snippet}
|
|
95
103
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import type { LegendSection } from '../types/
|
|
2
|
+
import type { LegendSection } from '../types/layout/legend';
|
|
3
3
|
import type { HoverStrategy } from '../types/layout/tooltip';
|
|
4
|
-
import type { ChartPlotSnippet
|
|
4
|
+
import type { ChartPlotSnippet } from '../types/charts/common';
|
|
5
|
+
import type { FacetConfig } from '../types/layout/facet';
|
|
6
|
+
import type { TimelineConfig } from '../types/layout/timeline';
|
|
5
7
|
declare function $$render<TRow extends Record<string, unknown>>(): {
|
|
6
8
|
props: {
|
|
7
9
|
width: number;
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
const getX = $derived(resolveAccessor<TRow>(x));
|
|
47
47
|
const getY = $derived(resolveAccessor<TRow>(y));
|
|
48
48
|
const getZ = $derived(resolveAccessor<TRow>(z));
|
|
49
|
-
const
|
|
49
|
+
const formatValue = $derived(styles.values?.format ?? ((v: number) => `${v}`));
|
|
50
50
|
|
|
51
51
|
// Compute domain over the full dataset so colour is comparable across facets/timeline steps.
|
|
52
52
|
// Caller-provided `scales.z.domain` always takes precedence.
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
const defaultFormat = $derived((row: TRow) => {
|
|
67
67
|
const xl = scales.x?.label || 'x';
|
|
68
68
|
const yl = scales.y?.label || 'y';
|
|
69
|
-
return `${xl} ${getX(row)} · ${yl} ${getY(row)}\n${
|
|
69
|
+
return `${xl} ${getX(row)} · ${yl} ${getY(row)}\n${formatValue(Number(getZ(row)), '')}`;
|
|
70
70
|
});
|
|
71
71
|
</script>
|
|
72
72
|
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
const getMagnitude = $derived(resolveAccessor<TRow>(magnitude));
|
|
49
49
|
const getCategory = $derived(resolveAccessor<TRow>(category));
|
|
50
50
|
const getSides = $derived(resolveAccessor<TRow>(sides));
|
|
51
|
-
const
|
|
51
|
+
const formatValue = $derived(styles.values?.format ?? ((v: number) => `${v}`));
|
|
52
52
|
|
|
53
53
|
// Computed once over the full dataset (not the per-facet/timeline-filtered
|
|
54
54
|
// rows) so the diverging axis and category bands stay comparable across
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
const resolvedStrategy = $derived(resolveHoverStrategy(tip.enabled ? tip.strategy : undefined, markers, 'y'));
|
|
87
87
|
const resolvedSync = $derived(resolveHoverSync(tip.enabled ? tip.sync : undefined, markers));
|
|
88
88
|
const defaultFormat = $derived(
|
|
89
|
-
(row: TRow) => `${String(getSides(row))}\n${String(getCategory(row))}: ${
|
|
89
|
+
(row: TRow) => `${String(getSides(row))}\n${String(getCategory(row))}: ${formatValue(Number(getMagnitude(row)), String(getSides(row)))}`,
|
|
90
90
|
);
|
|
91
91
|
</script>
|
|
92
92
|
|
|
@@ -89,6 +89,22 @@ export const DEFAULT_CONFIG = {
|
|
|
89
89
|
font: { fill: 'currentColor', fontSize: 12, dy: -18 },
|
|
90
90
|
area: { stroke: '#e6580d', strokeWidth: 2, fillOpacity: 0 },
|
|
91
91
|
},
|
|
92
|
+
facet: {
|
|
93
|
+
indicator: {
|
|
94
|
+
background: '#111827',
|
|
95
|
+
color: '#ffffff',
|
|
96
|
+
fontSize: '0.8125rem',
|
|
97
|
+
padding: '0.125rem 0.5rem',
|
|
98
|
+
radius: '999px',
|
|
99
|
+
},
|
|
100
|
+
navButton: {
|
|
101
|
+
background: '#111827',
|
|
102
|
+
color: '#ffffff',
|
|
103
|
+
size: '2rem',
|
|
104
|
+
fontSize: '1.1rem',
|
|
105
|
+
disabledOpacity: 0.3,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
92
108
|
};
|
|
93
109
|
// ── Merge ─────────────────────────────────────────────────────────────────────
|
|
94
110
|
function isPlainObject(v) {
|
|
@@ -162,6 +178,16 @@ export function configToCssVars(c) {
|
|
|
162
178
|
'--dct-heat-min': c.continuous.min,
|
|
163
179
|
'--dct-heat-max': c.continuous.max,
|
|
164
180
|
'--dct-heat-text': c.continuous.labelColor,
|
|
181
|
+
'--dct-facet-indicator-bg': c.facet.indicator.background,
|
|
182
|
+
'--dct-facet-indicator-color': c.facet.indicator.color,
|
|
183
|
+
'--dct-facet-indicator-size': c.facet.indicator.fontSize,
|
|
184
|
+
'--dct-facet-indicator-pad': c.facet.indicator.padding,
|
|
185
|
+
'--dct-facet-indicator-radius': c.facet.indicator.radius,
|
|
186
|
+
'--dct-facet-nav-bg': c.facet.navButton.background,
|
|
187
|
+
'--dct-facet-nav-color': c.facet.navButton.color,
|
|
188
|
+
'--dct-facet-nav-btn-size': c.facet.navButton.size,
|
|
189
|
+
'--dct-facet-nav-font-size': c.facet.navButton.fontSize,
|
|
190
|
+
'--dct-facet-nav-disabled-opacity': String(c.facet.navButton.disabledOpacity),
|
|
165
191
|
};
|
|
166
192
|
}
|
|
167
193
|
/** Serialize a CSS-vars record into a `style` attribute string. */
|
|
@@ -11,6 +11,10 @@ export const DARK_THEME = {
|
|
|
11
11
|
continuous: { min: '#1e293b', max: '#38bdf8', labelColor: '#e5e7eb' },
|
|
12
12
|
axis: { color: '#94a3b8', fontSize: 13, xTickSize: 6, yTickSize: 6 },
|
|
13
13
|
tooltip: { background: '#e5e7eb', color: '#0f172a' },
|
|
14
|
+
facet: {
|
|
15
|
+
indicator: { background: '#e5e7eb', color: '#0f172a' },
|
|
16
|
+
navButton: { background: '#e5e7eb', color: '#0f172a' },
|
|
17
|
+
},
|
|
14
18
|
line: { strokeWidth: 3, dotRadius: 4 },
|
|
15
19
|
timeline: { activeColor: '#38bdf8', axisColor: '#94a3b8', thickness: 60 },
|
|
16
20
|
hover: { rule: { stroke: '#38bdf8' } },
|
|
@@ -146,6 +146,22 @@ export declare const THEMES: {
|
|
|
146
146
|
borderRadius?: number | undefined;
|
|
147
147
|
} | undefined;
|
|
148
148
|
} | undefined;
|
|
149
|
+
facet?: {
|
|
150
|
+
indicator?: {
|
|
151
|
+
background?: string | undefined;
|
|
152
|
+
color?: string | undefined;
|
|
153
|
+
fontSize?: string | undefined;
|
|
154
|
+
padding?: string | undefined;
|
|
155
|
+
radius?: string | undefined;
|
|
156
|
+
} | undefined;
|
|
157
|
+
navButton?: {
|
|
158
|
+
background?: string | undefined;
|
|
159
|
+
color?: string | undefined;
|
|
160
|
+
size?: string | undefined;
|
|
161
|
+
fontSize?: string | undefined;
|
|
162
|
+
disabledOpacity?: number | undefined;
|
|
163
|
+
} | undefined;
|
|
164
|
+
} | undefined;
|
|
149
165
|
};
|
|
150
166
|
Editorial: {
|
|
151
167
|
font?: string | undefined;
|
|
@@ -288,6 +304,22 @@ export declare const THEMES: {
|
|
|
288
304
|
borderRadius?: number | undefined;
|
|
289
305
|
} | undefined;
|
|
290
306
|
} | undefined;
|
|
307
|
+
facet?: {
|
|
308
|
+
indicator?: {
|
|
309
|
+
background?: string | undefined;
|
|
310
|
+
color?: string | undefined;
|
|
311
|
+
fontSize?: string | undefined;
|
|
312
|
+
padding?: string | undefined;
|
|
313
|
+
radius?: string | undefined;
|
|
314
|
+
} | undefined;
|
|
315
|
+
navButton?: {
|
|
316
|
+
background?: string | undefined;
|
|
317
|
+
color?: string | undefined;
|
|
318
|
+
size?: string | undefined;
|
|
319
|
+
fontSize?: string | undefined;
|
|
320
|
+
disabledOpacity?: number | undefined;
|
|
321
|
+
} | undefined;
|
|
322
|
+
} | undefined;
|
|
291
323
|
};
|
|
292
324
|
Dark: {
|
|
293
325
|
font?: string | undefined;
|
|
@@ -430,6 +462,22 @@ export declare const THEMES: {
|
|
|
430
462
|
borderRadius?: number | undefined;
|
|
431
463
|
} | undefined;
|
|
432
464
|
} | undefined;
|
|
465
|
+
facet?: {
|
|
466
|
+
indicator?: {
|
|
467
|
+
background?: string | undefined;
|
|
468
|
+
color?: string | undefined;
|
|
469
|
+
fontSize?: string | undefined;
|
|
470
|
+
padding?: string | undefined;
|
|
471
|
+
radius?: string | undefined;
|
|
472
|
+
} | undefined;
|
|
473
|
+
navButton?: {
|
|
474
|
+
background?: string | undefined;
|
|
475
|
+
color?: string | undefined;
|
|
476
|
+
size?: string | undefined;
|
|
477
|
+
fontSize?: string | undefined;
|
|
478
|
+
disabledOpacity?: number | undefined;
|
|
479
|
+
} | undefined;
|
|
480
|
+
} | undefined;
|
|
433
481
|
};
|
|
434
482
|
Compact: {
|
|
435
483
|
font?: string | undefined;
|
|
@@ -572,6 +620,22 @@ export declare const THEMES: {
|
|
|
572
620
|
borderRadius?: number | undefined;
|
|
573
621
|
} | undefined;
|
|
574
622
|
} | undefined;
|
|
623
|
+
facet?: {
|
|
624
|
+
indicator?: {
|
|
625
|
+
background?: string | undefined;
|
|
626
|
+
color?: string | undefined;
|
|
627
|
+
fontSize?: string | undefined;
|
|
628
|
+
padding?: string | undefined;
|
|
629
|
+
radius?: string | undefined;
|
|
630
|
+
} | undefined;
|
|
631
|
+
navButton?: {
|
|
632
|
+
background?: string | undefined;
|
|
633
|
+
color?: string | undefined;
|
|
634
|
+
size?: string | undefined;
|
|
635
|
+
fontSize?: string | undefined;
|
|
636
|
+
disabledOpacity?: number | undefined;
|
|
637
|
+
} | undefined;
|
|
638
|
+
} | undefined;
|
|
575
639
|
};
|
|
576
640
|
};
|
|
577
641
|
export type ThemeName = keyof typeof THEMES;
|
package/dist/index.d.ts
CHANGED
|
@@ -18,8 +18,10 @@ export { argentinaBicontinental } from './layout/coordinates/projections/argenti
|
|
|
18
18
|
export { default as Timeline } from './layout/timeline/Timeline.svelte';
|
|
19
19
|
export { default as TimelineLayout } from './layout/timeline/TimelineLayout.svelte';
|
|
20
20
|
export { default as FacetLayout } from './layout/facet/FacetLayout.svelte';
|
|
21
|
+
export { default as FacetIndicator } from './layout/facet/FacetIndicator.svelte';
|
|
21
22
|
export { default as LegendLayout } from './layout/legend/LegendLayout.svelte';
|
|
22
23
|
export { default as Tooltip } from './layout/tooltip/Tooltip.svelte';
|
|
24
|
+
export { getLegendInteraction, provideLegendInteraction, createLegendInteraction, } from './layout/legend/interaction.svelte';
|
|
23
25
|
export { default as BarMarker } from './markers/BarMarker.svelte';
|
|
24
26
|
export { default as CellMarker } from './markers/CellMarker.svelte';
|
|
25
27
|
export { default as DeltaMarker } from './markers/DeltaMarker.svelte';
|
|
@@ -49,8 +51,10 @@ export type { GeoFeature, GeoJsonInput, GeoDataProps, Topology } from './types/p
|
|
|
49
51
|
export type { ScatterDataProps } from './types/plots/data/scatter';
|
|
50
52
|
export type { GeoProjectionName, GeoProjectionConfig, GeoCustomProjection, GeoCustomProjectionFactory, GeoScalesConfig, GeoZoomConfig, CoordinatesConfig, } from './types/layout/coordinates';
|
|
51
53
|
export type { GeoMarkersConfig, GeoInsetMarkerConfig, GeoInsetTooltipConfig, GeoInsetLocation, GeoInsetProjectionName, GeoInsetProjectionConfig, } from './types/markers/geo';
|
|
52
|
-
export type {
|
|
53
|
-
export type {
|
|
54
|
+
export type { ChartPlotContext, ChartPlotSnippet, } from './types/charts/common';
|
|
55
|
+
export type { TimeValue, Orientation, TimelineConfig, } from './types/layout/timeline';
|
|
56
|
+
export type { Responsive, FacetColumns, FacetMode, FacetConfig, FacetIndicatorContext, FacetIndicatorSnippet, FacetNavDirection, FacetNavContext, FacetNavSnippet, } from './types/layout/facet';
|
|
57
|
+
export type { LegendSection, DiscreteLegendSection, ContinuousLegendSection, LegendItem, LegendDisabledStyle, LegendInteractionStore, LegendPlotContext, } from './types/layout/legend';
|
|
54
58
|
export type { HoverStrategy, TooltipOptions, TooltipProp, TooltipAnchorX, TooltipAnchorY, } from './types/layout/tooltip';
|
|
55
59
|
export type { ChartConfig, ChartConfigInput, TextStyle } from './types/configuration/styling';
|
|
56
60
|
export type { ChartProps } from './types/charts/props';
|
package/dist/index.js
CHANGED
|
@@ -24,8 +24,10 @@ export { argentinaBicontinental } from './layout/coordinates/projections/argenti
|
|
|
24
24
|
export { default as Timeline } from './layout/timeline/Timeline.svelte';
|
|
25
25
|
export { default as TimelineLayout } from './layout/timeline/TimelineLayout.svelte';
|
|
26
26
|
export { default as FacetLayout } from './layout/facet/FacetLayout.svelte';
|
|
27
|
+
export { default as FacetIndicator } from './layout/facet/FacetIndicator.svelte';
|
|
27
28
|
export { default as LegendLayout } from './layout/legend/LegendLayout.svelte';
|
|
28
29
|
export { default as Tooltip } from './layout/tooltip/Tooltip.svelte';
|
|
30
|
+
export { getLegendInteraction, provideLegendInteraction, createLegendInteraction, } from './layout/legend/interaction.svelte';
|
|
29
31
|
// Markers
|
|
30
32
|
export { default as BarMarker } from './markers/BarMarker.svelte';
|
|
31
33
|
export { default as CellMarker } from './markers/CellMarker.svelte';
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
} = $props();
|
|
43
43
|
|
|
44
44
|
const resolvedProjection = $derived(
|
|
45
|
-
coordinates
|
|
45
|
+
coordinates ? resolveProjection(coordinates.projection, data) : undefined,
|
|
46
46
|
);
|
|
47
47
|
|
|
48
48
|
const zoomEnabled = $derived(coordinates?.zoom !== false && coordinates?.zoom != null);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Default position badge for `carousel`/`paginated` facet modes. Styled from
|
|
4
|
+
* the `--dct-facet-indicator-*` config vars; positioning is owned by the
|
|
5
|
+
* parent that renders it.
|
|
6
|
+
*/
|
|
7
|
+
let {
|
|
8
|
+
current,
|
|
9
|
+
total,
|
|
10
|
+
format,
|
|
11
|
+
}: {
|
|
12
|
+
current: number;
|
|
13
|
+
total: number;
|
|
14
|
+
format?: (current: number, total: number) => string;
|
|
15
|
+
} = $props();
|
|
16
|
+
|
|
17
|
+
const text = $derived(format ? format(current, total) : `${current} / ${total}`);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div class="dct-facet-indicator">{text}</div>
|
|
21
|
+
|
|
22
|
+
<style>
|
|
23
|
+
.dct-facet-indicator {
|
|
24
|
+
padding: var(--dct-facet-indicator-pad, 0.125rem 0.5rem);
|
|
25
|
+
border-radius: var(--dct-facet-indicator-radius, 999px);
|
|
26
|
+
font-size: var(--dct-facet-indicator-size, 0.8125rem);
|
|
27
|
+
background: var(--dct-facet-indicator-bg, #111827);
|
|
28
|
+
color: var(--dct-facet-indicator-color, #fff);
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
}
|
|
31
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
current: number;
|
|
3
|
+
total: number;
|
|
4
|
+
format?: (current: number, total: number) => string;
|
|
5
|
+
};
|
|
6
|
+
declare const FacetIndicator: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type FacetIndicator = ReturnType<typeof FacetIndicator>;
|
|
8
|
+
export default FacetIndicator;
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
<script lang="ts" generics="TRow extends Record<string, unknown>">
|
|
2
2
|
import { isMobile } from '../../stores/device';
|
|
3
3
|
import { groupBy } from '../../utils/grouping';
|
|
4
|
+
import FacetIndicator from './FacetIndicator.svelte';
|
|
4
5
|
import type {
|
|
5
6
|
FacetColumns,
|
|
7
|
+
FacetMode,
|
|
8
|
+
Responsive,
|
|
6
9
|
FacetCellSnippet,
|
|
7
|
-
|
|
10
|
+
FacetIndicatorSnippet,
|
|
11
|
+
FacetNavSnippet,
|
|
12
|
+
FacetNavDirection,
|
|
13
|
+
} from '../../types/layout/facet';
|
|
8
14
|
import type { Accessor } from '../../types/plots/data/common';
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
|
-
* Small-multiples layout. It splits the data by `by
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
17
|
+
* Small-multiples layout. It splits the data by `by` and hands each group to
|
|
18
|
+
* the `cell` snippet with its own measured box. Like the other primitives it
|
|
19
|
+
* never draws the plot itself — any plot that accepts `{ data, width, height }`
|
|
20
|
+
* can be faceted.
|
|
21
|
+
*
|
|
22
|
+
* `mode` picks how the groups are arranged: `'grid'` (default) lays them all
|
|
23
|
+
* out at once; `'paginated'` pages through the same grid in fixed chunks;
|
|
24
|
+
* `'carousel'` shows `visible` groups at a time with touch-drag/snap.
|
|
15
25
|
*/
|
|
16
26
|
let {
|
|
17
27
|
data,
|
|
@@ -19,6 +29,12 @@
|
|
|
19
29
|
columns = { desktop: 3, mobile: 1 },
|
|
20
30
|
values,
|
|
21
31
|
gap = 12,
|
|
32
|
+
mode = 'grid',
|
|
33
|
+
visible = 1,
|
|
34
|
+
pageSize,
|
|
35
|
+
formatIndicator,
|
|
36
|
+
indicator,
|
|
37
|
+
navButton,
|
|
22
38
|
width,
|
|
23
39
|
height,
|
|
24
40
|
cell,
|
|
@@ -28,11 +44,24 @@
|
|
|
28
44
|
columns?: FacetColumns;
|
|
29
45
|
values?: string[];
|
|
30
46
|
gap?: number;
|
|
47
|
+
mode?: Responsive<FacetMode>;
|
|
48
|
+
visible?: number;
|
|
49
|
+
pageSize?: number;
|
|
50
|
+
formatIndicator?: (current: number, total: number) => string;
|
|
51
|
+
indicator?: FacetIndicatorSnippet;
|
|
52
|
+
navButton?: FacetNavSnippet;
|
|
31
53
|
width: number;
|
|
32
54
|
height: number;
|
|
33
55
|
cell: FacetCellSnippet<TRow>;
|
|
34
56
|
} = $props();
|
|
35
57
|
|
|
58
|
+
function resolveResponsive<T>(value: Responsive<T>, mobile: boolean): T {
|
|
59
|
+
if (typeof value === 'object' && value !== null && 'desktop' in value) {
|
|
60
|
+
return mobile ? (value as { desktop: T; mobile: T }).mobile : (value as { desktop: T; mobile: T }).desktop;
|
|
61
|
+
}
|
|
62
|
+
return value as T;
|
|
63
|
+
}
|
|
64
|
+
|
|
36
65
|
const groups = $derived(groupBy(data, by));
|
|
37
66
|
|
|
38
67
|
const entries = $derived.by((): [string, TRow[]][] => {
|
|
@@ -42,13 +71,10 @@
|
|
|
42
71
|
.map((v) => [v, groups.get(v)!] as [string, TRow[]]);
|
|
43
72
|
});
|
|
44
73
|
|
|
74
|
+
const resolvedMode = $derived(resolveResponsive(mode, $isMobile));
|
|
75
|
+
|
|
45
76
|
const cols = $derived.by(() => {
|
|
46
|
-
const desired =
|
|
47
|
-
typeof columns === 'number'
|
|
48
|
-
? columns
|
|
49
|
-
: $isMobile
|
|
50
|
-
? columns.mobile
|
|
51
|
-
: columns.desktop;
|
|
77
|
+
const desired = resolveResponsive(columns, $isMobile);
|
|
52
78
|
return Math.max(1, Math.min(desired, entries.length || 1));
|
|
53
79
|
});
|
|
54
80
|
|
|
@@ -58,22 +84,164 @@
|
|
|
58
84
|
const cellHeight = $derived(
|
|
59
85
|
Math.max(0, Math.floor((height - (rows - 1) * gap) / rows) - TITLE_H),
|
|
60
86
|
);
|
|
87
|
+
|
|
88
|
+
// ── paginated ────────────────────────────────────────────────────────────
|
|
89
|
+
const resolvedPageSize = $derived(Math.max(1, pageSize ?? cols * rows));
|
|
90
|
+
let page = $state(0);
|
|
91
|
+
const totalPages = $derived(Math.max(1, Math.ceil(entries.length / resolvedPageSize)));
|
|
92
|
+
const pageEntries = $derived(
|
|
93
|
+
entries.slice(page * resolvedPageSize, page * resolvedPageSize + resolvedPageSize),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
$effect(() => {
|
|
97
|
+
if (page > totalPages - 1) page = Math.max(0, totalPages - 1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
function prevPage(): void {
|
|
101
|
+
page = Math.max(0, page - 1);
|
|
102
|
+
}
|
|
103
|
+
function nextPage(): void {
|
|
104
|
+
page = Math.min(totalPages - 1, page + 1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── carousel ─────────────────────────────────────────────────────────────
|
|
108
|
+
const visibleCount = $derived(Math.max(1, Math.min(visible, entries.length || 1)));
|
|
109
|
+
const carouselCellWidth = $derived(
|
|
110
|
+
Math.max(0, Math.floor((width - (visibleCount - 1) * gap) / visibleCount)),
|
|
111
|
+
);
|
|
112
|
+
const carouselCellHeight = $derived(Math.max(0, height - TITLE_H));
|
|
113
|
+
const maxIndex = $derived(Math.max(0, entries.length - visibleCount));
|
|
114
|
+
const stepWidth = $derived(carouselCellWidth + gap);
|
|
115
|
+
|
|
116
|
+
let activeIndex = $state(0);
|
|
117
|
+
let dragOffset = $state(0);
|
|
118
|
+
let dragging = $state(false);
|
|
119
|
+
let dragStartX = 0;
|
|
120
|
+
|
|
121
|
+
const translate = $derived(-activeIndex * stepWidth + dragOffset);
|
|
122
|
+
|
|
123
|
+
$effect(() => {
|
|
124
|
+
if (activeIndex > maxIndex) activeIndex = maxIndex;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Reset browsing position whenever the faceted field or its value set changes.
|
|
128
|
+
$effect(() => {
|
|
129
|
+
void by;
|
|
130
|
+
void values;
|
|
131
|
+
page = 0;
|
|
132
|
+
activeIndex = 0;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
function handleTouchStart(event: TouchEvent): void {
|
|
136
|
+
dragging = true;
|
|
137
|
+
dragStartX = event.touches[0].clientX;
|
|
138
|
+
dragOffset = 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function handleTouchMove(event: TouchEvent): void {
|
|
142
|
+
if (!dragging) return;
|
|
143
|
+
dragOffset = event.touches[0].clientX - dragStartX;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function handleTouchEnd(): void {
|
|
147
|
+
if (!dragging) return;
|
|
148
|
+
dragging = false;
|
|
149
|
+
const stepsMoved = Math.round(-dragOffset / stepWidth);
|
|
150
|
+
activeIndex = Math.max(0, Math.min(maxIndex, activeIndex + stepsMoved));
|
|
151
|
+
dragOffset = 0;
|
|
152
|
+
}
|
|
61
153
|
</script>
|
|
62
154
|
|
|
63
|
-
|
|
64
|
-
class="dct-facet"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
155
|
+
{#snippet indicatorSlot(current: number, total: number)}
|
|
156
|
+
<div class="dct-facet-indicator-slot">
|
|
157
|
+
{#if indicator}
|
|
158
|
+
{@render indicator({ current, total })}
|
|
159
|
+
{:else}
|
|
160
|
+
<FacetIndicator {current} {total} format={formatIndicator} />
|
|
161
|
+
{/if}
|
|
162
|
+
</div>
|
|
163
|
+
{/snippet}
|
|
164
|
+
|
|
165
|
+
{#snippet navButtons(direction: FacetNavDirection, disabled: boolean, onclick: () => void, label: string, glyph: string)}
|
|
166
|
+
<span class="dct-facet-nav-slot">
|
|
167
|
+
{#if navButton}
|
|
168
|
+
{@render navButton({ direction, disabled, onclick })}
|
|
169
|
+
{:else}
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
class="dct-facet-paginated__btn"
|
|
173
|
+
{disabled}
|
|
174
|
+
{onclick}
|
|
175
|
+
aria-label={label}
|
|
176
|
+
>
|
|
177
|
+
{glyph}
|
|
178
|
+
</button>
|
|
179
|
+
{/if}
|
|
180
|
+
</span>
|
|
181
|
+
{/snippet}
|
|
182
|
+
|
|
183
|
+
{#snippet grid(gridEntries: [string, TRow[]][])}
|
|
184
|
+
<div class="dct-facet" style:grid-template-columns="repeat({cols}, 1fr)" style:gap="{gap}px">
|
|
185
|
+
{#each gridEntries as [facetValue, rowsForCell] (facetValue)}
|
|
186
|
+
<div class="dct-facet__cell">
|
|
187
|
+
<div class="dct-facet__title">{facetValue}</div>
|
|
188
|
+
{#if cellWidth > 0 && cellHeight > 0}
|
|
189
|
+
{@render cell({ facetValue, data: rowsForCell, width: cellWidth, height: cellHeight })}
|
|
190
|
+
{/if}
|
|
191
|
+
</div>
|
|
192
|
+
{/each}
|
|
193
|
+
</div>
|
|
194
|
+
{/snippet}
|
|
195
|
+
|
|
196
|
+
{#if resolvedMode === 'paginated'}
|
|
197
|
+
<div class="dct-facet-paginated" style:width="{width}px" style:height="{height}px">
|
|
198
|
+
{@render grid(pageEntries)}
|
|
199
|
+
{#if totalPages > 1}
|
|
200
|
+
<div class="dct-facet-paginated__nav">
|
|
201
|
+
{@render navButtons('prev', page === 0, prevPage, 'Previous page', '‹')}
|
|
202
|
+
{@render navButtons('next', page >= totalPages - 1, nextPage, 'Next page', '›')}
|
|
203
|
+
</div>
|
|
204
|
+
{@render indicatorSlot(page + 1, totalPages)}
|
|
205
|
+
{/if}
|
|
206
|
+
</div>
|
|
207
|
+
{:else if resolvedMode === 'carousel'}
|
|
208
|
+
<div
|
|
209
|
+
class="dct-facet-carousel"
|
|
210
|
+
role="group"
|
|
211
|
+
aria-roledescription="carousel"
|
|
212
|
+
style:width="{width}px"
|
|
213
|
+
style:height="{height}px"
|
|
214
|
+
ontouchstart={handleTouchStart}
|
|
215
|
+
ontouchmove={handleTouchMove}
|
|
216
|
+
ontouchend={handleTouchEnd}
|
|
217
|
+
>
|
|
218
|
+
<div
|
|
219
|
+
class="dct-facet-carousel__track"
|
|
220
|
+
class:dct-facet-carousel__track--dragging={dragging}
|
|
221
|
+
style:gap="{gap}px"
|
|
222
|
+
style:transform="translateX({translate}px)"
|
|
223
|
+
>
|
|
224
|
+
{#each entries as [facetValue, rowsForCell] (facetValue)}
|
|
225
|
+
<div class="dct-facet__cell dct-facet-carousel__cell" style:width="{carouselCellWidth}px">
|
|
226
|
+
<div class="dct-facet__title">{facetValue}</div>
|
|
227
|
+
{#if carouselCellWidth > 0 && carouselCellHeight > 0}
|
|
228
|
+
{@render cell({
|
|
229
|
+
facetValue,
|
|
230
|
+
data: rowsForCell,
|
|
231
|
+
width: carouselCellWidth,
|
|
232
|
+
height: carouselCellHeight,
|
|
233
|
+
})}
|
|
234
|
+
{/if}
|
|
235
|
+
</div>
|
|
236
|
+
{/each}
|
|
74
237
|
</div>
|
|
75
|
-
|
|
76
|
-
|
|
238
|
+
{#if entries.length > 1}
|
|
239
|
+
{@render indicatorSlot(activeIndex + 1, entries.length)}
|
|
240
|
+
{/if}
|
|
241
|
+
</div>
|
|
242
|
+
{:else}
|
|
243
|
+
{@render grid(entries)}
|
|
244
|
+
{/if}
|
|
77
245
|
|
|
78
246
|
<style>
|
|
79
247
|
.dct-facet {
|
|
@@ -94,4 +262,61 @@
|
|
|
94
262
|
text-align: center;
|
|
95
263
|
opacity: 0.9;
|
|
96
264
|
}
|
|
265
|
+
|
|
266
|
+
.dct-facet-paginated {
|
|
267
|
+
position: relative;
|
|
268
|
+
display: flex;
|
|
269
|
+
flex-direction: column;
|
|
270
|
+
}
|
|
271
|
+
.dct-facet-paginated__nav {
|
|
272
|
+
position: absolute;
|
|
273
|
+
top: 50%;
|
|
274
|
+
left: 0;
|
|
275
|
+
right: 0;
|
|
276
|
+
display: flex;
|
|
277
|
+
justify-content: space-between;
|
|
278
|
+
transform: translateY(-50%);
|
|
279
|
+
pointer-events: none;
|
|
280
|
+
}
|
|
281
|
+
.dct-facet-nav-slot {
|
|
282
|
+
pointer-events: auto;
|
|
283
|
+
}
|
|
284
|
+
.dct-facet-paginated__btn {
|
|
285
|
+
border: none;
|
|
286
|
+
border-radius: 999px;
|
|
287
|
+
width: var(--dct-facet-nav-btn-size, 2rem);
|
|
288
|
+
height: var(--dct-facet-nav-btn-size, 2rem);
|
|
289
|
+
font-size: var(--dct-facet-nav-font-size, 1.1rem);
|
|
290
|
+
line-height: 1;
|
|
291
|
+
background: var(--dct-facet-nav-bg, #111827);
|
|
292
|
+
color: var(--dct-facet-nav-color, #fff);
|
|
293
|
+
cursor: pointer;
|
|
294
|
+
}
|
|
295
|
+
.dct-facet-paginated__btn:disabled {
|
|
296
|
+
opacity: var(--dct-facet-nav-disabled-opacity, 0.3);
|
|
297
|
+
cursor: default;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.dct-facet-indicator-slot {
|
|
301
|
+
position: absolute;
|
|
302
|
+
right: 0.5rem;
|
|
303
|
+
bottom: 0.5rem;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.dct-facet-carousel {
|
|
307
|
+
position: relative;
|
|
308
|
+
overflow: hidden;
|
|
309
|
+
touch-action: pan-y;
|
|
310
|
+
}
|
|
311
|
+
.dct-facet-carousel__track {
|
|
312
|
+
display: flex;
|
|
313
|
+
height: 100%;
|
|
314
|
+
transition: transform 0.25s ease;
|
|
315
|
+
}
|
|
316
|
+
.dct-facet-carousel__track--dragging {
|
|
317
|
+
transition: none;
|
|
318
|
+
}
|
|
319
|
+
.dct-facet-carousel__cell {
|
|
320
|
+
flex: 0 0 auto;
|
|
321
|
+
}
|
|
97
322
|
</style>
|