@fundar/data-chart-telling 1.14.0 → 1.16.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,22 @@
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.16.0] - 2026-11-04
6
+
7
+ ### Fixed
8
+
9
+ - A timeline tweened with `interpolate` is at its last step once `progress` comes within `timeline.snap` (default `1e-4`, a ten-thousandth of the span; `0` turns it off) of it. A scrubber that stops a hair short of `1` - a scrolled step ends at `0.97` - drew the line all the way to the end with its easing, but left out the last row: its dot, and a marker like a `delta` placed on it.
10
+
11
+ ## [1.15.0] - 2026-11-04
12
+
13
+ ### Fixed
14
+
15
+ - A bar plot's value labels follow the bars that go below zero (or left of it): `'outside'` puts a label beyond its bar's tip on that side - to the left of a horizontal bar, under a vertical one - and the plot's margins make room for it. On a horizontal bar `'start'` is its left end and `'end'` its right one, both inside, and `'middle'` is its centre (it sat on the tip); a vertical bar's anchors are as they were.
16
+
17
+ ### Changed
18
+
19
+ - On a line, the piece that joins two runs of a series is drawn as the first area segment that covers both of its ends - so a dashed projection that starts at the last observed year dashes the piece leaving that year, even where a faded range that ends there owns the point. It was always the catch-all's stroke, which left a solid piece at the start of the projection. A piece no area covers both ends of keeps the catch-all's stroke.
20
+
5
21
  ## [1.14.0] - 2026-11-04
6
22
 
7
23
  ### Added
@@ -184,6 +184,7 @@ export const DEFAULT_CONFIG = {
184
184
  activeColor: '#4e79a7',
185
185
  thickness: 60,
186
186
  gap: '12px',
187
+ snap: 1e-4,
187
188
  rule: {
188
189
  fontSize: 12,
189
190
  activeFontSize: 20,
@@ -59,8 +59,11 @@ export declare class ValueLabels {
59
59
  x?: number;
60
60
  y?: number;
61
61
  }, edges: MarginSide[]): EdgeBox;
62
- /** Each series' value farthest from zero, read on `valueAxis` - the widest label it can show. */
63
- extremes<S extends Series<any>>(series: S[], valueAxis?: 'x' | 'y'): {
62
+ /**
63
+ * Each series' value farthest from zero, read on `valueAxis` - the widest label it can show. With `'both'`,
64
+ * the farthest above zero and the farthest below it, whichever the series has.
65
+ */
66
+ extremes<S extends Series<any>>(series: S[], valueAxis?: 'x' | 'y', sides?: 'farthest' | 'both'): {
64
67
  series: S;
65
68
  value: number;
66
69
  }[];
@@ -88,21 +88,36 @@ export class ValueLabels {
88
88
  const box = this.textBox(text).beside({ x: at.x ?? 0, y: at.y ?? 0 }, side);
89
89
  return { box, edges };
90
90
  }
91
- /** Each series' value farthest from zero, read on `valueAxis` - the widest label it can show. */
91
+ /**
92
+ * Each series' value farthest from zero, read on `valueAxis` - the widest label it can show. With `'both'`,
93
+ * the farthest above zero and the farthest below it, whichever the series has.
94
+ */
92
95
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
- extremes(series, valueAxis = 'y') {
96
+ extremes(series, valueAxis = 'y', sides = 'farthest') {
94
97
  return series.flatMap((s) => {
95
98
  const accessor = s[valueAxis];
96
99
  if (accessor === undefined)
97
100
  return [];
98
101
  const y = resolveAccessor(accessor);
99
102
  let extreme;
103
+ let lowest;
104
+ let highest;
100
105
  for (const d of s.data) {
101
106
  const v = Number(y(d));
102
- if (!Number.isNaN(v) && (extreme === undefined || Math.abs(v) > Math.abs(extreme)))
107
+ if (Number.isNaN(v))
108
+ continue;
109
+ if (extreme === undefined || Math.abs(v) > Math.abs(extreme))
103
110
  extreme = v;
111
+ if (v < 0 && (lowest === undefined || v < lowest))
112
+ lowest = v;
113
+ if (v >= 0 && (highest === undefined || v > highest))
114
+ highest = v;
104
115
  }
105
- return extreme === undefined ? [] : [{ series: s, value: extreme }];
116
+ if (extreme === undefined)
117
+ return [];
118
+ if (sides === 'farthest')
119
+ return [{ series: s, value: extreme }];
120
+ return [lowest, highest].flatMap((value) => value === undefined ? [] : [{ series: s, value }]);
106
121
  });
107
122
  }
108
123
  /**
@@ -2,6 +2,7 @@
2
2
  import { untrack } from 'svelte';
3
3
  import { resolveAccessor } from '../../utils/accessors';
4
4
  import TimelineFrame from './TimelineFrame.svelte';
5
+ import { getConfiguration } from '../../configuration/config.svelte';
5
6
  import { TimelineSteps } from './TimelineSteps';
6
7
  import { TimelineTime } from './TimelineTime';
7
8
  import { Interpolation } from '../../utils/Interpolation';
@@ -82,7 +83,9 @@
82
83
  const segmentEasingFn = $derived(Interpolation.buildEasing(interpolate?.segmentEasing));
83
84
 
84
85
  /** Continuous scrub position across the full step domain - only meaningful when `interpolate` is set. */
85
- const currentTime = $derived(steps.timeAt(progress, progressEasingFn));
86
+ const currentTime = $derived(
87
+ steps.timeAt(progress, progressEasingFn, getConfiguration().timeline.snap)
88
+ );
86
89
 
87
90
  const selectedValue = $derived(
88
91
  (interpolate ? steps.nearest(currentTime) : steps.at(progress)) ?? 0
@@ -17,8 +17,12 @@ export declare class TimelineSteps {
17
17
  get domain(): TimeValue[];
18
18
  /** The step a scrubber `progress` snaps to, or `undefined` without steps. */
19
19
  at(progress: number): TimeValue | undefined;
20
- /** The continuous time `progress` is at across the steps, eased - `0` without steps. */
21
- timeAt(progress: number, easing: EasingFn): number;
20
+ /**
21
+ * The continuous time `progress` is at across the steps, eased - `0` without steps. Within `snap` (a fraction
22
+ * of the span) of the last step it is at it: a scrubber that stops a hair short of `1` (a scrolled step's
23
+ * last position, say) still shows everything, with an eased tail that long gone to the end.
24
+ */
25
+ timeAt(progress: number, easing: EasingFn, snap?: number): number;
22
26
  /** The step closest to `time`, or `undefined` without steps. */
23
27
  nearest(time: number): TimeValue | undefined;
24
28
  }
@@ -34,12 +34,18 @@ export class TimelineSteps {
34
34
  const index = Math.round(progress * (values.length - 1));
35
35
  return values[Math.min(values.length - 1, Math.max(0, index))];
36
36
  }
37
- /** The continuous time `progress` is at across the steps, eased - `0` without steps. */
38
- timeAt(progress, easing) {
37
+ /**
38
+ * The continuous time `progress` is at across the steps, eased - `0` without steps. Within `snap` (a fraction
39
+ * of the span) of the last step it is at it: a scrubber that stops a hair short of `1` (a scrolled step's
40
+ * last position, say) still shows everything, with an eased tail that long gone to the end.
41
+ */
42
+ timeAt(progress, easing, snap = 0) {
39
43
  const { first, last } = this;
40
44
  if (first === undefined || last === undefined)
41
45
  return 0;
42
- return first + (last - first) * easing(progress);
46
+ const span = last - first;
47
+ const time = first + span * easing(progress);
48
+ return last - time < span * snap ? last : time;
43
49
  }
44
50
  /** The step closest to `time`, or `undefined` without steps. */
45
51
  nearest(time) {
@@ -68,15 +68,25 @@ export function buildBarDefaults() {
68
68
  if (ProcessedPositions.span(state.processedData, axis).stacked)
69
69
  return [];
70
70
  if (!state.axes?.isHorizontal) {
71
- const at = { y: -labels.offset('y') };
72
- return [labels.edgeBox('0', { x: 'middle', y: 'before' }, at, ['top'])];
71
+ const offset = labels.offset('y');
72
+ const boxes = [
73
+ labels.edgeBox('0', { x: 'middle', y: 'before' }, { y: -offset }, ['top'])
74
+ ];
75
+ // A bar below zero has its label under its tip.
76
+ const hasNegative = labels
77
+ .extremes(state.resolvedSeries ?? [], PlotAxes.valueOf(state), 'both')
78
+ .some(({ value }) => value < 0);
79
+ if (hasNegative)
80
+ boxes.push(labels.edgeBox('0', { x: 'middle', y: 'after' }, { y: offset }, ['bottom']));
81
+ return boxes;
73
82
  }
74
- const at = { x: labels.offset('x') };
83
+ // A bar below zero reaches out to the left, with its label turned that way.
84
+ const offset = labels.offset('x');
75
85
  return labels
76
- .extremes(state.resolvedSeries ?? [], PlotAxes.valueOf(state))
77
- .map(({ series, value }) => labels.edgeBox(labels.format(value, series.name), { x: 'after', y: 'middle' }, at, [
78
- 'right'
79
- ]));
86
+ .extremes(state.resolvedSeries ?? [], PlotAxes.valueOf(state), 'both')
87
+ .map(({ series, value }) => value < 0
88
+ ? labels.edgeBox(labels.format(value, series.name), { x: 'before', y: 'middle' }, { x: -offset }, ['left'])
89
+ : labels.edgeBox(labels.format(value, series.name), { x: 'after', y: 'middle' }, { x: offset }, ['right']));
80
90
  }
81
91
  }
82
92
  },
@@ -82,60 +82,59 @@ export function buildValueLabels(labels, state, ctx) {
82
82
  const geometry = buildBarGeometry(state, ctx.cfg.bar.seriesPadding);
83
83
  const { font, anchor, format } = labels;
84
84
  const valueOf = ValueLabels.valueOf(state);
85
- const top = (d) => geometry.spanOf(d)?.[1] ?? 0;
86
- const position = geometry.stacked
87
- ? (d) => {
88
- const [from, to] = geometry.spanOf(d) ?? [0, 0];
89
- return (from + to) / 2;
90
- }
91
- : anchor === 'middle' && !isHorizontal
92
- ? (d) => top(d) / 2
93
- : anchor === 'end'
94
- ? () => 0
95
- : top;
96
- // Outward from the bar's top along the value axis: up for a vertical bar, right for a horizontal one.
97
- const outward = anchor === 'outside' ? 6 : anchor === 'start' ? -4 : anchor === 'end' ? 4 : 0;
98
- const textAnchor = labels.textAnchor ??
99
- (isHorizontal
100
- ? anchor === 'outside' || anchor === 'end'
101
- ? 'start'
102
- : anchor === 'start'
103
- ? 'end'
104
- : 'middle'
105
- : 'middle');
106
- const lineAnchor = font?.lineAnchor ??
107
- (isHorizontal
108
- ? 'middle'
109
- : anchor === 'outside' || anchor === 'end'
110
- ? 'bottom'
111
- : anchor === 'start'
112
- ? 'top'
113
- : 'middle');
85
+ const spanOf = (d) => geometry.spanOf(d) ?? [0, 0];
86
+ const lowOf = (d) => Math.min(...spanOf(d));
87
+ const highOf = (d) => Math.max(...spanOf(d));
88
+ const below = (d) => Number(valueOf(d)) < 0;
89
+ const place = (negative) => {
90
+ const mid = (d) => (lowOf(d) + highOf(d)) / 2;
91
+ if (geometry.stacked)
92
+ return { at: mid, side: 0 };
93
+ if (anchor === 'middle')
94
+ return { at: mid, side: 0 };
95
+ if (anchor === 'outside')
96
+ return isHorizontal
97
+ ? { at: negative ? lowOf : highOf, side: negative ? -1 : 1 }
98
+ : { at: negative ? lowOf : highOf, side: negative ? 1 : -1 };
99
+ if (anchor === 'start')
100
+ return isHorizontal ? { at: lowOf, side: 1 } : { at: highOf, side: 1 };
101
+ return isHorizontal ? { at: highOf, side: -1 } : { at: lowOf, side: -1 };
102
+ };
103
+ const gap = anchor === 'outside' ? 6 : 4;
114
104
  const labelled = processedData.data.filter((d) => d[GAP_VALUE] === undefined && geometry.spanOf(d) !== undefined);
115
105
  return (state.resolvedSeries ?? []).flatMap((s) => {
116
- const data = labelled.filter((d) => d[SERIES_ID_KEY] === seriesIdOf(s));
117
- if (data.length === 0)
118
- return [];
119
- return [
120
- {
121
- type: 'text',
122
- data,
123
- ...(isHorizontal
124
- ? { x: position, y: geometry.categoryOf }
125
- : { x: geometry.categoryOf, y: position }),
126
- bandSlice: geometry.sliceOf(s.name),
127
- text: (d) => format(Number(valueOf(d)), s.name, d),
128
- styles: {
129
- ...font,
130
- fill: font?.fill ?? ctx.cfg.axis.color,
131
- fontSize: font?.fontSize ?? 12,
132
- dx: font?.dx ?? (isHorizontal ? outward : 0),
133
- dy: font?.dy ?? (isHorizontal ? 0 : -outward),
134
- textAnchor,
135
- lineAnchor
106
+ const own = labelled.filter((d) => d[SERIES_ID_KEY] === seriesIdOf(s));
107
+ // Only a label beyond the tip turns with the sign of its value.
108
+ const turns = !geometry.stacked && anchor === 'outside';
109
+ return (turns ? [false, true] : [false]).flatMap((negative) => {
110
+ const data = turns ? own.filter((d) => below(d) === negative) : own;
111
+ if (data.length === 0)
112
+ return [];
113
+ const { at, side } = place(negative);
114
+ // A horizontal label reads away from its point to `side`; a vertical one sits on the far side of it.
115
+ const textAnchor = labels.textAnchor ??
116
+ (isHorizontal ? (side === 1 ? 'start' : side === -1 ? 'end' : 'middle') : 'middle');
117
+ const lineAnchor = font?.lineAnchor ??
118
+ (isHorizontal ? 'middle' : side === 1 ? 'top' : side === -1 ? 'bottom' : 'middle');
119
+ return [
120
+ {
121
+ type: 'text',
122
+ data,
123
+ ...(isHorizontal ? { x: at, y: geometry.categoryOf } : { x: geometry.categoryOf, y: at }),
124
+ bandSlice: geometry.sliceOf(s.name),
125
+ text: (d) => format(Number(valueOf(d)), s.name, d),
126
+ styles: {
127
+ ...font,
128
+ fill: font?.fill ?? ctx.cfg.axis.color,
129
+ fontSize: font?.fontSize ?? 12,
130
+ dx: font?.dx ?? (isHorizontal ? side * gap : 0),
131
+ dy: font?.dy ?? (isHorizontal ? 0 : side * gap),
132
+ textAnchor,
133
+ lineAnchor
134
+ }
136
135
  }
137
- }
138
- ];
136
+ ];
137
+ });
139
138
  });
140
139
  }
141
140
  /**
@@ -128,11 +128,34 @@ function resolveSeriesSegments(series, segmentsOfSeries, connect, base) {
128
128
  // the default palette color when the user has set a baseline via catch-all.
129
129
  // Dots are excluded to avoid duplicate marks at the shared boundary points.
130
130
  const catchAll = segs.find((s) => !s.areas || s.areas.length === 0);
131
- const connectorStyle = (() => {
132
- const rest = { ...(catchAll?.styles ?? baseStyle) };
131
+ const withoutDots = (style) => {
132
+ const rest = { ...style };
133
133
  delete rest.dot;
134
134
  return rest;
135
- })();
135
+ };
136
+ const connectorStyle = withoutDots((catchAll?.styles ?? baseStyle));
137
+ // A segment's style as its own run is drawn with: an area segment over the catch-all's.
138
+ const styleOf = (idx) => {
139
+ const seg = segs[idx];
140
+ const own = seg.styles ?? {};
141
+ const isAreaSpecific = seg.areas != null && seg.areas.length > 0;
142
+ return isAreaSpecific && catchAll?.styles
143
+ ? merge(catchAll.styles, own)
144
+ : own;
145
+ };
146
+ // The line that joins two runs is drawn as the first area segment that covers both of its ends - a dashed
147
+ // projection that starts at the last observed year also dashes the piece leaving it, even where a faded
148
+ // range that ends there owns that year's point. Where none does, it is the catch-all's.
149
+ const covers = (idx, row) => {
150
+ const areas = segs[idx].areas;
151
+ return (areas != null &&
152
+ areas.length > 0 &&
153
+ areas.some((area) => areaMatchesXY(getX(row), getY(row), area)));
154
+ };
155
+ const joining = (from, to) => {
156
+ const covering = evalOrder.find(({ i }) => covers(i, from) && covers(i, to));
157
+ return covering ? withoutDots(styleOf(covering.i)) : connectorStyle;
158
+ };
136
159
  const defaultVisual = [];
137
160
  const connectors = [];
138
161
  const userVisual = [];
@@ -161,7 +184,7 @@ function resolveSeriesSegments(series, segmentsOfSeries, connect, base) {
161
184
  if (r < runs.length - 1) {
162
185
  connectors.push({
163
186
  data: [last, runs[r + 1].data[0]],
164
- styles: connectorStyle,
187
+ styles: joining(last, runs[r + 1].data[0]),
165
188
  scales: undefined
166
189
  });
167
190
  }
@@ -188,7 +211,7 @@ function resolveSeriesSegments(series, segmentsOfSeries, connect, base) {
188
211
  if (connect && nextRun && nextRun.segIdx !== -1) {
189
212
  connectors.push({
190
213
  data: [run.data[run.data.length - 1], nextRun.data[0]],
191
- styles: connectorStyle,
214
+ styles: joining(run.data[run.data.length - 1], nextRun.data[0]),
192
215
  scales: undefined
193
216
  });
194
217
  }
@@ -271,6 +271,12 @@ export type PlotStylesConfig = {
271
271
  thickness: number;
272
272
  /** Space between a plot (or a composition's panels) and its timeline, as a CSS length. */
273
273
  gap: string;
274
+ /**
275
+ * How close to the first or last step a tweened timeline counts as being at it, as a fraction of its span.
276
+ * A scrubber that stops a hair short of `0` or `1` - a scrolled step ends at `0.97`, whose eased time is a
277
+ * hair short of the end - still shows the last row, and a marker placed on it. `0` turns it off.
278
+ */
279
+ snap: number;
274
280
  /** The `'rule'` timeline beside a plot: a line with its steps written along it. */
275
281
  rule: {
276
282
  /** Font size of the steps, in px. */
@@ -8,6 +8,11 @@
8
8
  * of a pyramid bar from zero, right of a line's last point).
9
9
  * - `'middle'` - centred on the mark.
10
10
  * - `'end'` - at the baseline/zero end of the mark.
11
+ *
12
+ * On a bar plot the anchors read as the bars are drawn, whatever the sign of the value: `'outside'` is
13
+ * beyond the tip (left of a horizontal bar below zero, under a vertical one), `'start'` the left end of a
14
+ * horizontal bar or the top end of a vertical one, `'end'` the right or bottom end, both inside, and
15
+ * `'middle'` the centre.
11
16
  */
12
17
  export type ValueAnchor = 'outside' | 'start' | 'middle' | 'end';
13
18
  /**
@@ -46,18 +46,18 @@
46
46
 
47
47
  ![End-of-line value labels](images/value-labels.png)
48
48
 
49
- | Field | Meaning |
50
- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
- | `show` | Draws the labels. Off by default, except on a heatmap |
52
- | `anchor` | `'outside'`, `'start'`, `'middle'`, `'end'` - where a label sits on its mark |
53
- | `select` | Which row of each group is labelled: `'first'`, `'last'`, `'min'`, `'max'`, or a list of them. A group is a series on a line or area plot (default `'last'`), a category on the plots that join dots (default: every dot) |
54
- | `series` | Labels only these series, by name |
55
- | `color` | `'series'` writes each label in its series' color; any other CSS color writes them all in it (`fontStyle.fill` wins) |
56
- | `decimals`, `prefix`, `suffix`, `signDisplay` | How the value reads, over `numbers.decimals` ([Number format](theming.md#number-format)); `format` replaces them |
57
- | `format` | `(value, seriesName, row) => string`; `row` is the labelled row, for its other fields |
58
- | `hoverIsolate` | Hovering a label dims every other series |
59
- | `fontStyle` | Text style: `fontSize`, `fill`, `dx`, `dy`, `textAnchor`, `lineAnchor`, `rotate`, `class`, ... Overrides the anchor's placement field by field |
60
- | `strokeStyle` | Style of the connector lines drawn from labels that had to move apart |
49
+ | Field | Meaning |
50
+ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
51
+ | `show` | Draws the labels. Off by default, except on a heatmap |
52
+ | `anchor` | `'outside'`, `'start'`, `'middle'`, `'end'` - where a label sits on its mark. On a bar, `'outside'` is beyond its tip (on either side of zero), `'start'` / `'end'` its left / right end (top / bottom on a vertical bar), inside, and `'middle'` its centre |
53
+ | `select` | Which row of each group is labelled: `'first'`, `'last'`, `'min'`, `'max'`, or a list of them. A group is a series on a line or area plot (default `'last'`), a category on the plots that join dots (default: every dot) |
54
+ | `series` | Labels only these series, by name |
55
+ | `color` | `'series'` writes each label in its series' color; any other CSS color writes them all in it (`fontStyle.fill` wins) |
56
+ | `decimals`, `prefix`, `suffix`, `signDisplay` | How the value reads, over `numbers.decimals` ([Number format](theming.md#number-format)); `format` replaces them |
57
+ | `format` | `(value, seriesName, row) => string`; `row` is the labelled row, for its other fields |
58
+ | `hoverIsolate` | Hovering a label dims every other series |
59
+ | `fontStyle` | Text style: `fontSize`, `fill`, `dx`, `dy`, `textAnchor`, `lineAnchor`, `rotate`, `class`, ... Overrides the anchor's placement field by field |
60
+ | `strokeStyle` | Style of the connector lines drawn from labels that had to move apart |
61
61
 
62
62
  Where the labels go depends on the plot: the end of each series on a line or area plot, one per bar on a bar, pyramid or histogram, one per point on a scatter, one per cell on a heatmap. On lines, overlapping labels spread apart, share one column and are joined to their points by elbow connectors; the right margin grows to fit them when it is `'auto'`. The spread is tuned by `text.declutter` in the [configuration](theming.md).
63
63
 
@@ -148,6 +148,7 @@ type Range = { from?: AxisValue; to?: AxisValue }; // inclusive on both ends
148
148
  - A segment applies to a data point (or heatmap cell) inside **any** of its `areas`.
149
149
  - An `Area` limits by `x`, `y` or both; leaving one out leaves that axis open.
150
150
  - A segment without `areas` is a **catch-all**: it matches every point, is evaluated last, and is overridden by any area-specific segment. A catch-all placed first is a baseline that highlighted ranges override.
151
+ - On a line, the piece that joins two runs of a series is drawn as the first area segment that covers both of its ends (its stroke, not its dots): a dashed range that starts at a year dashes the piece leaving it, even where a faded range ending there owns that year's point. One no area covers both ends of is drawn as the catch-all.
151
152
  - `'default'` applies to every series; a named key applies to that series only, and wins over `'default'` where both match.
152
153
  - `legend` names the segment on the automatic [legend](legend.md).
153
154
  - `facet` limits the segment to the [facet panels](facets.md#markers-and-segments-in-some-panels) it names - a `by` value or a list of them. Without it the segment applies to every panel.
package/docs/theming.md CHANGED
@@ -49,7 +49,7 @@ resetConfiguration(); // back to the shipped defaults
49
49
  | `connector` | What `styles.connector` falls back to on lollipop, dumbbell, Cleveland and forest plots: `sign` (`positive`, `negative`, `zero` colors), `lengthScheme` and `differenceScheme` (color ramps), `arrow` (`width`, `headLength`, `headAngle` of a connector drawn as an arrow), `outsideOpacity` (the part of a connector outside its `from`-`to` stretch), `capLength` (px, of a forest plot's end caps), `labelFontSize` and `labelGap` |
50
50
  | `hover` | Look of the hover marker: `rule`, `dot`, `font`, `area` |
51
51
  | `delta` | The `delta` marker's label, per axis the delta is taken on: `text.x` (beside a vertical delta: `dx: 12`, `textAnchor: 'start'`) and `text.y` (above a horizontal one: `dy: -10`, `textAnchor: 'middle'`). `lineAnchor` picks where along the delta the label sits and how it aligns there |
52
- | `timeline` | The rule (`rule`) and the player bar (`player`: controls, speeds, `stepDuration`, button, track, thumb, toggle) |
52
+ | `timeline` | The rule (`rule`) and the player bar (`player`: controls, speeds, `stepDuration`, button, track, thumb, toggle); `snap` is how close to its last step (a fraction of the span, `1e-4` by default) a tweened timeline counts as being at it |
53
53
  | `facet` | The position badge and prev/next buttons |
54
54
  | `frameClip` | Tolerance for clipping marks to the plot frame |
55
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fundar/data-chart-telling",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "Declarative charts, maps and small multiples for Svelte, built on svelteplot.",
5
5
  "license": "MIT",
6
6
  "author": "Ezequiel Puerta",