@fundar/data-chart-telling 1.8.1 → 1.9.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## [1.9.0] - 2026-11-03
6
+
7
+ ### Changed
8
+
9
+ - A continuous legend no longer lets its labels touch. Its edge labels (the two ends of the domain) are always written; a tick label is left out, with its mark, when it comes within `legend.continuous.label.overlap.margin` px of an edge label or of a tick before it (weighed from the low end up). Before, a tick near an end was written over it.
10
+ - In the interactive continuous legend, handle labels that meet no longer turn vertical: the label of the handle that rests on the domain's end is hidden, and the moving one stays horizontal and writes both values (`18% – 20%`) until they part. That label follows its handle but is kept inside the legend's width. If both or neither rest on an end, the one moved last stays.
11
+
12
+ - `legend.continuous.label.weight` (`--dct-continuous-label-weight`) styles every label of a continuous legend - the edges, the ticks and the interactive handles - and defaults to `400`. It styled only the handle labels, at `600`, so an interactive legend wrote its values in bold and a plain one did not. Set it to `600` to keep the old look of the handles.
13
+
14
+ ### Removed
15
+
16
+ - `legend.continuous.label.overlap.rotate` and its CSS variable `--dct-continuous-label-overlap-rotate`: there is no vertical label to rotate any more. `overlap.enabled` and `overlap.margin` stay, and now govern both behaviours above.
17
+
5
18
  ## [1.8.1] - 2026-11-02
6
19
 
7
20
  ### Fixed
@@ -140,11 +140,10 @@ export const DEFAULT_CONFIG = {
140
140
  label: {
141
141
  rotate: '0deg',
142
142
  size: '0.875rem',
143
- weight: 600,
143
+ weight: 400,
144
144
  overlap: {
145
145
  enabled: true,
146
- margin: 4,
147
- rotate: '90deg'
146
+ margin: 4
148
147
  }
149
148
  }
150
149
  }
@@ -392,7 +391,6 @@ export function configToCssVars(c) {
392
391
  '--dct-continuous-label-rotate': c.legend.continuous.label.rotate,
393
392
  '--dct-continuous-label-size': c.legend.continuous.label.size,
394
393
  '--dct-continuous-label-weight': String(c.legend.continuous.label.weight),
395
- '--dct-continuous-label-overlap-rotate': c.legend.continuous.label.overlap.rotate,
396
394
  '--dct-tooltip-bg': c.tooltip.background,
397
395
  '--dct-tooltip-color': c.tooltip.color,
398
396
  '--dct-tooltip-size': c.tooltip.fontSize,
@@ -51,6 +51,32 @@
51
51
  }))
52
52
  );
53
53
 
54
+ // ── Labels that would touch ───────────────────────────────────────────────
55
+ // The edge labels (the domain's two ends) are always written. A tick label stays only if it clears them and
56
+ // every tick kept before it, by `overlap.margin` px; ticks are weighed from the low end up.
57
+ const overlapLook = $derived(cfg.legend.continuous.label.overlap);
58
+ let edgeWidths = $state({ start: 0, end: 0 });
59
+ let tickWidths = $state<Record<number, number>>({});
60
+
61
+ const hiddenTicks = $derived.by((): number[] => {
62
+ const hidden: number[] = [];
63
+ if (isInteractive || !overlapLook.enabled) return hidden;
64
+ const margin = overlapLook.margin;
65
+ const taken: [number, number][] = [
66
+ [0, edgeWidths.start],
67
+ [barWidth - edgeWidths.end, barWidth]
68
+ ];
69
+ for (const tick of [...tickPositions].sort((a, b) => a.t - b.t)) {
70
+ const half = (tickWidths[tick.v] ?? 0) / 2;
71
+ const from = trackX(tick.t) - half;
72
+ const to = trackX(tick.t) + half;
73
+ if (taken.some(([start, end]) => from < end + margin && to > start - margin))
74
+ hidden.push(tick.v);
75
+ else taken.push([from, to]);
76
+ }
77
+ return hidden;
78
+ });
79
+
54
80
  // ── Drag-to-narrow-margins ────────────────────────────────────────────────
55
81
  const interaction = LegendState.ensure();
56
82
 
@@ -58,6 +84,9 @@
58
84
  let lo = $state(untrack(() => section.min));
59
85
  let hi = $state(untrack(() => section.max));
60
86
 
87
+ // The handle moved last, which keeps its label when the two labels meet and one has to go.
88
+ let lastDragged = $state<'lo' | 'hi' | null>(null);
89
+
61
90
  // Whether each handle was dragged; an untouched one follows the section's bounds.
62
91
  let loAdjusted = $state(false);
63
92
  let hiAdjusted = $state(false);
@@ -150,6 +179,7 @@
150
179
  event.preventDefault();
151
180
  if (which === 'lo') loAdjusted = true;
152
181
  else hiAdjusted = true;
182
+ lastDragged = which;
153
183
  const target = event.currentTarget as SVGElement;
154
184
  target.setPointerCapture(event.pointerId);
155
185
  const startX = event.clientX;
@@ -190,12 +220,40 @@
190
220
 
191
221
  let loLabelWidth = $state(0);
192
222
  let hiLabelWidth = $state(0);
223
+ let mergedLabelWidth = $state(0);
193
224
 
194
225
  const labelsOverlap = $derived(
195
226
  isInteractive &&
196
- cfg.legend.continuous.label.overlap.enabled &&
197
- (hiT - loT) * trackWidth - loLabelWidth - hiLabelWidth <
198
- cfg.legend.continuous.label.overlap.margin
227
+ overlapLook.enabled &&
228
+ (hiT - loT) * trackWidth - loLabelWidth - hiLabelWidth < overlapLook.margin
229
+ );
230
+
231
+ // When the handle labels meet, the one whose handle still sits on the domain's end gives way and the moving
232
+ // one stays, horizontal, and writes both values ("18% – 20%"). If both or neither sit on an end, the one moved
233
+ // last stays.
234
+ const hiddenHandle = $derived.by((): 'lo' | 'hi' | null => {
235
+ if (!labelsOverlap) return null;
236
+ const loOnEnd = lo <= section.min;
237
+ const hiOnEnd = hi >= section.max;
238
+ if (loOnEnd !== hiOnEnd) return loOnEnd ? 'lo' : 'hi';
239
+ return lastDragged === 'lo' ? 'hi' : 'lo';
240
+ });
241
+ const survivingHandle = $derived(
242
+ hiddenHandle === null ? null : hiddenHandle === 'lo' ? 'hi' : 'lo'
243
+ );
244
+ const mergedLabel = $derived(`${fmt(lo)} – ${fmt(hi)}`);
245
+
246
+ // Where each handle label is anchored: at its handle, except the merged one, which follows its handle but is
247
+ // kept inside the legend's width, growing inward from the end it sits at.
248
+ const loLabelLeft = $derived(
249
+ survivingHandle === 'lo'
250
+ ? Math.max(0, Math.min(trackX(loT), barWidth - mergedLabelWidth))
251
+ : trackX(loT)
252
+ );
253
+ const hiLabelLeft = $derived(
254
+ survivingHandle === 'hi'
255
+ ? Math.min(barWidth, Math.max(trackX(hiT), mergedLabelWidth))
256
+ : trackX(hiT)
199
257
  );
200
258
  </script>
201
259
 
@@ -247,7 +305,9 @@
247
305
  fill="url(#{gradientId})"
248
306
  />
249
307
  {#each tickPositions as tick (tick.v)}
250
- <rect class="dct-continuous__tick-mark" x={trackX(tick.t)} y={barHeight} />
308
+ {#if !hiddenTicks.includes(tick.v)}
309
+ <rect class="dct-continuous__tick-mark" x={trackX(tick.t)} y={barHeight} />
310
+ {/if}
251
311
  {/each}
252
312
 
253
313
  {#if isInteractive}
@@ -328,22 +388,37 @@
328
388
  <span class="dct-continuous__tick-measure" aria-hidden="true" bind:clientWidth={hiLabelWidth}
329
389
  >{fmt(hi)}</span
330
390
  >
391
+ <span
392
+ class="dct-continuous__tick-measure"
393
+ aria-hidden="true"
394
+ bind:clientWidth={mergedLabelWidth}>{mergedLabel}</span
395
+ >
331
396
  <span
332
397
  class="dct-continuous__tick dct-continuous__tick--handle-lo"
333
- class:dct-continuous__tick--vertical={labelsOverlap}
334
- style:left="{trackX(loT)}px">{fmt(lo)}</span
398
+ class:dct-continuous__tick--hidden={hiddenHandle === 'lo'}
399
+ style:left="{loLabelLeft}px">{survivingHandle === 'lo' ? mergedLabel : fmt(lo)}</span
335
400
  >
336
401
  <span
337
402
  class="dct-continuous__tick dct-continuous__tick--handle-hi"
338
- class:dct-continuous__tick--vertical={labelsOverlap}
339
- style:left="{trackX(hiT)}px">{fmt(hi)}</span
403
+ class:dct-continuous__tick--hidden={hiddenHandle === 'hi'}
404
+ style:left="{hiLabelLeft}px">{survivingHandle === 'hi' ? mergedLabel : fmt(hi)}</span
340
405
  >
341
406
  {:else}
342
- <span class="dct-continuous__edge dct-continuous__edge--start">{fmt(section.min)}</span>
407
+ <span
408
+ class="dct-continuous__edge dct-continuous__edge--start"
409
+ bind:clientWidth={edgeWidths.start}>{fmt(section.min)}</span
410
+ >
343
411
  {#each tickPositions as tick (tick.v)}
344
- <span class="dct-continuous__tick" style:left="{trackX(tick.t)}px">{tick.label}</span>
412
+ <span
413
+ class="dct-continuous__tick"
414
+ class:dct-continuous__tick--hidden={hiddenTicks.includes(tick.v)}
415
+ style:left="{trackX(tick.t)}px"
416
+ bind:clientWidth={tickWidths[tick.v]}>{tick.label}</span
417
+ >
345
418
  {/each}
346
- <span class="dct-continuous__edge dct-continuous__edge--end">{fmt(section.max)}</span>
419
+ <span class="dct-continuous__edge dct-continuous__edge--end" bind:clientWidth={edgeWidths.end}
420
+ >{fmt(section.max)}</span
421
+ >
347
422
  {/if}
348
423
  </div>
349
424
  </div>
@@ -358,6 +433,7 @@
358
433
  position: relative;
359
434
  height: 1rem;
360
435
  font-size: var(--dct-continuous-label-size, 0.875rem);
436
+ font-weight: var(--dct-continuous-label-weight, 400);
361
437
  user-select: none;
362
438
  }
363
439
  .dct-continuous__edge {
@@ -376,10 +452,6 @@
376
452
  transform: translateX(-50%) rotate(var(--dct-continuous-label-rotate, 0deg));
377
453
  }
378
454
  /* Anchored outward so the label grows into the bar. */
379
- .dct-continuous__tick--handle-lo,
380
- .dct-continuous__tick--handle-hi {
381
- font-weight: var(--dct-continuous-label-weight, 600);
382
- }
383
455
  .dct-continuous__tick--handle-lo {
384
456
  transform: rotate(var(--dct-continuous-label-rotate, 0deg));
385
457
  }
@@ -392,13 +464,10 @@
392
464
  position: absolute;
393
465
  visibility: hidden;
394
466
  white-space: nowrap;
395
- font-weight: var(--dct-continuous-label-weight, 600);
396
467
  }
397
- /* Handle labels that would collide read top to bottom, pivoting on their anchor. */
398
- .dct-continuous__tick--handle-lo.dct-continuous__tick--vertical,
399
- .dct-continuous__tick--handle-hi.dct-continuous__tick--vertical {
400
- transform-origin: left bottom;
401
- transform: rotate(var(--dct-continuous-label-overlap-rotate, 90deg));
468
+ /* A label that would touch another keeps its place, so its width stays measured, and is not drawn. */
469
+ .dct-continuous__tick--hidden {
470
+ visibility: hidden;
402
471
  }
403
472
  /* Tick mark below the bar at each non-interactive tick's position. */
404
473
  .dct-continuous__tick-mark {
@@ -209,16 +209,14 @@ export type PlotStylesConfig = {
209
209
  rotate: string;
210
210
  /** font-size of the tick/edge/handle labels. */
211
211
  size: string;
212
- /** font-weight of the draggable lo/hi handle labels. */
212
+ /** font-weight of every label: the edges, the ticks and the draggable lo/hi handles. */
213
213
  weight: number | string;
214
- /** 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. */
214
+ /** Labels that would touch are thinned out: a tick label gives way to an edge label and to the ticks before it, and, in the interactive mode, the lo/hi handle label of a handle that rests on the domain's end gives way to the moving one. */
215
215
  overlap: {
216
- /** Whether the flip-to-vertical behavior is active at all; false always keeps both labels horizontal, however close together. */
216
+ /** Whether labels are thinned out at all; false always writes every label, however close together. */
217
217
  enabled: boolean;
218
- /** How many px of horizontal slack the two labels need between them before they're considered overlapping (and flipped). */
218
+ /** How many px of horizontal slack two labels need between them before they are considered touching. */
219
219
  margin: number;
220
- /** 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. */
221
- rotate: string;
222
220
  };
223
221
  };
224
222
  };
package/docs/theming.md CHANGED
@@ -124,35 +124,34 @@ The text blocks map `title`, `subtitle` and `caption` to `--dct-{block}-{font|si
124
124
 
125
125
  ### legend
126
126
 
127
- | CSS variable | Config path |
128
- | --------------------------------------- | ---------------------------------------- |
129
- | `--dct-legend-font` | `legend.font` |
130
- | `--dct-legend-size` | `legend.size` |
131
- | `--dct-legend-weight` | `legend.weight` |
132
- | `--dct-legend-color` | `legend.color` |
133
- | `--dct-legend-title-size` | `legend.titleSize` |
134
- | `--dct-legend-title-weight` | `legend.titleWeight` |
135
- | `--dct-legend-gap` | `legend.gap` |
136
- | `--dct-legend-swatch` | `legend.swatchSize` |
137
- | `--dct-legend-size-gap` | `legend.sizeScale.gap` |
138
- | `--dct-legend-nav-bg` | `legend.navButton.background` |
139
- | `--dct-legend-nav-color` | `legend.navButton.color` |
140
- | `--dct-legend-nav-size` | `legend.navButton.size` |
141
- | `--dct-legend-nav-font-size` | `legend.navButton.fontSize` |
142
- | `--dct-legend-nav-disabled-opacity` | `legend.navButton.disabledOpacity` |
143
- | `--dct-continuous-handle-width` | `legend.continuous.handle.width` |
144
- | `--dct-continuous-handle-height` | `legend.continuous.handle.height` |
145
- | `--dct-continuous-handle-radius` | `legend.continuous.handle.radius` |
146
- | `--dct-continuous-handle-fill` | `legend.continuous.handle.fill` |
147
- | `--dct-continuous-handle-stroke` | `legend.continuous.handle.stroke` |
148
- | `--dct-continuous-handle-stroke-width` | `legend.continuous.handle.strokeWidth` |
149
- | `--dct-continuous-tick-color` | `legend.continuous.tick.color` |
150
- | `--dct-continuous-tick-width` | `legend.continuous.tick.width` |
151
- | `--dct-continuous-tick-length` | `legend.continuous.tick.length` |
152
- | `--dct-continuous-label-rotate` | `legend.continuous.label.rotate` |
153
- | `--dct-continuous-label-size` | `legend.continuous.label.size` |
154
- | `--dct-continuous-label-weight` | `legend.continuous.label.weight` |
155
- | `--dct-continuous-label-overlap-rotate` | `legend.continuous.label.overlap.rotate` |
127
+ | CSS variable | Config path |
128
+ | -------------------------------------- | -------------------------------------- |
129
+ | `--dct-legend-font` | `legend.font` |
130
+ | `--dct-legend-size` | `legend.size` |
131
+ | `--dct-legend-weight` | `legend.weight` |
132
+ | `--dct-legend-color` | `legend.color` |
133
+ | `--dct-legend-title-size` | `legend.titleSize` |
134
+ | `--dct-legend-title-weight` | `legend.titleWeight` |
135
+ | `--dct-legend-gap` | `legend.gap` |
136
+ | `--dct-legend-swatch` | `legend.swatchSize` |
137
+ | `--dct-legend-size-gap` | `legend.sizeScale.gap` |
138
+ | `--dct-legend-nav-bg` | `legend.navButton.background` |
139
+ | `--dct-legend-nav-color` | `legend.navButton.color` |
140
+ | `--dct-legend-nav-size` | `legend.navButton.size` |
141
+ | `--dct-legend-nav-font-size` | `legend.navButton.fontSize` |
142
+ | `--dct-legend-nav-disabled-opacity` | `legend.navButton.disabledOpacity` |
143
+ | `--dct-continuous-handle-width` | `legend.continuous.handle.width` |
144
+ | `--dct-continuous-handle-height` | `legend.continuous.handle.height` |
145
+ | `--dct-continuous-handle-radius` | `legend.continuous.handle.radius` |
146
+ | `--dct-continuous-handle-fill` | `legend.continuous.handle.fill` |
147
+ | `--dct-continuous-handle-stroke` | `legend.continuous.handle.stroke` |
148
+ | `--dct-continuous-handle-stroke-width` | `legend.continuous.handle.strokeWidth` |
149
+ | `--dct-continuous-tick-color` | `legend.continuous.tick.color` |
150
+ | `--dct-continuous-tick-width` | `legend.continuous.tick.width` |
151
+ | `--dct-continuous-tick-length` | `legend.continuous.tick.length` |
152
+ | `--dct-continuous-label-rotate` | `legend.continuous.label.rotate` |
153
+ | `--dct-continuous-label-size` | `legend.continuous.label.size` |
154
+ | `--dct-continuous-label-weight` | `legend.continuous.label.weight` |
156
155
 
157
156
  ### axis
158
157
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fundar/data-chart-telling",
3
- "version": "1.8.1",
3
+ "version": "1.9.0",
4
4
  "description": "Declarative charts, maps and small multiples for Svelte, built on svelteplot.",
5
5
  "license": "MIT",
6
6
  "author": "Ezequiel Puerta",