@fundar/data-chart-telling 0.0.15 → 0.0.17

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.
@@ -5,6 +5,7 @@
5
5
  import LegendLayout from '../layout/legend/LegendLayout.svelte';
6
6
  import { getConfiguration } from '../configuration/config.svelte';
7
7
  import { provideHover } from '../layout/tooltip/hover.svelte';
8
+ import { provideLegendInteraction } from '../layout/legend/interaction.svelte';
8
9
  import type { Snippet } from 'svelte';
9
10
  import type { LegendSection } from '../types/charts/legend';
10
11
  import type { HoverStrategy } from '../types/layout/tooltip';
@@ -59,6 +60,7 @@
59
60
  const cfg = $derived(getConfiguration());
60
61
 
61
62
  const hoverStore = provideHover();
63
+ provideLegendInteraction();
62
64
  $effect(() => {
63
65
  hoverStore.strategy = hover?.strategy ?? 'x';
64
66
  hoverStore.sync = hover?.sync ?? false;
@@ -46,6 +46,7 @@ export const DEFAULT_CONFIG = {
46
46
  titleWeight: 500,
47
47
  gap: '0.5rem 2rem',
48
48
  swatchSize: '1.25rem',
49
+ disabledOpacity: 0.12,
49
50
  },
50
51
  axis: {
51
52
  color: 'currentColor',
@@ -48,6 +48,7 @@ export declare const THEMES: {
48
48
  titleWeight?: string | number | undefined;
49
49
  gap?: string | undefined;
50
50
  swatchSize?: string | undefined;
51
+ disabledOpacity?: number | undefined;
51
52
  } | undefined;
52
53
  axis?: {
53
54
  color?: string | undefined;
@@ -170,6 +171,7 @@ export declare const THEMES: {
170
171
  titleWeight?: string | number | undefined;
171
172
  gap?: string | undefined;
172
173
  swatchSize?: string | undefined;
174
+ disabledOpacity?: number | undefined;
173
175
  } | undefined;
174
176
  axis?: {
175
177
  color?: string | undefined;
@@ -292,6 +294,7 @@ export declare const THEMES: {
292
294
  titleWeight?: string | number | undefined;
293
295
  gap?: string | undefined;
294
296
  swatchSize?: string | undefined;
297
+ disabledOpacity?: number | undefined;
295
298
  } | undefined;
296
299
  axis?: {
297
300
  color?: string | undefined;
@@ -414,6 +417,7 @@ export declare const THEMES: {
414
417
  titleWeight?: string | number | undefined;
415
418
  gap?: string | undefined;
416
419
  swatchSize?: string | undefined;
420
+ disabledOpacity?: number | undefined;
417
421
  } | undefined;
418
422
  axis?: {
419
423
  color?: string | undefined;
package/dist/index.d.ts CHANGED
@@ -44,7 +44,7 @@ export type { GeoProjectionName, GeoProjectionConfig, GeoCustomProjection, GeoCu
44
44
  export type { GeoStyles, GeoTileLayerConfig, GeoZoomConfig } from './types/plots/styles/geo';
45
45
  export type { GeoMarker, GeoInsetMarker, GeoInsetLocation, GeoInsetProjectionName, GeoInsetProjectionConfig, } from './types/markers/geo';
46
46
  export type { TimeValue, Orientation, FacetColumns, FacetConfig, TimelineConfig, ChartPlotContext, ChartPlotSnippet, } from './types/charts/common';
47
- export type { LegendSection, DiscreteLegendSection, ContinuousLegendSection, LegendItem, } from './types/charts/legend';
47
+ export type { LegendSection, DiscreteLegendSection, ContinuousLegendSection, LegendItem, LegendDisabledStyle, } from './types/charts/legend';
48
48
  export type { HoverStrategy, TooltipOptions, TooltipProp, } from './types/layout/tooltip';
49
49
  export type { ChartConfig, ChartConfigInput, TextStyle } from './types/configuration/styling';
50
50
  export type { ChartProps } from './types/charts/props';
@@ -1,32 +1,69 @@
1
1
  <script lang="ts">
2
+ import { untrack } from 'svelte';
2
3
  import type { ContinuousLegendSection } from '../../types/charts/legend';
3
4
  import { makeColorScale } from '../../utils/color';
4
5
  import { getConfiguration } from '../../configuration/config.svelte';
6
+ import { getLegendInteraction, createLegendInteraction } from './interaction.svelte';
5
7
 
6
8
  let { section }: { section: ContinuousLegendSection } = $props();
7
9
 
8
10
  const cfg = $derived(getConfiguration());
9
11
  const gradientId = `dct-grad-${Math.random().toString(36).slice(2)}`;
10
12
  const barWidth = $derived(section.width ?? 170);
13
+
14
+ const fmt = $derived(section.format ?? ((v: number) => String(v)));
15
+
16
+ const tickPositions = $derived(
17
+ section.interactive
18
+ ? []
19
+ : (section.ticks ?? []).map((v) => ({
20
+ v,
21
+ t: (v - section.min) / (section.max - section.min || 1),
22
+ label: fmt(v),
23
+ })),
24
+ );
25
+
26
+ // ── Drag-to-narrow-margins ────────────────────────────────────────────────
27
+ const interaction = getLegendInteraction() ?? createLegendInteraction();
28
+
29
+ // Seeded once from the section's own bounds (deliberately untracked — a
30
+ // later `section.min`/`max` change re-clamps the existing lo/hi instead of
31
+ // resetting them back out to the new full range, see below).
32
+ let lo = $state(untrack(() => section.min));
33
+ let hi = $state(untrack(() => section.max));
34
+
35
+ // `section.min`/`max` can shift under an already-active selection — e.g. a
36
+ // consumer swaps `data` for a different year on its own timeline, and the
37
+ // value that used to be the max is no longer present. Re-clamping (instead
38
+ // of leaving lo/hi at their old absolute values) keeps the handles inside
39
+ // whatever range now actually exists: the lower handle can only move up to
40
+ // meet a risen min, the upper handle can only move down to meet a fallen
41
+ // max. Clamping each bound against *both* of the new min/max first (not
42
+ // just its own side) keeps lo/hi from inverting if the domain shifts
43
+ // entirely past the old selection.
44
+ $effect(() => {
45
+ lo = Math.min(Math.max(lo, section.min), section.max);
46
+ hi = Math.max(Math.min(hi, section.max), section.min);
47
+ });
48
+
49
+ function toT(v: number): number {
50
+ return (v - section.min) / (section.max - section.min || 1);
51
+ }
52
+ const loT = $derived(toT(lo));
53
+ const hiT = $derived(toT(hi));
54
+
55
+ const colorDomain = $derived.by((): [number, number] =>
56
+ section.interactive && (section.mode ?? 'filter') === 'rescale' ? [lo, hi] : [section.min, section.max],
57
+ );
11
58
  const colorScale = $derived(
12
59
  makeColorScale(
13
- section.min,
14
- section.max,
60
+ colorDomain[0],
61
+ colorDomain[1],
15
62
  section.colorMin ?? cfg.continuous.min,
16
63
  section.colorMax ?? cfg.continuous.max,
17
64
  ),
18
65
  );
19
66
 
20
- const fmt = $derived(section.format ?? ((v: number) => String(v)));
21
-
22
- const tickPositions = $derived(
23
- (section.ticks ?? []).map((v) => ({
24
- v,
25
- t: (v - section.min) / (section.max - section.min || 1),
26
- label: fmt(v),
27
- })),
28
- );
29
-
30
67
  const stops = $derived(
31
68
  Array.from({ length: 20 }, (_, i) => {
32
69
  const t = i / 19;
@@ -34,6 +71,56 @@
34
71
  return { offset: t * 100, color: colorScale(v) };
35
72
  }),
36
73
  );
74
+
75
+ $effect(() => {
76
+ if (!section.interactive) return;
77
+ interaction.continuousRange =
78
+ lo <= section.min && hi >= section.max
79
+ ? null
80
+ : {
81
+ min: lo,
82
+ max: hi,
83
+ mode: section.mode ?? 'filter',
84
+ disabledStyle: section.disabledStyle ?? { opacity: cfg.legend.disabledOpacity },
85
+ };
86
+ });
87
+
88
+ function startDrag(which: 'lo' | 'hi') {
89
+ return (event: PointerEvent) => {
90
+ if (!section.interactive) return;
91
+ event.preventDefault();
92
+ const target = event.currentTarget as SVGElement;
93
+ target.setPointerCapture(event.pointerId);
94
+ const startX = event.clientX;
95
+ const startValue = which === 'lo' ? lo : hi;
96
+ const valuePerPixel = (section.max - section.min) / barWidth;
97
+
98
+ function onMove(e: PointerEvent): void {
99
+ const next = startValue + (e.clientX - startX) * valuePerPixel;
100
+ if (which === 'lo') {
101
+ lo = Math.max(section.min, Math.min(next, hi));
102
+ } else {
103
+ hi = Math.min(section.max, Math.max(next, lo));
104
+ }
105
+ }
106
+ function onUp(e: PointerEvent): void {
107
+ target.releasePointerCapture(e.pointerId);
108
+ target.removeEventListener('pointermove', onMove);
109
+ target.removeEventListener('pointerup', onUp);
110
+ target.removeEventListener('pointercancel', onUp);
111
+ }
112
+ target.addEventListener('pointermove', onMove);
113
+ target.addEventListener('pointerup', onUp);
114
+ target.addEventListener('pointercancel', onUp);
115
+ };
116
+ }
117
+
118
+ function resetHandle(which: 'lo' | 'hi') {
119
+ return () => {
120
+ if (which === 'lo') lo = section.min;
121
+ else hi = section.max;
122
+ };
123
+ }
37
124
  </script>
38
125
 
39
126
  <div class="dct-continuous" style:width="{barWidth}px">
@@ -56,14 +143,56 @@
56
143
  stroke-width="1"
57
144
  />
58
145
  {/each}
146
+
147
+ {#if section.interactive}
148
+ {#if loT > 0}
149
+ <rect x="0" y="0" width={loT * barWidth} height="12" rx="2" fill="currentColor" fill-opacity="0.35" />
150
+ {/if}
151
+ {#if hiT < 1}
152
+ <rect x={hiT * barWidth} y="0" width={barWidth - hiT * barWidth} height="12" rx="2" fill="currentColor" fill-opacity="0.35" />
153
+ {/if}
154
+ <circle
155
+ class="dct-continuous__handle"
156
+ cx={loT * barWidth}
157
+ cy="6"
158
+ r="6"
159
+ onpointerdown={startDrag('lo')}
160
+ ondblclick={resetHandle('lo')}
161
+ role="slider"
162
+ aria-label="Margen inferior"
163
+ aria-valuemin={section.min}
164
+ aria-valuemax={hi}
165
+ aria-valuenow={lo}
166
+ tabindex="0"
167
+ />
168
+ <circle
169
+ class="dct-continuous__handle"
170
+ cx={hiT * barWidth}
171
+ cy="6"
172
+ r="6"
173
+ onpointerdown={startDrag('hi')}
174
+ ondblclick={resetHandle('hi')}
175
+ role="slider"
176
+ aria-label="Margen superior"
177
+ aria-valuemin={lo}
178
+ aria-valuemax={section.max}
179
+ aria-valuenow={hi}
180
+ tabindex="0"
181
+ />
182
+ {/if}
59
183
  </svg>
60
184
 
61
185
  <div class="dct-continuous__labels" style:width="{barWidth}px">
62
- <span class="dct-continuous__edge dct-continuous__edge--start">{fmt(section.min)}</span>
63
- {#each tickPositions as tick (tick.v)}
64
- <span class="dct-continuous__tick" style:left="{tick.t * barWidth}px">{tick.label}</span>
65
- {/each}
66
- <span class="dct-continuous__edge dct-continuous__edge--end">{fmt(section.max)}</span>
186
+ {#if section.interactive}
187
+ <span class="dct-continuous__tick dct-continuous__tick--handle-lo" style:left="{loT * barWidth}px">{fmt(lo)}</span>
188
+ <span class="dct-continuous__tick dct-continuous__tick--handle-hi" style:left="{hiT * barWidth}px">{fmt(hi)}</span>
189
+ {:else}
190
+ <span class="dct-continuous__edge dct-continuous__edge--start">{fmt(section.min)}</span>
191
+ {#each tickPositions as tick (tick.v)}
192
+ <span class="dct-continuous__tick" style:left="{tick.t * barWidth}px">{tick.label}</span>
193
+ {/each}
194
+ <span class="dct-continuous__edge dct-continuous__edge--end">{fmt(section.max)}</span>
195
+ {/if}
67
196
  </div>
68
197
  </div>
69
198
 
@@ -92,4 +221,31 @@
92
221
  position: absolute;
93
222
  transform: translateX(-50%);
94
223
  }
224
+ /*
225
+ * Anchored outward (not centered, unlike a plain tick) so the label always
226
+ * grows into the bar instead of over its own edge — a centered label at
227
+ * lo=min or hi=max would overflow past `.dct-chart`'s `overflow: hidden`
228
+ * and get clipped, exactly like the old fixed min/max edge labels never did.
229
+ */
230
+ .dct-continuous__tick--handle-lo,
231
+ .dct-continuous__tick--handle-hi {
232
+ font-weight: 600;
233
+ }
234
+ .dct-continuous__tick--handle-lo {
235
+ transform: none;
236
+ }
237
+ .dct-continuous__tick--handle-hi {
238
+ transform: translateX(-100%);
239
+ }
240
+ .dct-continuous__handle {
241
+ fill: currentColor;
242
+ stroke: var(--dct-bg, #fff);
243
+ stroke-width: 1.5;
244
+ cursor: ew-resize;
245
+ touch-action: none;
246
+ }
247
+ .dct-continuous__handle:focus-visible {
248
+ outline: 2px solid currentColor;
249
+ outline-offset: 2px;
250
+ }
95
251
  </style>
@@ -1,13 +1,42 @@
1
1
  <script lang="ts">
2
+ import { SvelteSet } from 'svelte/reactivity';
2
3
  import { getConfiguration } from '../../configuration/config.svelte';
3
4
  import { paletteColor } from '../../utils/color';
4
- import type { DiscreteLegendSection } from '../../types/charts/legend';
5
+ import { getLegendInteraction, createLegendInteraction } from './interaction.svelte';
6
+ import type { DiscreteLegendSection, LegendItem } from '../../types/charts/legend';
5
7
  import type { LineStyle } from '../../types/plots/constants';
6
8
 
7
9
  let { section }: { section: DiscreteLegendSection } = $props();
8
10
 
9
11
  const cfg = $derived(getConfiguration());
10
12
  const columns = $derived(section.columns ?? 1);
13
+ const interaction = getLegendInteraction() ?? createLegendInteraction();
14
+
15
+ const seeded = new SvelteSet<string>();
16
+ $effect(() => {
17
+ for (const item of section.items) {
18
+ if (seeded.has(item.name)) continue;
19
+ seeded.add(item.name);
20
+ if (item.disabled) {
21
+ interaction.disabledSeries.set(item.name, section.disabledStyle ?? { opacity: cfg.legend.disabledOpacity });
22
+ }
23
+ }
24
+ });
25
+
26
+ function toggle(item: LegendItem): void {
27
+ if (!section.interactive) return;
28
+ if (interaction.disabledSeries.has(item.name)) {
29
+ interaction.disabledSeries.delete(item.name);
30
+ } else {
31
+ interaction.disabledSeries.set(item.name, section.disabledStyle ?? { opacity: cfg.legend.disabledOpacity });
32
+ }
33
+ }
34
+
35
+ function onKeydown(event: KeyboardEvent, item: LegendItem): void {
36
+ if (event.key !== 'Enter' && event.key !== ' ') return;
37
+ event.preventDefault();
38
+ toggle(item);
39
+ }
11
40
 
12
41
  function dashArray(style?: LineStyle): string | undefined {
13
42
  switch (style) {
@@ -25,8 +54,23 @@
25
54
 
26
55
  <div class="dct-discrete" style:grid-template-columns="repeat({columns}, max-content)">
27
56
  {#each section.items as item, i (item.name)}
28
- {@const color = item.color ?? paletteColor(i, cfg.palette)}
29
- <div class="dct-discrete__item" style:opacity={item.opacity ?? 1}>
57
+ {@const disabledStyle = interaction.disabledSeries.get(item.name)}
58
+ {@const color = disabledStyle?.fill ?? disabledStyle?.stroke?.stroke ?? item.color ?? paletteColor(i, cfg.palette)}
59
+ {@const itemOpacity = disabledStyle ? (disabledStyle.opacity ?? cfg.legend.disabledOpacity) : (item.opacity ?? 1)}
60
+ <div
61
+ class="dct-discrete__item"
62
+ class:dct-discrete__item--interactive={section.interactive}
63
+ style:opacity={itemOpacity}
64
+ {...section.interactive
65
+ ? {
66
+ role: 'button',
67
+ tabindex: 0,
68
+ 'aria-pressed': disabledStyle != null,
69
+ onclick: () => toggle(item),
70
+ onkeydown: (e: KeyboardEvent) => onKeydown(e, item),
71
+ }
72
+ : {}}
73
+ >
30
74
  <svg class="dct-discrete__swatch" viewBox="0 0 15 10">
31
75
  {#if item.symbol === 'circle'}
32
76
  <circle cx="5" cy="5" r="4" fill={color} />
@@ -70,6 +114,14 @@
70
114
  align-items: center;
71
115
  gap: 0.35rem;
72
116
  min-width: 0;
117
+ transition: opacity 0.15s ease;
118
+ }
119
+ .dct-discrete__item--interactive {
120
+ cursor: pointer;
121
+ }
122
+ .dct-discrete__item--interactive:focus-visible {
123
+ outline: 2px solid currentColor;
124
+ outline-offset: 2px;
73
125
  }
74
126
  .dct-discrete__swatch {
75
127
  width: var(--dct-legend-swatch, 1.25rem);
@@ -0,0 +1,7 @@
1
+ import type { LegendInteractionStore } from '../../types/layout/legend';
2
+ /** Create + register the shared legend-interaction store. Call during component init. */
3
+ export declare function provideLegendInteraction(): LegendInteractionStore;
4
+ /** Read the shared legend-interaction store, or `null` when there is no provider. */
5
+ export declare function getLegendInteraction(): LegendInteractionStore | null;
6
+ /** A private legend-interaction store for a legend/plot used outside of a {@link BaseChart}. */
7
+ export declare function createLegendInteraction(): LegendInteractionStore;
@@ -0,0 +1,24 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ import { SvelteMap } from 'svelte/reactivity';
3
+ const LEGEND_INTERACTION_KEY = Symbol('dct-legend-interaction');
4
+ function blank() {
5
+ return {
6
+ disabledSeries: new SvelteMap(),
7
+ continuousRange: null,
8
+ };
9
+ }
10
+ /** Create + register the shared legend-interaction store. Call during component init. */
11
+ export function provideLegendInteraction() {
12
+ const store = $state(blank());
13
+ setContext(LEGEND_INTERACTION_KEY, store);
14
+ return store;
15
+ }
16
+ /** Read the shared legend-interaction store, or `null` when there is no provider. */
17
+ export function getLegendInteraction() {
18
+ return getContext(LEGEND_INTERACTION_KEY) ?? null;
19
+ }
20
+ /** A private legend-interaction store for a legend/plot used outside of a {@link BaseChart}. */
21
+ export function createLegendInteraction() {
22
+ const store = $state(blank());
23
+ return store;
24
+ }
@@ -18,6 +18,7 @@
18
18
  isHorizontal,
19
19
  BarMark,
20
20
  defaultColor,
21
+ disabledFill,
21
22
  }: {
22
23
  group: VisualGroup<Series<TData>, BarSegmentStyle>;
23
24
  seriesIndex: number;
@@ -25,6 +26,7 @@
25
26
  isHorizontal: boolean;
26
27
  BarMark: typeof BarX | typeof BarY;
27
28
  defaultColor: string;
29
+ disabledFill?: { fill?: string; fillOpacity?: number };
28
30
  } = $props();
29
31
 
30
32
  const xFn = $derived(resolveAccessor(group.series.x));
@@ -39,7 +41,8 @@
39
41
  {...isHorizontal
40
42
  ? { y: xFn, x1: stackedBaselineFn, x2: stackedTopFn }
41
43
  : { x: xFn, y1: stackedBaselineFn, y2: stackedTopFn }}
42
- fill={defaultColor}
44
+ fill={disabledFill?.fill ?? defaultColor}
45
+ fillOpacity={disabledFill?.fillOpacity ?? 1}
43
46
  />
44
47
  {:else}
45
48
  {#each group.visualSegments as seg, i (`${group.series.name}-${i}`)}
@@ -48,8 +51,8 @@
48
51
  {...isHorizontal
49
52
  ? { y: xFn, x1: barLayout.valueBaseline, x2: yFn }
50
53
  : { x: xFn, y1: barLayout.valueBaseline, y2: yFn }}
51
- fill={seg.style.fill ?? defaultColor}
52
- fillOpacity={seg.style.fillOpacity ?? 1}
54
+ fill={disabledFill?.fill ?? seg.style.fill ?? defaultColor}
55
+ fillOpacity={disabledFill?.fillOpacity ?? seg.style.fillOpacity ?? 1}
53
56
  {...barLayout.layout === 'grouped'
54
57
  ? isHorizontal
55
58
  ? {
@@ -11,6 +11,10 @@ declare function $$render<TData extends Record<string, unknown>>(): {
11
11
  isHorizontal: boolean;
12
12
  BarMark: typeof BarX | typeof BarY;
13
13
  defaultColor: string;
14
+ disabledFill?: {
15
+ fill?: string;
16
+ fillOpacity?: number;
17
+ };
14
18
  };
15
19
  exports: {};
16
20
  bindings: "";
@@ -5,6 +5,8 @@
5
5
  import { SvelteMap } from 'svelte/reactivity';
6
6
  import { paletteColor } from '../../utils/color';
7
7
  import { buildSeries } from '../../utils/grouping';
8
+ import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
9
+ import { disabledFillOverride } from '../utils/legendDisabled';
8
10
  import { BarX, BarY } from 'svelteplot';
9
11
  import { resolveAccessor } from '../utils/accessors';
10
12
  import { getConfiguration } from '../../configuration/config.svelte';
@@ -160,7 +162,14 @@
160
162
  // which drive tick/domain computation and the Pointer's hit-testing) —
161
163
  // `row` (see BasePlotLayout/HoverPoint) is what lets the template recover the
162
164
  // semantic category regardless of which axis it landed on.
163
- const hoverPoints = $derived(buildHoverPoints(resolvedSeries, isHorizontal, barLayout, cfg.palette));
165
+ const legendInteraction = $derived(getLegendInteraction());
166
+ // A legend-disabled series is dimmed on the bar itself, but never surfaces
167
+ // in hover/tooltip — its points are simply absent from the candidate list.
168
+ const hoverPoints = $derived(
169
+ buildHoverPoints(resolvedSeries, isHorizontal, barLayout, cfg.palette).filter(
170
+ (p) => !legendInteraction?.disabledSeries.has(p.series!),
171
+ ),
172
+ );
164
173
 
165
174
  const hoverActive = $derived(tooltip != null || hasHoverMarker(markers));
166
175
  const hoverStrategy = $derived(resolveHoverStrategy(tooltip?.strategy, markers, categoricalAxis));
@@ -189,7 +198,8 @@
189
198
  <RuleLayout {markers} seriesData={resolvedSeries} {matchedPoints} {ruleX} {ruleY} {impliesRuleX} {impliesRuleY}>
190
199
  {#each groupedSeries as group, seriesIndex (group.series.name)}
191
200
  {@const defaultColor = paletteColor(seriesIndex, cfg.palette)}
192
- <BarSegments {group} {seriesIndex} {barLayout} {isHorizontal} {BarMark} {defaultColor} />
201
+ {@const disabledFill = disabledFillOverride(legendInteraction?.disabledSeries.get(group.series.name))}
202
+ <BarSegments {group} {seriesIndex} {barLayout} {isHorizontal} {BarMark} {defaultColor} {disabledFill} />
193
203
 
194
204
  {#if showVals}
195
205
  <ValueLabels {group} {seriesIndex} {barLayout} {isHorizontal} {valAnchor} {fmtVal} values={styles.values} axisColor={cfg.axis.color} />
@@ -13,6 +13,8 @@
13
13
  import { isTopology, topologyToGeoJson } from '../utils/topojson';
14
14
  import { makeColorScale } from '../../utils/color';
15
15
  import { getConfiguration } from '../../configuration/config.svelte';
16
+ import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
17
+ import { disabledFillOverride } from '../utils/legendDisabled';
16
18
  import { GEO_MARGIN_ESTIMATE } from '../../layout/plot/margins';
17
19
  import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
18
20
  import GeoLayout from '../../layout/geo/GeoLayout.svelte';
@@ -165,8 +167,16 @@
165
167
  return vals.length ? [Math.min(...vals), Math.max(...vals)] : null;
166
168
  });
167
169
 
170
+ // ── Legend-driven range filter (mirrors HeatmapPlot's own) ────────────────
171
+ const legendInteraction = $derived(getLegendInteraction());
172
+ const range = $derived(legendInteraction?.continuousRange);
173
+ const effectiveDomain = $derived.by((): [number, number] | null => {
174
+ if (!valueDomain) return null;
175
+ return range && range.mode === 'rescale' ? [range.min, range.max] : valueDomain;
176
+ });
177
+
168
178
  const colorScale = $derived(
169
- valueDomain ? makeColorScale(valueDomain[0], valueDomain[1], minColor, maxColor) : null,
179
+ effectiveDomain ? makeColorScale(effectiveDomain[0], effectiveDomain[1], minColor, maxColor) : null,
170
180
  );
171
181
 
172
182
  function resolveBaseFill(f: TaggedFeature): string {
@@ -177,6 +187,14 @@
177
187
  return styles.fill ?? cfg.palette[0];
178
188
  }
179
189
 
190
+ function rangeFilteredStyle(base: GeoSegmentStyle, f: TaggedFeature): GeoSegmentStyle {
191
+ const r = range;
192
+ if (r == null || !value) return base;
193
+ const v = value(f);
194
+ if (v == null || (v >= r.min && v <= r.max)) return base;
195
+ return { ...base, ...disabledFillOverride(r.disabledStyle) };
196
+ }
197
+
180
198
  /** Per-marker color ramp for a `dot` marker's `value` channel — independent of the base map's own choropleth ramp. */
181
199
  function makeDotColorScale(marker: GeoDotMarker): ((v: number) => string) | null {
182
200
  if (!marker.value) return null;
@@ -197,7 +215,7 @@
197
215
  // shared numeric-scale machinery collapses a per-row fillOpacity/strokeWidth
198
216
  // *accessor* to a single value, so literal per-call constants are required.)
199
217
  function getFeatureStyle(f: TaggedFeature): GeoSegmentStyle {
200
- return resolveFeatureStyle<GeoSegmentStyle>(segments, f.__id);
218
+ return rangeFilteredStyle(resolveFeatureStyle<GeoSegmentStyle>(segments, f.__id), f);
201
219
  }
202
220
 
203
221
  type StyleGroup = { style: GeoSegmentStyle; data: TaggedFeature[] };
@@ -228,7 +246,18 @@
228
246
  features.flatMap((f) => geometryPartCentroids(f.geometry).map(([cx, cy]) => ({ ...f, __cx: cx, __cy: cy }))),
229
247
  );
230
248
 
231
- const hoverPoints = $derived(hoverCandidates.map((f) => ({ x: f.__cx, y: f.__cy, row: f, label: f.__id })));
249
+ // A feature outside the active range is dimmed in place, but never
250
+ // surfaces in hover/tooltip — it's simply absent from the candidate list.
251
+ const hoverPoints = $derived(
252
+ hoverCandidates
253
+ .filter((f) => {
254
+ const r = range;
255
+ if (r == null || !value) return true;
256
+ const v = value(f);
257
+ return v == null || (v >= r.min && v <= r.max);
258
+ })
259
+ .map((f) => ({ x: f.__cx, y: f.__cy, row: f, label: f.__id })),
260
+ );
232
261
  const hoverGetX = (f: TaggedFeature) => f.__cx;
233
262
  const hoverGetY = (f: TaggedFeature) => f.__cy;
234
263
 
@@ -5,6 +5,8 @@
5
5
  import { buildSeries } from '../../utils/grouping';
6
6
  import { makeColorScale } from '../../utils/color';
7
7
  import { getConfiguration } from '../../configuration/config.svelte';
8
+ import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
9
+ import { disabledFillOverride } from '../utils/legendDisabled';
8
10
  import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
9
11
  import AxisLayout from '../../layout/plot/AxisLayout.svelte';
10
12
  import GridLayout from '../../layout/plot/GridLayout.svelte';
@@ -86,15 +88,6 @@
86
88
  const getY = $derived(resolveAccessor<TData>(resolvedSeries[0]?.y ?? y!));
87
89
  const getZ = $derived(resolveAccessor<TData>(resolvedSeries[0]?.z ?? z!));
88
90
 
89
- const hoverPoints = $derived(
90
- flat.map((d) => ({
91
- x: getX(d) as AxisValue,
92
- y: getY(d) as AxisValue,
93
- row: d,
94
- label: fmtVal(Number(getZ(d)), ''),
95
- })),
96
- );
97
-
98
91
  const xDomain = $derived([...new Set(flat.map((d) => getX(d) as AxisValue))]);
99
92
  const yDomain = $derived([...new Set(flat.map((d) => getY(d) as AxisValue))]);
100
93
 
@@ -106,8 +99,42 @@
106
99
  return vals.length ? [Math.min(...vals, 0), Math.max(...vals)] : [0, 1];
107
100
  });
108
101
 
102
+ // ── Legend-driven range filter ────────────────────────────────────────────
103
+ const legendInteraction = $derived(getLegendInteraction());
104
+ const range = $derived(legendInteraction?.continuousRange);
105
+ // 'rescale' reprojects the ramp onto the narrowed range; 'filter' (and no
106
+ // active range) keeps the original domain — only which cells are hidden
107
+ // changes.
108
+ const effectiveDomain = $derived.by((): [number, number] =>
109
+ range && range.mode === 'rescale' ? [range.min, range.max] : valueDomain,
110
+ );
111
+
112
+ function rangeFilteredStyle(base: CellSegmentStyle, value: number): CellSegmentStyle {
113
+ const r = range;
114
+ if (r == null || (value >= r.min && value <= r.max)) return base;
115
+ return { ...base, ...disabledFillOverride(r.disabledStyle) };
116
+ }
117
+
109
118
  const colorScale = $derived(
110
- makeColorScale(valueDomain[0], valueDomain[1], minColor, maxColor),
119
+ makeColorScale(effectiveDomain[0], effectiveDomain[1], minColor, maxColor),
120
+ );
121
+
122
+ // A cell outside the active range is dimmed in place, but never surfaces
123
+ // in hover/tooltip — it's simply absent from the candidate list.
124
+ const hoverPoints = $derived(
125
+ flat
126
+ .filter((d) => {
127
+ const r = range;
128
+ if (r == null) return true;
129
+ const v = Number(getZ(d));
130
+ return v >= r.min && v <= r.max;
131
+ })
132
+ .map((d) => ({
133
+ x: getX(d) as AxisValue,
134
+ y: getY(d) as AxisValue,
135
+ row: d,
136
+ label: fmtVal(Number(getZ(d)), ''),
137
+ })),
111
138
  );
112
139
 
113
140
  const segList = $derived(segments.default ?? []);
@@ -152,7 +179,11 @@
152
179
  });
153
180
 
154
181
  const getCellStyle = $derived(
155
- (d: TData) => segStyleMap.get(`${String(getX(d))}::${String(getY(d))}`) ?? {},
182
+ (d: TData) =>
183
+ rangeFilteredStyle(
184
+ segStyleMap.get(`${String(getX(d))}::${String(getY(d))}`) ?? {},
185
+ Number(getZ(d)),
186
+ ),
156
187
  );
157
188
 
158
189
  /**
@@ -8,6 +8,8 @@
8
8
  import { getConfiguration } from '../../configuration/config.svelte';
9
9
  import { paletteColor } from '../../utils/color';
10
10
  import { buildSeries } from '../../utils/grouping';
11
+ import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
12
+ import { disabledStrokeOverride, disabledDotsOverride } from '../utils/legendDisabled';
11
13
  import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
12
14
  import AxisLayout from '../../layout/plot/AxisLayout.svelte';
13
15
  import GridLayout from '../../layout/plot/GridLayout.svelte';
@@ -50,6 +52,7 @@
50
52
  }: Props = $props();
51
53
 
52
54
  const resolvedSeries = $derived(series ?? buildSeries(data!, x!, y!, z));
55
+ const legendInteraction = $derived(getLegendInteraction());
53
56
 
54
57
  $effect(() => { validateSegments(segments); });
55
58
 
@@ -82,8 +85,11 @@
82
85
  const xAcc = $derived(resolveAccessor(resolvedSeries[0]?.x ?? ((d: TData) => d)));
83
86
  const yAcc = $derived(resolveAccessor(resolvedSeries[0]?.y ?? ((d: TData) => d)));
84
87
  const flat = $derived(resolvedSeries.flatMap((s) => s.data));
88
+ // A legend-disabled series is dimmed on the mark itself, but never surfaces
89
+ // in hover/tooltip — its points are simply absent from the candidate list.
85
90
  const hoverPoints = $derived(
86
91
  resolvedSeries.flatMap((s, idx) => {
92
+ if (legendInteraction?.disabledSeries.has(s.name)) return [];
87
93
  const sx = resolveAccessor(s.x);
88
94
  const sy = resolveAccessor(s.y);
89
95
  return s.data.map((d) => ({
@@ -131,6 +137,9 @@
131
137
  <RuleLayout {markers} seriesData={resolvedSeries} {matchedPoints} {ruleX} {ruleY} {impliesRuleX} {impliesRuleY}>
132
138
  {#each groupedSeries as group, seriesIndex (group.series.name)}
133
139
  {@const defaultColor = paletteColor(seriesIndex, cfg.palette)}
140
+ {@const disabledOverride = legendInteraction?.disabledSeries.get(group.series.name)}
141
+ {@const disabledStroke = disabledStrokeOverride(disabledOverride)}
142
+ {@const disabledDots = disabledDotsOverride(disabledOverride)}
134
143
  {#each group.visualSegments as seg, i (`${group.series.name}-${i}`)}
135
144
  {@const strokeStyle = seg.style.stroke}
136
145
  {@const lineData = seg.data as unknown as Record<string | symbol, never>[]}
@@ -138,12 +147,12 @@
138
147
  data={lineData}
139
148
  x={resolveAccessor(group.series.x)}
140
149
  y={resolveAccessor(group.series.y)}
141
- stroke={strokeStyle?.stroke ?? defaultColor}
142
- strokeWidth={strokeStyle?.strokeWidth ?? cfg.line.strokeWidth}
143
- strokeOpacity={strokeStyle?.strokeOpacity ?? 1}
144
- strokeDasharray={strokeStyle?.strokeDasharray}
145
- strokeLinejoin={strokeStyle?.strokeLinejoin}
146
- strokeLinecap={strokeStyle?.strokeLinecap}
150
+ stroke={disabledStroke?.stroke ?? strokeStyle?.stroke ?? defaultColor}
151
+ strokeWidth={disabledStroke?.strokeWidth ?? strokeStyle?.strokeWidth ?? cfg.line.strokeWidth}
152
+ strokeOpacity={disabledStroke?.strokeOpacity ?? strokeStyle?.strokeOpacity ?? 1}
153
+ strokeDasharray={disabledStroke?.strokeDasharray ?? strokeStyle?.strokeDasharray}
154
+ strokeLinejoin={disabledStroke?.strokeLinejoin ?? strokeStyle?.strokeLinejoin}
155
+ strokeLinecap={disabledStroke?.strokeLinecap ?? strokeStyle?.strokeLinecap}
147
156
  />
148
157
  {#if seg.style.dots}
149
158
  {@const dotStyle = seg.style.dots}
@@ -151,14 +160,14 @@
151
160
  data={seg.data}
152
161
  x={resolveAccessor(group.series.x)}
153
162
  y={resolveAccessor(group.series.y)}
154
- r={dotStyle.dotRadius ?? cfg.line.dotRadius}
155
- fill={dotStyle.dotFill ?? strokeStyle?.stroke ?? defaultColor}
156
- symbol={dotStyle.dotSymbol ?? 'circle'}
157
- stroke={dotStyle.dotStroke ?? strokeStyle?.stroke ?? defaultColor}
158
- strokeWidth={dotStyle.dotStrokeWidth ?? 1}
159
- fillOpacity={dotStyle.dotFillOpacity ?? strokeStyle?.strokeOpacity ?? 1}
160
- strokeOpacity={dotStyle.dotStrokeOpacity ?? strokeStyle?.strokeOpacity ?? 1}
161
- strokeDasharray={dotStyle.dotStrokeDasharray}
163
+ r={disabledDots?.dotRadius ?? dotStyle.dotRadius ?? cfg.line.dotRadius}
164
+ fill={disabledDots?.dotFill ?? dotStyle.dotFill ?? strokeStyle?.stroke ?? defaultColor}
165
+ symbol={disabledDots?.dotSymbol ?? dotStyle.dotSymbol ?? 'circle'}
166
+ stroke={disabledDots?.dotStroke ?? dotStyle.dotStroke ?? strokeStyle?.stroke ?? defaultColor}
167
+ strokeWidth={disabledDots?.dotStrokeWidth ?? dotStyle.dotStrokeWidth ?? 1}
168
+ fillOpacity={disabledDots?.dotFillOpacity ?? dotStyle.dotFillOpacity ?? strokeStyle?.strokeOpacity ?? 1}
169
+ strokeOpacity={disabledDots?.dotStrokeOpacity ?? dotStyle.dotStrokeOpacity ?? strokeStyle?.strokeOpacity ?? 1}
170
+ strokeDasharray={disabledDots?.dotStrokeDasharray ?? dotStyle.dotStrokeDasharray}
162
171
  />
163
172
  {/if}
164
173
  {/each}
@@ -5,6 +5,8 @@
5
5
  import { getConfiguration } from '../../configuration/config.svelte';
6
6
  import { paletteColor } from '../../utils/color';
7
7
  import { buildSeries } from '../../utils/grouping';
8
+ import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
9
+ import { disabledFillOverride } from '../utils/legendDisabled';
8
10
  import { buildGroupedSeries, validateSegments, resolveSegmentsForSeries, matchesSegmentX, getDataForSegments } from '../utils/segments';
9
11
  import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
10
12
  import AxisLayout from '../../layout/plot/AxisLayout.svelte';
@@ -65,6 +67,7 @@
65
67
  $effect(() => { validateSegments(segments); });
66
68
 
67
69
  const cfg = $derived(getConfiguration());
70
+ const legendInteraction = $derived(getLegendInteraction());
68
71
  const leftColor = $derived(styles.colors?.left ?? paletteColor(0, cfg.palette));
69
72
  const rightColor = $derived(styles.colors?.right ?? paletteColor(1, cfg.palette));
70
73
  const showVals = $derived(styles.values?.show ?? false);
@@ -134,15 +137,19 @@
134
137
  const getY = (d: TaggedRow) => d.__y;
135
138
  const labelFor = (d: TaggedRow) => d.__label;
136
139
 
140
+ // A legend-disabled series is dimmed on the bar itself, but never surfaces
141
+ // in hover/tooltip — its points are simply absent from the candidate list.
137
142
  const hoverPoints = $derived(
138
- flat.map((d) => ({
139
- x: d.__x,
140
- y: d.__y,
141
- row: d,
142
- label: d.__label,
143
- series: d.__series,
144
- color: d.__side === 'left' ? leftColor : rightColor,
145
- })),
143
+ flat
144
+ .filter((d) => !legendInteraction?.disabledSeries.has(d.__series))
145
+ .map((d) => ({
146
+ x: d.__x,
147
+ y: d.__y,
148
+ row: d,
149
+ label: d.__label,
150
+ series: d.__series,
151
+ color: d.__side === 'left' ? leftColor : rightColor,
152
+ })),
146
153
  );
147
154
 
148
155
  const yDomain = $derived([
@@ -216,14 +223,15 @@
216
223
  {#each groupedSeries as group (group.series.name)}
217
224
  {@const defaultColor = group.series.side === 'left' ? leftColor : rightColor}
218
225
  {@const getCategory = resolveAccessor(group.series.x)}
226
+ {@const disabledFill = disabledFillOverride(legendInteraction?.disabledSeries.get(group.series.name))}
219
227
  {#each group.visualSegments as seg, i (`${group.series.name}-${i}`)}
220
228
  <BarX
221
229
  data={seg.data}
222
230
  x1={0}
223
231
  x2={(d: TData) => signedValue(group.series, d)}
224
232
  y={getCategory}
225
- fill={seg.style.fill ?? defaultColor}
226
- fillOpacity={seg.style.fillOpacity ?? 1}
233
+ fill={disabledFill?.fill ?? seg.style.fill ?? defaultColor}
234
+ fillOpacity={disabledFill?.fillOpacity ?? seg.style.fillOpacity ?? 1}
227
235
  />
228
236
  {/each}
229
237
 
@@ -0,0 +1,15 @@
1
+ import type { LegendDisabledStyle } from '../../types/charts/legend';
2
+ import type { StrokeStyle, DotStyle } from '../../types/plots/styling';
3
+ /**
4
+ * The three per-mark override layers a legend "disabled" state can apply,
5
+ * derived once from a {@link LegendDisabledStyle}. Each is `undefined` when
6
+ * there's no active override, so call sites can slot the result in as just
7
+ * one more `??` layer ahead of their existing segment-style/default-color
8
+ * chain — no branching needed at the call site.
9
+ */
10
+ export declare function disabledStrokeOverride(disabled: LegendDisabledStyle | undefined): StrokeStyle | undefined;
11
+ export declare function disabledDotsOverride(disabled: LegendDisabledStyle | undefined): DotStyle | undefined;
12
+ export declare function disabledFillOverride(disabled: LegendDisabledStyle | undefined): {
13
+ fill?: string;
14
+ fillOpacity?: number;
15
+ } | undefined;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The three per-mark override layers a legend "disabled" state can apply,
3
+ * derived once from a {@link LegendDisabledStyle}. Each is `undefined` when
4
+ * there's no active override, so call sites can slot the result in as just
5
+ * one more `??` layer ahead of their existing segment-style/default-color
6
+ * chain — no branching needed at the call site.
7
+ */
8
+ export function disabledStrokeOverride(disabled) {
9
+ if (!disabled)
10
+ return undefined;
11
+ return {
12
+ stroke: disabled.stroke?.stroke,
13
+ strokeWidth: disabled.stroke?.strokeWidth,
14
+ strokeOpacity: disabled.stroke?.strokeOpacity ?? disabled.opacity,
15
+ strokeDasharray: disabled.stroke?.strokeDasharray,
16
+ strokeLinecap: disabled.stroke?.strokeLinecap,
17
+ strokeLinejoin: disabled.stroke?.strokeLinejoin,
18
+ };
19
+ }
20
+ export function disabledDotsOverride(disabled) {
21
+ if (!disabled)
22
+ return undefined;
23
+ return {
24
+ dotRadius: disabled.dots?.dotRadius,
25
+ dotFill: disabled.dots?.dotFill,
26
+ dotFillOpacity: disabled.dots?.dotFillOpacity ?? disabled.opacity,
27
+ dotSymbol: disabled.dots?.dotSymbol,
28
+ dotStroke: disabled.dots?.dotStroke,
29
+ dotStrokeWidth: disabled.dots?.dotStrokeWidth,
30
+ dotStrokeOpacity: disabled.dots?.dotStrokeOpacity ?? disabled.opacity,
31
+ dotStrokeDasharray: disabled.dots?.dotStrokeDasharray,
32
+ };
33
+ }
34
+ export function disabledFillOverride(disabled) {
35
+ if (!disabled)
36
+ return undefined;
37
+ return {
38
+ fill: disabled.fill,
39
+ fillOpacity: disabled.fillOpacity ?? disabled.opacity,
40
+ };
41
+ }
@@ -1,5 +1,20 @@
1
1
  import type { SymbolType, LineStyle } from '../plots/constants';
2
+ import type { StrokeStyle, DotStyle } from '../plots/styling';
2
3
  export type LegendSectionType = 'discrete' | 'continuous';
4
+ /**
5
+ * Style applied to whatever a legend interaction has turned "off" — a
6
+ * toggled-off discrete series, or a datapoint/cell/feature outside an active
7
+ * continuous range filter. `opacity` is a blanket convenience: any
8
+ * `*Opacity` field a consumer doesn't set falls back to it, so
9
+ * `{ opacity: 0.1 }` alone is enough to read as "disabled" everywhere.
10
+ */
11
+ export type LegendDisabledStyle = {
12
+ opacity?: number;
13
+ stroke?: StrokeStyle;
14
+ dots?: DotStyle;
15
+ fill?: string;
16
+ fillOpacity?: number;
17
+ };
3
18
  export type LegendItem = {
4
19
  name: string;
5
20
  label?: string;
@@ -7,12 +22,18 @@ export type LegendItem = {
7
22
  symbol?: SymbolType;
8
23
  opacity?: number;
9
24
  lineStyle?: LineStyle;
25
+ /** Initial toggled-off state (uncontrolled — the user can still click it back on). */
26
+ disabled?: boolean;
10
27
  };
11
28
  export type DiscreteLegendSection = {
12
29
  type: 'discrete';
13
30
  title?: string;
14
31
  items: LegendItem[];
15
32
  columns?: number;
33
+ /** Enables click-to-toggle on each item. */
34
+ interactive?: boolean;
35
+ /** Style applied to a toggled-off item's series. Falls back to `cfg.legend.disabledOpacity`. */
36
+ disabledStyle?: LegendDisabledStyle;
16
37
  };
17
38
  export type ContinuousLegendSection = {
18
39
  type: 'continuous';
@@ -25,5 +46,15 @@ export type ContinuousLegendSection = {
25
46
  ticks?: number[];
26
47
  width?: number;
27
48
  format?: (v: number) => string;
49
+ /** Enables drag handles on the bar's edges to narrow the visible range. */
50
+ interactive?: boolean;
51
+ /**
52
+ * How the color ramp responds to a narrowed range: `'rescale'` reprojects
53
+ * it onto the new bounds, `'filter'` keeps the original domain and only
54
+ * excludes out-of-range values. Defaults to `'filter'`.
55
+ */
56
+ mode?: 'rescale' | 'filter';
57
+ /** Style applied to values outside the active range. Falls back to `cfg.legend.disabledOpacity`. */
58
+ disabledStyle?: LegendDisabledStyle;
28
59
  };
29
60
  export type LegendSection = DiscreteLegendSection | ContinuousLegendSection;
@@ -37,6 +37,8 @@ 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 `disabledStyle`. */
41
+ disabledOpacity: number;
40
42
  };
41
43
  axis: {
42
44
  color: string;
@@ -0,0 +1,26 @@
1
+ import type { SvelteMap } from 'svelte/reactivity';
2
+ import type { LegendDisabledStyle } from '../charts/legend';
3
+ /**
4
+ * Shared legend-interaction state for cross-cutting legend → plot wiring.
5
+ *
6
+ * {@link BaseChart} provides one store via context (mirrors {@link HoverStore});
7
+ * `DiscreteSection`/`ContinuousSection` write to it on click/drag, and every
8
+ * plot kind (line/bar/pyramid/heatmap/geo) reads the same instance to know
9
+ * what to dim. A standalone legend/plot with no provider falls back to its
10
+ * own private store via `createLegendInteraction`.
11
+ */
12
+ export type LegendInteractionStore = {
13
+ /** Legend item name → resolved style to apply. Presence in the map means "disabled". */
14
+ disabledSeries: SvelteMap<string, LegendDisabledStyle>;
15
+ /**
16
+ * The active drag state of the (first) interactive continuous section, or
17
+ * `null` when none is interactive or the user hasn't narrowed it yet (full
18
+ * range — nothing hidden).
19
+ */
20
+ continuousRange: {
21
+ min: number;
22
+ max: number;
23
+ mode: 'rescale' | 'filter';
24
+ disabledStyle: LegendDisabledStyle;
25
+ } | null;
26
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fundar/data-chart-telling",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"