@fundar/data-chart-telling 0.0.59 → 0.0.61

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/README.md CHANGED
@@ -399,6 +399,193 @@ Omitting `segments` (or passing `[]`) renders every mark in its default palette
399
399
 
400
400
  ---
401
401
 
402
+ ## Theming & configuration
403
+
404
+ The whole package reads its default styling — fonts, colours, spacing, legend/tooltip/axis chrome, plot geometry — from one global, reactive config store (`src/lib/configuration/config.svelte.ts`). Overriding it re-themes every chart on screen; nothing needs to be passed prop-by-prop to each component.
405
+
406
+ ```ts
407
+ import { setConfiguration, getConfiguration, resetConfiguration } from '@fundar/data-chart-telling';
408
+
409
+ setConfiguration({
410
+ font: '"Inter", sans-serif',
411
+ palette: ['#4f46e5', '#e6580d', '#16a34a'],
412
+ legend: { continuous: { label: { size: '0.8125rem', weight: 400 } } }
413
+ });
414
+
415
+ getConfiguration(); // read the live, merged config
416
+ resetConfiguration(); // restore shipped defaults
417
+ ```
418
+
419
+ - `setConfiguration` takes a **deep-partial** patch (`ChartConfigInput`) and deep-merges it into the current config — you only specify the leaves you want to change, everything else keeps its previous value. It's reactive (`$state`-backed), so applying it re-renders every mounted chart.
420
+ - Call it once at app startup (or per-theme-switch); it's global, not per-chart. A single chart can still override specific visuals via its own props (`styles`, `scales`, a section's own `format`/`color`, etc.) — those always win over the global config.
421
+
422
+ ### How values reach the DOM
423
+
424
+ The config feeds two different consumers, and a field is **one or the other**, never both:
425
+
426
+ 1. **CSS custom properties** — `configToCssVars` flattens the relevant fields into `--dct-*` variables, which `ChartContainer` stamps on its own root element; every component's scoped `<style>` block reads them with a hardcoded fallback (e.g. `font-size: var(--dct-continuous-label-size, 0.875rem)`). These are the fields listed with a **CSS variable** below — you can also override them directly with plain CSS on a wrapping element instead of going through `setConfiguration`, if you only need a one-off, non-reactive tweak.
427
+ 2. **Plain JS values** — read directly by plot components as props svelteplot needs (numeric tick sizes, the categorical `palette` array, margins, gradient endpoints, hover geometry, per-mark defaults). These have **no** CSS variable — `setConfiguration` (or editing `DEFAULT_CONFIG`) is the only way to change them.
428
+
429
+ ### Reference
430
+
431
+ All types live in `src/lib/types/configuration/styling.ts`; all defaults in `src/lib/configuration/config.svelte.ts`. "CSS variable" is `—` for JS-only fields.
432
+
433
+ #### Base
434
+
435
+ | Field | Type | Default | CSS variable |
436
+ | ----- | ---- | ------- | ------------- |
437
+ | `font` | `string` | `'inherit'` | `--dct-font` |
438
+ | `color` | `string` | `'currentColor'` | `--dct-fg` |
439
+ | `background` | `string` | `'transparent'` | `--dct-bg` |
440
+ | `border` | `string` | `'color-mix(in srgb, currentColor 15%, transparent)'` | `--dct-border` |
441
+
442
+ #### Spacing
443
+
444
+ | Field | Type | Default | CSS variable |
445
+ | ----- | ---- | ------- | ------------- |
446
+ | `spacing.headerPadding` | `string` | `'0.75rem 0 1rem'` | `--dct-header-pad` |
447
+ | `spacing.legendPadding` | `string` | `'0.75rem 0'` | `--dct-legend-pad` |
448
+ | `spacing.captionPadding` | `string` | `'0.5rem 0'` | `--dct-caption-pad` |
449
+ | `spacing.bleed` | `string` | `'12px'` | `--dct-bleed` |
450
+
451
+ #### Text blocks: `title`, `subtitle`, `caption`
452
+
453
+ Each is a `TextStyle` — `{ font, size, weight, color, leading, spacing }` — mapped to `--dct-{title|subtitle|caption}-{font|size|weight|color|leading|spacing}`. Shared base before per-block overrides: `font: 'inherit'`, `size: '1rem'`, `weight: 400`, `color: 'inherit'`, `leading: '1.3'`, `spacing: 'normal'`.
454
+
455
+ | Block | Overrides its own default |
456
+ | ----- | -------------------------- |
457
+ | `title` | `size: '1.5rem'`, `weight: 500`, `leading: '1.2'` |
458
+ | `subtitle` | `size: '1rem'`, `weight: 400`, `color: 'color-mix(in srgb, currentColor 85%, transparent)'` |
459
+ | `caption` | `size: '0.75rem'`, `color: 'color-mix(in srgb, currentColor 75%, transparent)'` |
460
+
461
+ #### Legend
462
+
463
+ | Field | Type | Default | CSS variable |
464
+ | ----- | ---- | ------- | ------------- |
465
+ | `legend.font` | `string` | `'inherit'` | `--dct-legend-font` |
466
+ | `legend.size` | `string` | `'0.875rem'` | `--dct-legend-size` |
467
+ | `legend.weight` | `number \| string` | `400` | `--dct-legend-weight` |
468
+ | `legend.color` | `string` | `'inherit'` | `--dct-legend-color` |
469
+ | `legend.titleSize` | `string` | `'0.875rem'` | `--dct-legend-title-size` |
470
+ | `legend.titleWeight` | `number \| string` | `500` | `--dct-legend-title-weight` |
471
+ | `legend.gap` | `string` (CSS `gap` shorthand) | `'0.5rem 2rem'` | `--dct-legend-gap` |
472
+ | `legend.swatchSize` | `string` | `'1.25rem'` | `--dct-legend-swatch` |
473
+ | `legend.disabledOpacity` | `number` | `0.12` | — (fallback for a legend-disabled series/value when a section doesn't set its own style) |
474
+
475
+ `legend.navButton` — prev/next arrows on an overflowing horizontal discrete section:
476
+
477
+ | Field | Type | Default | CSS variable |
478
+ | ----- | ---- | ------- | ------------- |
479
+ | `legend.navButton.background` | `string` | `'transparent'` | `--dct-legend-nav-bg` |
480
+ | `legend.navButton.color` | `string` | `'currentColor'` | `--dct-legend-nav-color` |
481
+ | `legend.navButton.size` | `string` | `'1.5rem'` | `--dct-legend-nav-size` |
482
+ | `legend.navButton.fontSize` | `string` | `'1rem'` | `--dct-legend-nav-font-size` |
483
+ | `legend.navButton.disabledOpacity` | `number` | `0.3` | `--dct-legend-nav-disabled-opacity` |
484
+
485
+ `legend.continuous` — the gradient bar's draggable handles, ticks, and labels:
486
+
487
+ | Field | Type | Default | CSS variable |
488
+ | ----- | ---- | ------- | ------------- |
489
+ | `legend.continuous.handle.width` | `string` | `'6px'` | `--dct-continuous-handle-width` |
490
+ | `legend.continuous.handle.height` | `string` | `'20px'` | `--dct-continuous-handle-height` |
491
+ | `legend.continuous.handle.radius` | `string` | `'3px'` | `--dct-continuous-handle-radius` |
492
+ | `legend.continuous.handle.fill` | `string` | `'currentColor'` | `--dct-continuous-handle-fill` |
493
+ | `legend.continuous.handle.stroke` | `string` | `'var(--dct-bg, #fff)'` | `--dct-continuous-handle-stroke` |
494
+ | `legend.continuous.handle.strokeWidth` | `string` | `'1.5px'` | `--dct-continuous-handle-stroke-width` |
495
+ | `legend.continuous.tick.color` | `string` | `'currentColor'` | `--dct-continuous-tick-color` |
496
+ | `legend.continuous.tick.width` | `string` | `'1px'` | `--dct-continuous-tick-width` |
497
+ | `legend.continuous.tick.length` | `string` | `'6px'` | `--dct-continuous-tick-length` |
498
+ | `legend.continuous.label.rotate` | `string` | `'0deg'` | `--dct-continuous-label-rotate` |
499
+ | `legend.continuous.label.size` | `string` | `'0.875rem'` | `--dct-continuous-label-size` |
500
+ | `legend.continuous.label.weight` | `number \| string` | `600` | `--dct-continuous-label-weight` |
501
+
502
+ `legend.continuous.label.overlap` — interactive mode only: once dragging brings the lo/hi handle labels too close to read side by side, both flip to read top-to-bottom instead of clipping or overlapping:
503
+
504
+ | Field | Type | Default | CSS variable |
505
+ | ----- | ---- | ------- | ------------- |
506
+ | `legend.continuous.label.overlap.enabled` | `boolean` | `true` | — (`false` always keeps both labels horizontal, however close together) |
507
+ | `legend.continuous.label.overlap.margin` | `number` (px) | `4` | — (horizontal slack the two labels need before they're considered overlapping) |
508
+ | `legend.continuous.label.overlap.rotate` | `string` | `'90deg'` | `--dct-continuous-label-overlap-rotate` (lo rotates by this; hi by its negation, via `calc(... * -1)`, so both still grow inward toward the bar) |
509
+
510
+ #### Axis
511
+
512
+ Consumed as plain numeric/boolean props by the underlying svelteplot axes — only `color` is also exposed as a CSS variable (used by non-plot axis-adjacent chrome).
513
+
514
+ | Field | Type | Default | CSS variable |
515
+ | ----- | ---- | ------- | ------------- |
516
+ | `axis.color` | `string` | `'currentColor'` | `--dct-axis-color` |
517
+ | `axis.fontSize` | `number` | `14` | — |
518
+ | `axis.labelFontSize` | `number` | `14` | — |
519
+ | `axis.titleFontSize` | `number` | `14` | — |
520
+ | `axis.xTickSize` | `number` | `5` | — |
521
+ | `axis.yTickSize` | `number` | `5` | — |
522
+ | `axis.xTickPadding` | `number` | `10` | — |
523
+ | `axis.yTickPadding` | `number` | `10` | — |
524
+ | `axis.grid` | `boolean` | `false` | — |
525
+
526
+ #### Tooltip
527
+
528
+ | Field | Type | Default | CSS variable |
529
+ | ----- | ---- | ------- | ------------- |
530
+ | `tooltip.background` | `string` | `'#111827'` | `--dct-tooltip-bg` |
531
+ | `tooltip.color` | `string` | `'#ffffff'` | `--dct-tooltip-color` |
532
+ | `tooltip.fontSize` | `string` | `'0.8125rem'` | `--dct-tooltip-size` |
533
+ | `tooltip.padding` | `string` | `'0.5rem 0.75rem'` | `--dct-tooltip-pad` |
534
+ | `tooltip.radius` | `string` | `'0.375rem'` | `--dct-tooltip-radius` |
535
+ | `tooltip.border` | `string` | `'none'` | `--dct-tooltip-border` |
536
+
537
+ #### Plot geometry & colour (all JS-only — no CSS variables)
538
+
539
+ | Field | Type | Default |
540
+ | ----- | ---- | ------- |
541
+ | `margins.top/right/bottom/left` | `number \| 'auto'` | `'auto'` each — lets svelteplot size that side to fit whatever it actually renders there; a number pins it exactly |
542
+ | `palette` | `string[]` | `['#4f46e5', '#e6580d', '#16a34a', '#dc2626', '#0891b2', '#9333ea', '#ca8a04', '#db2777']` — categorical series colours |
543
+ | `continuous.min` / `continuous.max` | `string` | `'#e0e7ff'` / `'#4f46e5'` — heatmap/choropleth gradient endpoints |
544
+ | `continuous.labelColor` | `string` | `'#111827'` — on-cell label colour |
545
+ | `line.strokeWidth` | `number` | `3` |
546
+ | `line.dotRadius` | `number` | `4` |
547
+ | `line.gap.stroke` / `line.gap.dots` | `StrokeStyle` / `DotStyle` | `{ strokeOpacity: 0.6, strokeDasharray: '4 4' }` / `{}` — fallback for `LinePlot`'s `styles.gaps` |
548
+ | `line.valueLabels.elbowGap` | `number` | `10` — shortest leader-line lane distance before it bends |
549
+ | `line.valueLabels.laneStep` | `number` | `7` — extra distance each subsequent lane sits out |
550
+ | `line.valueLabels.labelGap` | `number` | `6` — clearance between the outermost lane and its label |
551
+ | `line.valueLabels.connectorColor` | `string` | `'#94a3b8'` |
552
+ | `line.valueLabels.connectorWidth` | `number` | `0.5` |
553
+ | `bar.gap` | `AreaStyle` | `{ fillOpacity: 0.5 }` — fallback for `BarPlot`'s `styles.gaps` |
554
+ | `bar.seriesPadding` | `number` (0–1) | `0.2` — fallback `scales.z.seriesPadding` for grouped series |
555
+ | `bar.paddingInner` | `number` (0–1) | `0.15` — gap between category groups |
556
+ | `bar.paddingOuter` | `number` (0–1) | `0.15` — gap before the first / after the last group |
557
+ | `pyramid.gap` | `AreaStyle` | `{ fillOpacity: 0.5 }` — fallback for `PyramidPlot`'s `styles.gaps` |
558
+ | `scatter.dotRadius` | `number` | `5` |
559
+ | `timeline.thickness` | `number` | `60` |
560
+ | `hover.rule` | `StrokeStyle` | `{ stroke: '#e6580d', strokeOpacity: 0.35, strokeWidth: 1.5, strokeDasharray: '4 4' }` — hover crosshair |
561
+ | `hover.dot` | `DotStyle` | `{ dotRadius: 5, dotFill: 'currentColor' }` |
562
+ | `hover.font` | `FontStyle` | `{ fill: 'currentColor', fontSize: 12, dy: -18 }` |
563
+ | `hover.area` | `AreaStyle` | `{ fill: '#94a3b8', fillOpacity: 0.25 }` — band highlight on a band scale |
564
+
565
+ `timeline.axisColor` / `timeline.activeColor` are the two exceptions in this group — they *do* have CSS variables (see below), since the timeline gutter is HTML chrome, not an SVG plot mark.
566
+
567
+ | Field | Type | Default | CSS variable |
568
+ | ----- | ---- | ------- | ------------- |
569
+ | `timeline.axisColor` | `string` | `'currentColor'` | `--dct-timeline-axis` |
570
+ | `timeline.activeColor` | `string` | `'#e6580d'` | `--dct-timeline-active` |
571
+
572
+ #### Facet (small multiples)
573
+
574
+ | Field | Type | Default | CSS variable |
575
+ | ----- | ---- | ------- | ------------- |
576
+ | `facet.indicator.background` | `string` | `'#111827'` | `--dct-facet-indicator-bg` |
577
+ | `facet.indicator.color` | `string` | `'#ffffff'` | `--dct-facet-indicator-color` |
578
+ | `facet.indicator.fontSize` | `string` | `'0.8125rem'` | `--dct-facet-indicator-size` |
579
+ | `facet.indicator.padding` | `string` | `'0.125rem 0.5rem'` | `--dct-facet-indicator-pad` |
580
+ | `facet.indicator.radius` | `string` | `'999px'` | `--dct-facet-indicator-radius` |
581
+ | `facet.navButton.background` | `string` | `'#111827'` | `--dct-facet-nav-bg` |
582
+ | `facet.navButton.color` | `string` | `'#ffffff'` | `--dct-facet-nav-color` |
583
+ | `facet.navButton.size` | `string` | `'2rem'` | `--dct-facet-nav-btn-size` |
584
+ | `facet.navButton.fontSize` | `string` | `'1.1rem'` | `--dct-facet-nav-font-size` |
585
+ | `facet.navButton.disabledOpacity` | `number` | `0.3` | `--dct-facet-nav-disabled-opacity` |
586
+
587
+ ---
588
+
402
589
  ## Public API
403
590
 
404
591
  ```ts
@@ -75,7 +75,14 @@ export const DEFAULT_CONFIG = {
75
75
  length: '6px'
76
76
  },
77
77
  label: {
78
- rotate: '0deg'
78
+ rotate: '0deg',
79
+ size: '0.875rem',
80
+ weight: 600,
81
+ overlap: {
82
+ enabled: true,
83
+ margin: 4,
84
+ rotate: '90deg'
85
+ }
79
86
  }
80
87
  }
81
88
  },
@@ -224,6 +231,9 @@ export function configToCssVars(c) {
224
231
  '--dct-continuous-tick-width': c.legend.continuous.tick.width,
225
232
  '--dct-continuous-tick-length': c.legend.continuous.tick.length,
226
233
  '--dct-continuous-label-rotate': c.legend.continuous.label.rotate,
234
+ '--dct-continuous-label-size': c.legend.continuous.label.size,
235
+ '--dct-continuous-label-weight': String(c.legend.continuous.label.weight),
236
+ '--dct-continuous-label-overlap-rotate': c.legend.continuous.label.overlap.rotate,
227
237
  '--dct-axis-color': c.axis.color,
228
238
  '--dct-tooltip-bg': c.tooltip.background,
229
239
  '--dct-tooltip-color': c.tooltip.color,
@@ -73,6 +73,13 @@ export declare const THEMES: {
73
73
  } | undefined;
74
74
  label?: {
75
75
  rotate?: string | undefined;
76
+ size?: string | undefined;
77
+ weight?: string | number | undefined;
78
+ overlap?: {
79
+ enabled?: boolean | undefined;
80
+ margin?: number | undefined;
81
+ rotate?: string | undefined;
82
+ } | undefined;
76
83
  } | undefined;
77
84
  } | undefined;
78
85
  } | undefined;
@@ -302,6 +309,13 @@ export declare const THEMES: {
302
309
  } | undefined;
303
310
  label?: {
304
311
  rotate?: string | undefined;
312
+ size?: string | undefined;
313
+ weight?: string | number | undefined;
314
+ overlap?: {
315
+ enabled?: boolean | undefined;
316
+ margin?: number | undefined;
317
+ rotate?: string | undefined;
318
+ } | undefined;
305
319
  } | undefined;
306
320
  } | undefined;
307
321
  } | undefined;
@@ -531,6 +545,13 @@ export declare const THEMES: {
531
545
  } | undefined;
532
546
  label?: {
533
547
  rotate?: string | undefined;
548
+ size?: string | undefined;
549
+ weight?: string | number | undefined;
550
+ overlap?: {
551
+ enabled?: boolean | undefined;
552
+ margin?: number | undefined;
553
+ rotate?: string | undefined;
554
+ } | undefined;
534
555
  } | undefined;
535
556
  } | undefined;
536
557
  } | undefined;
@@ -760,6 +781,13 @@ export declare const THEMES: {
760
781
  } | undefined;
761
782
  label?: {
762
783
  rotate?: string | undefined;
784
+ size?: string | undefined;
785
+ weight?: string | number | undefined;
786
+ overlap?: {
787
+ enabled?: boolean | undefined;
788
+ margin?: number | undefined;
789
+ rotate?: string | undefined;
790
+ } | undefined;
763
791
  } | undefined;
764
792
  } | undefined;
765
793
  } | undefined;
@@ -166,6 +166,15 @@
166
166
  }
167
167
  };
168
168
  }
169
+
170
+ let loLabelWidth = $state(0);
171
+ let hiLabelWidth = $state(0);
172
+
173
+ const labelsOverlap = $derived(
174
+ isInteractive &&
175
+ cfg.legend.continuous.label.overlap.enabled &&
176
+ (hiT - loT) * barWidth - loLabelWidth - hiLabelWidth < cfg.legend.continuous.label.overlap.margin,
177
+ );
169
178
  </script>
170
179
 
171
180
  <div class="dct-continuous" style:width="{barWidth}px">
@@ -237,8 +246,18 @@
237
246
 
238
247
  <div class="dct-continuous__labels" style:width="{barWidth}px">
239
248
  {#if isInteractive}
240
- <span class="dct-continuous__tick dct-continuous__tick--handle-lo" style:left="{loT * barWidth}px">{fmt(lo)}</span>
241
- <span class="dct-continuous__tick dct-continuous__tick--handle-hi" style:left="{hiT * barWidth}px">{fmt(hi)}</span>
249
+ <span class="dct-continuous__tick-measure" aria-hidden="true" bind:clientWidth={loLabelWidth}>{fmt(lo)}</span>
250
+ <span class="dct-continuous__tick-measure" aria-hidden="true" bind:clientWidth={hiLabelWidth}>{fmt(hi)}</span>
251
+ <span
252
+ class="dct-continuous__tick dct-continuous__tick--handle-lo"
253
+ class:dct-continuous__tick--vertical={labelsOverlap}
254
+ style:left="{loT * barWidth}px"
255
+ >{fmt(lo)}</span>
256
+ <span
257
+ class="dct-continuous__tick dct-continuous__tick--handle-hi"
258
+ class:dct-continuous__tick--vertical={labelsOverlap}
259
+ style:left="{hiT * barWidth}px"
260
+ >{fmt(hi)}</span>
242
261
  {:else}
243
262
  <span class="dct-continuous__edge dct-continuous__edge--start">{fmt(section.min)}</span>
244
263
  {#each tickPositions as tick (tick.v)}
@@ -258,7 +277,7 @@
258
277
  .dct-continuous__labels {
259
278
  position: relative;
260
279
  height: 1rem;
261
- font-size: var(--dct-legend-size, 0.75rem);
280
+ font-size: var(--dct-continuous-label-size, 0.875rem);
262
281
  user-select: none;
263
282
  }
264
283
  .dct-continuous__edge {
@@ -284,7 +303,7 @@
284
303
  */
285
304
  .dct-continuous__tick--handle-lo,
286
305
  .dct-continuous__tick--handle-hi {
287
- font-weight: 600;
306
+ font-weight: var(--dct-continuous-label-weight, 600);
288
307
  }
289
308
  .dct-continuous__tick--handle-lo {
290
309
  transform: rotate(var(--dct-continuous-label-rotate, 0deg));
@@ -292,6 +311,22 @@
292
311
  .dct-continuous__tick--handle-hi {
293
312
  transform: translateX(-100%) rotate(var(--dct-continuous-label-rotate, 0deg));
294
313
  }
314
+ /* Invisible, always-horizontal twin used only to measure a handle label's
315
+ natural width (see `labelsOverlap` above) — never rendered. */
316
+ .dct-continuous__tick-measure {
317
+ position: absolute;
318
+ visibility: hidden;
319
+ white-space: nowrap;
320
+ font-weight: var(--dct-continuous-label-weight, 600);
321
+ }
322
+ .dct-continuous__tick--handle-lo.dct-continuous__tick--vertical {
323
+ transform-origin: left bottom;
324
+ transform: rotate(var(--dct-continuous-label-overlap-rotate, 90deg));
325
+ }
326
+ .dct-continuous__tick--handle-hi.dct-continuous__tick--vertical {
327
+ transform-origin: right bottom;
328
+ transform: rotate(calc(var(--dct-continuous-label-overlap-rotate, 90deg) * -1));
329
+ }
295
330
  /* Tick mark below the bar at each non-interactive tick's position. */
296
331
  .dct-continuous__tick-mark {
297
332
  width: var(--dct-continuous-tick-width, 1px);
@@ -68,6 +68,19 @@ export type ChartConfig = {
68
68
  label: {
69
69
  /** Rotation applied to tick/edge label text. */
70
70
  rotate: string;
71
+ /** font-size of the tick/edge/handle labels. */
72
+ size: string;
73
+ /** font-weight of the draggable lo/hi handle labels. */
74
+ weight: number | string;
75
+ /** Interactive-mode-only: the lo/hi handle labels flip to read top-to-bottom once dragging brings them too close to read side by side. */
76
+ overlap: {
77
+ /** Whether the flip-to-vertical behavior is active at all; false always keeps both labels horizontal, however close together. */
78
+ enabled: boolean;
79
+ /** How many px of horizontal slack the two labels need between them before they're considered overlapping (and flipped). */
80
+ margin: number;
81
+ /** Rotation magnitude applied once overlapping — lo rotates by this amount, hi by its negation, so both still grow inward toward the bar instead of off its edges. */
82
+ rotate: string;
83
+ };
71
84
  };
72
85
  };
73
86
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fundar/data-chart-telling",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"