@fundar/data-chart-telling 1.0.0 → 1.1.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.
Files changed (155) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +57 -18
  3. package/dist/configuration/config.svelte.js +13 -1
  4. package/dist/configuration/themes/index.d.ts +124 -0
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +1 -0
  7. package/dist/layout/base/BasePlotLayout.svelte +18 -5
  8. package/dist/layout/hover/HoverSettings.d.ts +20 -6
  9. package/dist/layout/hover/HoverSettings.js +29 -4
  10. package/dist/layout/legend/LegendState.svelte.d.ts +5 -0
  11. package/dist/layout/legend/LegendState.svelte.js +4 -0
  12. package/dist/layout/plot/Plot.svelte +6 -3
  13. package/dist/layout/plot/PublicPlotContext.d.ts +5 -4
  14. package/dist/layout/plot/PublicPlotContext.js +7 -3
  15. package/dist/layout/plot/Slice.js +4 -2
  16. package/dist/layout/scales/EdgeTicks.d.ts +39 -0
  17. package/dist/layout/scales/EdgeTicks.js +82 -0
  18. package/dist/layout/scales/NiceTicks.d.ts +40 -0
  19. package/dist/layout/scales/NiceTicks.js +72 -0
  20. package/dist/layout/scales/ScalesLayout.svelte +64 -30
  21. package/dist/layout/scales/ScalesLayout.svelte.d.ts +5 -0
  22. package/dist/layout/scales/SveltePlotScale.d.ts +15 -0
  23. package/dist/layout/scales/SveltePlotScale.js +79 -0
  24. package/dist/layout/styles/ValueLabels.js +3 -2
  25. package/dist/layout/timeline/Timeline.svelte +5 -2
  26. package/dist/layout/timeline/Timeline.svelte.d.ts +2 -0
  27. package/dist/layout/timeline/TimelineLayout.svelte +38 -12
  28. package/dist/layout/timeline/TimelineLayout.svelte.d.ts +7 -5
  29. package/dist/layout/timeline/TimelineRule.svelte +1 -0
  30. package/dist/layout/timeline/TimelineState.svelte.d.ts +13 -5
  31. package/dist/layout/timeline/TimelineState.svelte.js +42 -8
  32. package/dist/layout/timeline/TimelineTime.d.ts +18 -0
  33. package/dist/layout/timeline/TimelineTime.js +117 -0
  34. package/dist/layout/tooltip/Tooltip.svelte +6 -3
  35. package/dist/layout/tooltip/Tooltip.svelte.d.ts +2 -0
  36. package/dist/layout/tooltip/TooltipState.svelte.d.ts +8 -2
  37. package/dist/layout/tooltip/TooltipState.svelte.js +22 -3
  38. package/dist/markers/delta/DeltaMarker.svelte +9 -7
  39. package/dist/phases/Pipeline.js +3 -2
  40. package/dist/phases/data/DataPhase.d.ts +5 -11
  41. package/dist/phases/data/DataPhase.js +11 -8
  42. package/dist/phases/facet/FacetPanels.d.ts +54 -0
  43. package/dist/phases/facet/FacetPanels.js +95 -0
  44. package/dist/phases/facet/FacetPhase.d.ts +1 -42
  45. package/dist/phases/facet/FacetPhase.js +1 -87
  46. package/dist/phases/hover/HoverPhase.d.ts +6 -43
  47. package/dist/phases/hover/HoverPhase.js +19 -54
  48. package/dist/phases/mapping/MappingPhase.d.ts +1 -1
  49. package/dist/phases/mapping/MappingPhase.js +2 -4
  50. package/dist/phases/scales/ScalesPhase.d.ts +1 -3
  51. package/dist/phases/scales/ScalesPhase.js +17 -21
  52. package/dist/phases/sharedScales/SharedScalesPhase.d.ts +4 -1
  53. package/dist/phases/sharedScales/SharedScalesPhase.js +10 -7
  54. package/dist/phases/statistics/StatisticsPhase.js +3 -4
  55. package/dist/phases/tooltip/TooltipPhase.d.ts +4 -2
  56. package/dist/phases/tooltip/TooltipPhase.js +10 -3
  57. package/dist/phases/transforms/ProcessedPositions.d.ts +30 -0
  58. package/dist/phases/transforms/ProcessedPositions.js +74 -0
  59. package/dist/phases/transforms/TransformsPhase.d.ts +5 -42
  60. package/dist/phases/transforms/TransformsPhase.js +20 -57
  61. package/dist/plots/area/markers.js +1 -2
  62. package/dist/plots/bar/defaults.js +6 -10
  63. package/dist/plots/bar/markers.js +5 -4
  64. package/dist/plots/box/BoxSummaries.d.ts +19 -0
  65. package/dist/plots/box/BoxSummaries.js +101 -0
  66. package/dist/plots/box/Plot.svelte +42 -0
  67. package/dist/plots/box/Plot.svelte.d.ts +26 -0
  68. package/dist/plots/box/defaults.d.ts +12 -0
  69. package/dist/plots/box/defaults.js +34 -0
  70. package/dist/plots/box/markers.d.ts +12 -0
  71. package/dist/plots/box/markers.js +34 -0
  72. package/dist/plots/geo/phases/hover.js +5 -5
  73. package/dist/plots/geo/phases/markers.js +3 -4
  74. package/dist/plots/geo/slices.js +6 -3
  75. package/dist/plots/heatmap/markers.js +2 -1
  76. package/dist/plots/histogram/defaults.js +1 -1
  77. package/dist/plots/histogram/markers.js +4 -4
  78. package/dist/plots/line/markers.js +1 -2
  79. package/dist/plots/pyramid/defaults.js +3 -2
  80. package/dist/plots/pyramid/markers.js +3 -6
  81. package/dist/plots/scatter/defaults.js +3 -2
  82. package/dist/plots/scatter/markers.js +4 -3
  83. package/dist/types/configuration/styling.d.ts +24 -2
  84. package/dist/types/layout/composer.d.ts +5 -2
  85. package/dist/types/layout/scales.d.ts +16 -0
  86. package/dist/types/layout/timeline.d.ts +14 -5
  87. package/dist/types/phases/hover/phase.d.ts +6 -0
  88. package/dist/types/phases/state.d.ts +5 -2
  89. package/dist/types/phases/statistics/phase.d.ts +3 -1
  90. package/dist/types/phases/transforms/phase.d.ts +10 -0
  91. package/dist/types/plots/box/props.d.ts +10 -0
  92. package/dist/types/plots/box/props.js +1 -0
  93. package/dist/types/plots/box/state.d.ts +23 -0
  94. package/dist/types/plots/box/state.js +1 -0
  95. package/dist/types/plots/props.d.ts +2 -0
  96. package/docs/chrome.md +39 -8
  97. package/docs/composer.md +19 -3
  98. package/docs/concepts.md +1 -1
  99. package/docs/development.md +14 -5
  100. package/docs/facets.md +62 -8
  101. package/docs/getting-started.md +8 -9
  102. package/docs/legend.md +62 -15
  103. package/docs/markers.md +17 -6
  104. package/docs/plots.md +168 -42
  105. package/docs/scales.md +36 -19
  106. package/docs/state.md +1 -1
  107. package/docs/styles-and-segments.md +85 -31
  108. package/docs/theming.md +30 -19
  109. package/docs/timeline.md +62 -28
  110. package/docs/tooltip-and-hover.md +72 -24
  111. package/docs/transforms-and-statistics.md +47 -17
  112. package/examples/BarValues.svelte +2 -2
  113. package/examples/Box.svelte +7 -0
  114. package/examples/Chrome.svelte +17 -0
  115. package/examples/ChromeOrder.svelte +18 -0
  116. package/examples/Composer.svelte +23 -0
  117. package/examples/CustomTooltip.svelte +21 -14
  118. package/examples/Facets.svelte +3 -3
  119. package/examples/FacetsCarousel.svelte +14 -0
  120. package/examples/FacetsRowsCols.svelte +23 -0
  121. package/examples/FirstPlot.svelte +7 -0
  122. package/examples/Gaps.svelte +21 -0
  123. package/examples/GroupedBars.svelte +15 -0
  124. package/examples/Heatmap.svelte +7 -0
  125. package/examples/HeatmapSegments.svelte +26 -0
  126. package/examples/Histogram.svelte +14 -0
  127. package/examples/HorizontalBars.svelte +15 -0
  128. package/examples/LegendSections.svelte +27 -0
  129. package/examples/LegendToggle.svelte +3 -3
  130. package/examples/LineByRegion.svelte +18 -0
  131. package/examples/LineDates.svelte +15 -0
  132. package/examples/Markers.svelte +27 -0
  133. package/examples/MovingAverage.svelte +15 -0
  134. package/examples/Pyramid.svelte +7 -0
  135. package/examples/Regression.svelte +14 -0
  136. package/examples/ResponsiveLine.svelte +7 -7
  137. package/examples/Scales.svelte +26 -0
  138. package/examples/Scatter.svelte +16 -0
  139. package/examples/SegmentHighlight.svelte +22 -0
  140. package/examples/Segments.svelte +11 -3
  141. package/examples/SegmentsBySeries.svelte +25 -0
  142. package/examples/StackedArea.svelte +16 -0
  143. package/examples/StackedBars.svelte +15 -0
  144. package/examples/TimelinePlayer.svelte +8 -7
  145. package/examples/TimelineRule.svelte +19 -0
  146. package/examples/Tooltip.svelte +3 -3
  147. package/examples/ValueLabels.svelte +21 -0
  148. package/examples/data.ts +320 -8
  149. package/llms-full.txt +33 -5
  150. package/llms.txt +30 -2
  151. package/package.json +3 -2
  152. package/dist/layout/scales/buildScale.d.ts +0 -8
  153. package/dist/layout/scales/buildScale.js +0 -67
  154. package/dist/layout/scales/niceDomain.d.ts +0 -23
  155. package/dist/layout/scales/niceDomain.js +0 -59
@@ -3,9 +3,11 @@
3
3
  import { resolveAccessor } from '../../plots/utils/accessors';
4
4
  import TimelineFrame from './TimelineFrame.svelte';
5
5
  import { TimelineSteps } from './TimelineSteps';
6
+ import { TimelineTime } from './TimelineTime';
6
7
  import { Interpolation } from '../../utils/Interpolation';
7
8
  import type {
8
9
  Orientation,
10
+ TimeInput,
9
11
  TimeValue,
10
12
  TimelineChildSnippet,
11
13
  TimelineInterpolation
@@ -22,6 +24,7 @@
22
24
  let {
23
25
  data,
24
26
  accessor,
27
+ dateFormat,
25
28
  progress,
26
29
  values,
27
30
  orientation = 'vertical',
@@ -35,28 +38,44 @@
35
38
  child
36
39
  }: {
37
40
  data: TRow[];
38
- accessor: Accessor<TRow, TimeValue>;
41
+ accessor: Accessor<TRow, TimeInput>;
42
+ /** How a non-ISO date string reads. */
43
+ dateFormat?: string;
39
44
  progress: number;
40
- values?: TimeValue[];
45
+ values?: TimeInput[];
41
46
  orientation?: Orientation;
42
47
  show?: boolean;
43
48
  thickness?: number;
44
49
  filterMode?: 'exact' | 'upTo';
45
50
  interpolate?: TimelineInterpolation<TRow>;
46
- /** Told the steps, whenever they change - for whoever else draws or drives the timeline. */
47
- onsteps?: (values: TimeValue[]) => void;
51
+ /** Told the steps, and whether they are dates, when they change. */
52
+ onsteps?: (values: TimeValue[], temporal: boolean) => void;
48
53
  width: number;
49
54
  height: number;
50
55
  child: TimelineChildSnippet<TRow>;
51
56
  } = $props();
52
57
 
53
- const getTime = $derived(resolveAccessor<TRow>(accessor));
58
+ const time = $derived(new TimelineTime(dateFormat));
59
+ const getRaw = $derived(resolveAccessor<TRow>(accessor) as (row: TRow) => unknown);
60
+ /** A row's step, or `undefined` when its value reads as none. */
61
+ const getTime = $derived((row: TRow) => time.toTime(getRaw(row)));
62
+ /** The rows with a step; one without shows at every step. */
63
+ const timed = $derived(data.filter((row) => getTime(row) !== undefined));
64
+ const untimed = $derived(data.filter((row) => getTime(row) === undefined));
54
65
 
55
- const steps = $derived(TimelineSteps.of(values, data, getTime));
66
+ const steps = $derived(
67
+ TimelineSteps.of(
68
+ values?.map((value) => time.toTime(value)).filter((v): v is TimeValue => v !== undefined),
69
+ timed,
70
+ getTime as (row: TRow) => TimeValue
71
+ )
72
+ );
73
+ const temporal = $derived(time.isTemporal(values?.length ? values : timed.map(getRaw)));
56
74
 
57
75
  $effect(() => {
58
76
  const found = steps.values;
59
- untrack(() => onsteps?.(found));
77
+ const dates = temporal;
78
+ untrack(() => onsteps?.(found, dates));
60
79
  });
61
80
 
62
81
  const progressEasingFn = $derived(Interpolation.buildEasing(interpolate?.progressEasing));
@@ -77,12 +96,19 @@
77
96
  );
78
97
  return upper;
79
98
  }
80
- return { ...upper, [accessor]: x, [valueField]: value } as TRow;
99
+ return {
100
+ ...upper,
101
+ [accessor]: time.toValueLike(x, upper[accessor]),
102
+ [valueField]: value
103
+ } as TRow;
81
104
  }
82
105
 
83
106
  const filtered = $derived.by(() => {
84
107
  const discrete = () =>
85
- data.filter((d) => TimelineSteps.matches(getTime(d), selectedValue, filterMode));
108
+ data.filter((d) => {
109
+ const at = getTime(d);
110
+ return at === undefined || TimelineSteps.matches(at, selectedValue, filterMode);
111
+ });
86
112
 
87
113
  if (!interpolate) return discrete();
88
114
  if (!interpolate.value) {
@@ -96,14 +122,14 @@
96
122
  const getValue = resolveAccessor<TRow>(interpolate.value) as (d: TRow) => number;
97
123
  const create = interpolate.create ?? defaultCreate;
98
124
 
99
- const groups = Map.groupBy(data, (row) => String(getKey(row)));
125
+ const groups = Map.groupBy(timed, (row) => String(getKey(row)));
100
126
 
101
- const result: TRow[] = [];
127
+ const result: TRow[] = [...untimed];
102
128
  for (const rows of groups.values()) {
103
129
  const tweened = Interpolation.tweenSeries({
104
130
  rows,
105
131
  x: currentTime,
106
- getTime,
132
+ getTime: getTime as (row: TRow) => TimeValue,
107
133
  getValue,
108
134
  create,
109
135
  segmentEasing: segmentEasingFn
@@ -1,18 +1,20 @@
1
- import type { Orientation, TimeValue, TimelineChildSnippet, TimelineInterpolation } from '../../types/layout/timeline';
1
+ import type { Orientation, TimeInput, TimeValue, TimelineChildSnippet, TimelineInterpolation } from '../../types/layout/timeline';
2
2
  import type { Accessor } from '../../types/plots/data/common';
3
3
  declare function $$render<TRow extends Record<string, unknown>>(): {
4
4
  props: {
5
5
  data: TRow[];
6
- accessor: Accessor<TRow, TimeValue>;
6
+ accessor: Accessor<TRow, TimeInput>;
7
+ /** How a non-ISO date string reads. */
8
+ dateFormat?: string;
7
9
  progress: number;
8
- values?: TimeValue[];
10
+ values?: TimeInput[];
9
11
  orientation?: Orientation;
10
12
  show?: boolean;
11
13
  thickness?: number;
12
14
  filterMode?: "exact" | "upTo";
13
15
  interpolate?: TimelineInterpolation<TRow>;
14
- /** Told the steps, whenever they change - for whoever else draws or drives the timeline. */
15
- onsteps?: (values: TimeValue[]) => void;
16
+ /** Told the steps, and whether they are dates, when they change. */
17
+ onsteps?: (values: TimeValue[], temporal: boolean) => void;
16
18
  width: number;
17
19
  height: number;
18
20
  child: TimelineChildSnippet<TRow>;
@@ -28,6 +28,7 @@
28
28
  <Timeline
29
29
  currentValue={timeline.value}
30
30
  values={timeline.steps}
31
+ format={timeline.format}
31
32
  orientation={timeline.orientation}
32
33
  width={measuredWidth}
33
34
  height={measuredHeight}
@@ -1,10 +1,12 @@
1
- import type { PlayerConfig, PlayerControl, Orientation, TimeValue, TimelineCallbacks, TimelineType } from '../../types/layout/timeline';
1
+ import type { PlayerConfig, PlayerControl, Orientation, TimeInput, TimeValue, TimelineStep, TimelineCallbacks, TimelineType } from '../../types/layout/timeline';
2
2
  /** What a {@link TimelineState} reads from the timeline's owner, each time it needs it. */
3
3
  export type TimelineSettings = TimelineCallbacks & {
4
4
  type?: TimelineType;
5
5
  player?: PlayerConfig;
6
6
  progress?: number;
7
- values?: TimeValue[];
7
+ values?: TimeInput[];
8
+ /** How a non-ISO date string in `values` reads. */
9
+ dateFormat?: string;
8
10
  show?: boolean;
9
11
  orientation?: Orientation;
10
12
  thickness?: number;
@@ -37,8 +39,10 @@ export declare class TimelineState {
37
39
  get shown(): boolean;
38
40
  /** The steps it moves across, ascending: the owner's `values`, else those its plots report. */
39
41
  get steps(): TimeValue[];
40
- /** Reports the steps the plots found in their rows. */
41
- setSteps(values: TimeValue[]): void;
42
+ /** Whether the steps are dates. */
43
+ get temporal(): boolean;
44
+ /** Reports the steps the plots found in their rows, and whether they are dates. */
45
+ setSteps(values: TimeValue[], temporal?: boolean): void;
42
46
  /** The position across the steps, `0`-`1`. */
43
47
  get progress(): number;
44
48
  /** The step the position lands on, as an index; `-1` without steps. */
@@ -55,8 +59,12 @@ export declare class TimelineState {
55
59
  get speeds(): number[];
56
60
  /** Milliseconds each step lasts at speed `1`. */
57
61
  get stepDuration(): number;
58
- /** How the current step reads. */
62
+ /** How a step reads: the player's `format`, else a date label or the number. */
59
63
  get format(): (value: TimeValue) => string;
64
+ /** A step as authors see it. */
65
+ step(value: TimeValue): TimelineStep;
66
+ /** An author's step as the timeline's own, or `undefined` when it reads as none. */
67
+ timeOf(value: TimeInput): TimeValue | undefined;
60
68
  /** Moves to `progress` (`0`-`1`), and tells the owner. */
61
69
  seek(progress: number): void;
62
70
  /** Moves to the step at `index`. */
@@ -1,6 +1,7 @@
1
1
  import { getContext, setContext, untrack } from 'svelte';
2
2
  import { getConfiguration } from '../../configuration/config.svelte';
3
3
  import { TimelineSteps } from './TimelineSteps';
4
+ import { TimelineTime } from './TimelineTime';
4
5
  const TIMELINE_KEY = Symbol('dct-timeline');
5
6
  /** The longest a frame counts for while playing, so a hidden tab doesn't jump ahead when it comes back. */
6
7
  const MAX_FRAME_MS = 250;
@@ -17,6 +18,7 @@ const MAX_FRAME_MS = 250;
17
18
  export class TimelineState {
18
19
  #settings;
19
20
  #steps = $state([]);
21
+ #temporal = $state(false);
20
22
  #own = $state(null);
21
23
  #playing = $state(false);
22
24
  #loop = $state(false);
@@ -58,14 +60,33 @@ export class TimelineState {
58
60
  }
59
61
  /** The steps it moves across, ascending: the owner's `values`, else those its plots report. */
60
62
  get steps() {
61
- const values = this.#settings()?.values;
62
- return values?.length ? new TimelineSteps(values).values : this.#steps;
63
- }
64
- /** Reports the steps the plots found in their rows. */
65
- setSteps(values) {
63
+ const values = this.#ownerValues;
64
+ return values ? new TimelineSteps(values).values : this.#steps;
65
+ }
66
+ /** Whether the steps are dates. */
67
+ get temporal() {
68
+ const settings = this.#settings();
69
+ if (!settings?.values?.length)
70
+ return this.#temporal;
71
+ return new TimelineTime(settings.dateFormat).isTemporal(settings.values);
72
+ }
73
+ /** Reports the steps the plots found in their rows, and whether they are dates. */
74
+ setSteps(values, temporal = false) {
66
75
  const same = values.length === this.#steps.length && values.every((v, i) => v === this.#steps[i]);
67
76
  if (!same)
68
77
  this.#steps = values;
78
+ if (temporal !== this.#temporal)
79
+ this.#temporal = temporal;
80
+ }
81
+ /** The owner's `values` as steps, or `undefined` without any. */
82
+ get #ownerValues() {
83
+ const settings = this.#settings();
84
+ if (!settings?.values?.length)
85
+ return undefined;
86
+ const time = new TimelineTime(settings.dateFormat);
87
+ return settings.values
88
+ .map((value) => time.toTime(value))
89
+ .filter((value) => value !== undefined);
69
90
  }
70
91
  /** The position across the steps, `0`-`1`. */
71
92
  get progress() {
@@ -103,9 +124,22 @@ export class TimelineState {
103
124
  get stepDuration() {
104
125
  return (this.#settings()?.player?.stepDuration ?? getConfiguration().timeline.player.stepDuration);
105
126
  }
106
- /** How the current step reads. */
127
+ /** How a step reads: the player's `format`, else a date label or the number. */
107
128
  get format() {
108
- return this.#settings()?.player?.format ?? ((value) => String(value));
129
+ const own = this.#settings()?.player?.format;
130
+ if (own)
131
+ return (value) => own(this.step(value));
132
+ if (this.temporal)
133
+ return TimelineTime.dateLabel(this.steps, getConfiguration().axis.locale);
134
+ return (value) => String(value);
135
+ }
136
+ /** A step as authors see it. */
137
+ step(value) {
138
+ return TimelineTime.toStep(value, this.temporal);
139
+ }
140
+ /** An author's step as the timeline's own, or `undefined` when it reads as none. */
141
+ timeOf(value) {
142
+ return new TimelineTime(this.#settings()?.dateFormat).toTime(value);
109
143
  }
110
144
  /** Moves to `progress` (`0`-`1`), and tells the owner. */
111
145
  seek(progress) {
@@ -196,7 +230,7 @@ export class TimelineState {
196
230
  const value = this.value;
197
231
  untrack(() => {
198
232
  if (last !== undefined && value !== undefined && value !== last) {
199
- this.#settings()?.onvalue?.(value);
233
+ this.#settings()?.onvalue?.(this.step(value));
200
234
  }
201
235
  last = value;
202
236
  });
@@ -0,0 +1,18 @@
1
+ import type { TimeValue, TimelineStep } from '../../types/layout/timeline';
2
+ /** Reads a timeline's values (numbers, `Date`s, ISO or `dateFormat` strings) as numeric steps, and back. */
3
+ export declare class TimelineTime {
4
+ #private;
5
+ readonly dateFormat?: string | undefined;
6
+ /** @param dateFormat How a non-ISO date string reads, e.g. `'DD/MM/YYYY'`. */
7
+ constructor(dateFormat?: string | undefined);
8
+ /** A value as its step, or `undefined` when it reads as none. */
9
+ toTime(value: unknown): TimeValue | undefined;
10
+ /** A step as authors see it: a `Date` when `temporal`, else the number. */
11
+ static toStep(value: TimeValue, temporal: boolean): TimelineStep;
12
+ /** Whether `values` hold dates rather than plain numbers. */
13
+ isTemporal(values: unknown[]): boolean;
14
+ /** A step written back in the same form as `like`, for interpolated rows. */
15
+ toValueLike(time: TimeValue, like: unknown): unknown;
16
+ /** A date label as precise as the steps need: year, month, day or time. */
17
+ static dateLabel(steps: TimeValue[], locale?: string): (time: TimeValue) => string;
18
+ }
@@ -0,0 +1,117 @@
1
+ /** The `dateFormat` tokens and the UTC date part each reads or writes. */
2
+ const TOKENS = {
3
+ YYYY: { group: '(\\d{4})', get: (date) => date.getUTCFullYear(), width: 4 },
4
+ MM: { group: '(\\d{1,2})', get: (date) => date.getUTCMonth() + 1, width: 2 },
5
+ DD: { group: '(\\d{1,2})', get: (date) => date.getUTCDate(), width: 2 },
6
+ HH: { group: '(\\d{1,2})', get: (date) => date.getUTCHours(), width: 2 },
7
+ mm: { group: '(\\d{1,2})', get: (date) => date.getUTCMinutes(), width: 2 },
8
+ ss: { group: '(\\d{1,2})', get: (date) => date.getUTCSeconds(), width: 2 }
9
+ };
10
+ const TOKEN_PATTERN = /YYYY|MM|DD|HH|mm|ss/g;
11
+ /** An ISO 8601 date, from a bare year to a full date-time. */
12
+ const ISO_DATE = /^\d{4}(-\d{2}(-\d{2}([T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?)?)?$/;
13
+ const DAY = 24 * 60 * 60 * 1000;
14
+ /** Reads a timeline's values (numbers, `Date`s, ISO or `dateFormat` strings) as numeric steps, and back. */
15
+ export class TimelineTime {
16
+ dateFormat;
17
+ /** @param dateFormat How a non-ISO date string reads, e.g. `'DD/MM/YYYY'`. */
18
+ constructor(dateFormat) {
19
+ this.dateFormat = dateFormat;
20
+ }
21
+ /** A value as its step, or `undefined` when it reads as none. */
22
+ toTime(value) {
23
+ if (typeof value === 'number')
24
+ return Number.isFinite(value) ? value : undefined;
25
+ if (value instanceof Date) {
26
+ const time = value.getTime();
27
+ return Number.isFinite(time) ? time : undefined;
28
+ }
29
+ if (typeof value !== 'string')
30
+ return undefined;
31
+ const text = value.trim();
32
+ if (this.dateFormat) {
33
+ const parsed = TimelineTime.#parse(text, this.dateFormat);
34
+ if (parsed !== undefined)
35
+ return parsed;
36
+ }
37
+ if (text !== '' && Number.isFinite(Number(text)) && !ISO_DATE.test(text))
38
+ return Number(text);
39
+ if (ISO_DATE.test(text)) {
40
+ // A bare year stays a year, like a number.
41
+ if (/^\d{4}$/.test(text))
42
+ return Number(text);
43
+ const time = Date.parse(/^\d{4}-\d{2}$/.test(text) ? `${text}-01` : text);
44
+ return Number.isFinite(time) ? time : undefined;
45
+ }
46
+ return undefined;
47
+ }
48
+ /** A step as authors see it: a `Date` when `temporal`, else the number. */
49
+ static toStep(value, temporal) {
50
+ return temporal ? new Date(value) : value;
51
+ }
52
+ /** Whether `values` hold dates rather than plain numbers. */
53
+ isTemporal(values) {
54
+ return values.some((value) => {
55
+ if (value instanceof Date)
56
+ return true;
57
+ if (typeof value !== 'string')
58
+ return false;
59
+ const text = value.trim();
60
+ return !/^-?\d+(\.\d+)?$/.test(text) && this.toTime(text) !== undefined;
61
+ });
62
+ }
63
+ /** A step written back in the same form as `like`, for interpolated rows. */
64
+ toValueLike(time, like) {
65
+ if (like instanceof Date)
66
+ return new Date(time);
67
+ if (typeof like !== 'string')
68
+ return time;
69
+ const text = like.trim();
70
+ if (this.dateFormat && TimelineTime.#parse(text, this.dateFormat) !== undefined) {
71
+ return TimelineTime.#write(new Date(time), this.dateFormat);
72
+ }
73
+ if (ISO_DATE.test(text) && !/^\d{4}$/.test(text)) {
74
+ return new Date(time).toISOString().slice(0, text.length);
75
+ }
76
+ return String(time);
77
+ }
78
+ /** A date label as precise as the steps need: year, month, day or time. */
79
+ static dateLabel(steps, locale) {
80
+ const dates = steps.map((time) => new Date(time));
81
+ const options = dates.every((d) => d.getUTCMonth() === 0 && d.getUTCDate() === 1 && d.getTime() % DAY === 0)
82
+ ? { year: 'numeric' }
83
+ : dates.every((d) => d.getUTCDate() === 1 && d.getTime() % DAY === 0)
84
+ ? { month: 'short', year: 'numeric' }
85
+ : dates.every((d) => d.getTime() % DAY === 0)
86
+ ? { day: 'numeric', month: 'short', year: 'numeric' }
87
+ : { dateStyle: 'medium', timeStyle: 'short' };
88
+ const format = new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' });
89
+ return (time) => format.format(new Date(time));
90
+ }
91
+ /** `text` read through `format`, or `undefined` when it does not match. */
92
+ static #parse(text, format) {
93
+ const order = [];
94
+ const pattern = format
95
+ .split(TOKEN_PATTERN)
96
+ .map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
97
+ const tokens = format.match(TOKEN_PATTERN) ?? [];
98
+ let source = pattern[0];
99
+ tokens.forEach((token, index) => {
100
+ order.push(token);
101
+ source += TOKENS[token].group + pattern[index + 1];
102
+ });
103
+ const match = new RegExp(`^${source}$`).exec(text);
104
+ if (!match || !order.length)
105
+ return undefined;
106
+ const parts = { YYYY: 1970, MM: 1, DD: 1, HH: 0, mm: 0, ss: 0 };
107
+ order.forEach((token, index) => (parts[token] = Number(match[index + 1])));
108
+ return Date.UTC(parts.YYYY, parts.MM - 1, parts.DD, parts.HH, parts.mm, parts.ss);
109
+ }
110
+ /** `date` written through `format`. */
111
+ static #write(date, format) {
112
+ return format.replace(TOKEN_PATTERN, (token) => {
113
+ const { get, width } = TOKENS[token];
114
+ return String(get(date)).padStart(width, '0');
115
+ });
116
+ }
117
+ }
@@ -6,8 +6,8 @@
6
6
  /**
7
7
  * A plot's floating tooltip at `(x, y)`, inside a `position: relative`
8
8
  * parent and themed by the `--dct-tooltip-*` variables. It reads the rows
9
- * the hover matched: its own `content` snippet, given every row, or each
10
- * row's `format` text, one per line. It sits at `anchorX`/`anchorY` of the
9
+ * the hover matched: its own `content` snippet, given every row as matched,
10
+ * or each row's `format` text, one per line, in `sort`'s order. It sits at `anchorX`/`anchorY` of the
11
11
  * point (default right and below) and moves over where it would leave its
12
12
  * plot's box - or `bounds`, when given. See {@link TooltipState}.
13
13
  */
@@ -17,6 +17,7 @@
17
17
  y,
18
18
  rows,
19
19
  format,
20
+ sort,
20
21
  content,
21
22
  bounds,
22
23
  anchorX = 'right',
@@ -30,6 +31,8 @@
30
31
  /** Every row the hover matched. */
31
32
  rows: TRow[];
32
33
  format: (row: TRow) => string;
34
+ /** The order the `format` text reads the rows in. Unset, as matched. */
35
+ sort?: (a: TRow, b: TRow) => number;
33
36
  /** Custom tooltip body, given every row; replaces the `format` text and the default box. */
34
37
  content?: Snippet<[{ rows: TRow[] }]>;
35
38
  /** Box the tooltip must stay within. Defaults to its plot's box. */
@@ -63,7 +66,7 @@
63
66
  {#if content}
64
67
  {@render content({ rows })}
65
68
  {:else}
66
- {TooltipState.text(rows, format)}
69
+ {TooltipState.text(rows, format, sort)}
67
70
  {/if}
68
71
  </div>
69
72
  {/if}
@@ -8,6 +8,8 @@ declare function $$render<TRow extends Record<string, unknown>>(): {
8
8
  /** Every row the hover matched. */
9
9
  rows: TRow[];
10
10
  format: (row: TRow) => string;
11
+ /** The order the `format` text reads the rows in. Unset, as matched. */
12
+ sort?: (a: TRow, b: TRow) => number;
11
13
  /** Custom tooltip body, given every row; replaces the `format` text and the default box. */
12
14
  content?: Snippet<[{
13
15
  rows: TRow[];
@@ -35,8 +35,14 @@ export declare class TooltipState {
35
35
  constructor(inputs: TooltipInputs);
36
36
  /** Where the tooltip sits. */
37
37
  readonly box: Box;
38
- /** The text `rows` read as through `format`, one line each. */
39
- static text<TRow>(rows: TRow[], format: (row: TRow) => string): string;
38
+ /** The text `rows` read as through `format`, one line each, in `sort`'s order when given. */
39
+ static text<TRow>(rows: TRow[], format: (row: TRow) => string, sort?: (a: TRow, b: TRow) => number): string;
40
+ /**
41
+ * Orders rows from the highest value `valueOf` reads to the lowest. A row
42
+ * without a numeric value goes after every one with, and rows that tie keep
43
+ * the order the hover matched them in.
44
+ */
45
+ static descending<TRow>(valueOf: (row: TRow) => unknown): (a: TRow, b: TRow) => number;
40
46
  /** A value as plain text: a date as a locale date, anything else as itself. */
41
47
  static value(value: unknown): string;
42
48
  /**
@@ -39,9 +39,28 @@ export class TooltipState {
39
39
  return (this.#plotMargins?.place(size, point, anchor) ??
40
40
  size.placeNear(point, anchor, Box.size(Infinity, Infinity)));
41
41
  });
42
- /** The text `rows` read as through `format`, one line each. */
43
- static text(rows, format) {
44
- return rows.map(format).join('\n');
42
+ /** The text `rows` read as through `format`, one line each, in `sort`'s order when given. */
43
+ static text(rows, format, sort) {
44
+ return (sort ? [...rows].sort(sort) : rows).map(format).join('\n');
45
+ }
46
+ /**
47
+ * Orders rows from the highest value `valueOf` reads to the lowest. A row
48
+ * without a numeric value goes after every one with, and rows that tie keep
49
+ * the order the hover matched them in.
50
+ */
51
+ static descending(valueOf) {
52
+ const numberOf = (row) => {
53
+ const value = valueOf(row);
54
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
55
+ };
56
+ return (a, b) => {
57
+ const left = numberOf(a);
58
+ const right = numberOf(b);
59
+ if (left === undefined || right === undefined) {
60
+ return (left === undefined ? 1 : 0) - (right === undefined ? 1 : 0);
61
+ }
62
+ return right - left;
63
+ };
45
64
  }
46
65
  /** A value as plain text: a date as a locale date, anything else as itself. */
47
66
  static value(value) {
@@ -3,12 +3,15 @@
3
3
  import DotMarker from '../dot/DotMarker.svelte';
4
4
  import TextMarker from '../text/TextMarker.svelte';
5
5
  import { Delta } from './Delta';
6
+ import { getConfiguration } from '../../configuration/config.svelte';
6
7
  import type { Series } from '../../types/plots/data/common';
7
8
  import type { DeltaMarkerConfig } from '../../types/markers/axis/delta';
8
9
 
9
10
  let { marker, data = [] }: { marker: DeltaMarkerConfig; data?: Series<TData>[] } = $props();
10
11
 
11
12
  const resolved = $derived(Delta.build(marker.axis, data, marker));
13
+ const label = $derived(getConfiguration().delta.text[marker.axis]);
14
+ const lineAnchor = $derived(marker.text?.lineAnchor ?? label.lineAnchor);
12
15
 
13
16
  const clampRange = $derived.by((): [number, number] | undefined => {
14
17
  const domain = marker.axis === 'x' ? marker.yDomain : marker.xDomain;
@@ -27,10 +30,9 @@
27
30
  }
28
31
 
29
32
  function textVaryCoord(fromVis: number, toVis: number) {
30
- const anchor = marker.text?.lineAnchor ?? 'middle';
31
- return anchor === 'top'
33
+ return lineAnchor === 'top'
32
34
  ? Math.max(fromVis, toVis)
33
- : anchor === 'bottom'
35
+ : lineAnchor === 'bottom'
34
36
  ? Math.min(fromVis, toVis)
35
37
  : (fromVis + toVis) / 2;
36
38
  }
@@ -82,12 +84,12 @@
82
84
  styles: {
83
85
  fontWeight: 'bold',
84
86
  fontSize: 14,
85
- textAnchor: 'middle',
87
+ textAnchor: label.textAnchor,
86
88
  fill: marker.line?.stroke ?? 'currentColor',
87
89
  ...marker.text,
88
- dx: marker.text?.dx ?? (marker.axis === 'x' ? 12 : 0),
89
- dy: marker.text?.dy ?? (marker.axis === 'x' ? 0 : -10),
90
- lineAnchor: marker.axis === 'x' ? (marker.text?.lineAnchor ?? 'middle') : 'bottom'
90
+ dx: marker.text?.dx ?? label.dx,
91
+ dy: marker.text?.dy ?? label.dy,
92
+ lineAnchor
91
93
  }
92
94
  }}
93
95
  />
@@ -9,6 +9,7 @@ import { StylesPhase } from './styles/StylesPhase';
9
9
  import { MarkersPhase } from './markers/MarkersPhase';
10
10
  import { TooltipPhase } from './tooltip/TooltipPhase';
11
11
  import { HoverPhase } from './hover/HoverPhase';
12
+ import { HoverSettings } from '../layout/hover/HoverSettings';
12
13
  import { PlotInteraction } from '../layout/interaction/PlotInteraction.svelte';
13
14
  import { DEFAULT_CONFIG } from '../configuration/config.svelte';
14
15
  /**
@@ -77,7 +78,7 @@ export class Pipeline {
77
78
  new FacetPhase(),
78
79
  new DataPhase(kind.data),
79
80
  new MappingPhase(kind.mapping),
80
- new TransformsPhase(kind.transforms),
81
+ new TransformsPhase(kind.transforms, kind.data?.segments?.connect),
81
82
  new StatisticsPhase(kind.statistics),
82
83
  new SharedScalesPhase(kind.sharedScales, scales),
83
84
  scales,
@@ -143,7 +144,7 @@ export class Pipeline {
143
144
  return fixed;
144
145
  const upToData = this.stages.filter((s) => s.phase === 'data');
145
146
  const state = Pipeline.#through(upToData, this.#seed(props), Pipeline.#ctxFor(cfg, null));
146
- return HoverPhase.strategyOf(state);
147
+ return HoverSettings.strategyOf(state.axes);
147
148
  }
148
149
  /** The plot's default `timeline.filterMode`. */
149
150
  get filterMode() {
@@ -1,12 +1,12 @@
1
- import type { Accessor } from '../../types/plots/data/common';
2
1
  import type { AxisPlotState, AxisPlotCtx, DataStageOptions, PhaseStage } from '../../types/phases';
3
2
  type AnyAxisState = AxisPlotState<any, any, any, any>;
4
3
  /**
5
4
  * The "data" phase every axis-based plot shares: its series (from `series`,
6
- * or flat rows split by {@link DataPhase.buildGrouping}), what each position
7
- * channel is and which way the plot runs ({@link PlotAxes}), each series'
8
- * color, the series a legend omits dropped, each series cut into its styled
9
- * visual segments, and - for a discrete mark - the values its gaps fill in.
5
+ * or flat rows split by `seriesGrouping`: its `grouping`, or a categorical
6
+ * `color`), what each position channel is and which way the plot runs
7
+ * ({@link PlotAxes}), each series' color, the series a legend omits dropped,
8
+ * each series cut into its styled visual segments, and - for a discrete
9
+ * mark - the values its gaps fill in.
10
10
  * Colors and axes come from every series, before any is omitted or split by
11
11
  * facet panel, so they stay put and every panel agrees.
12
12
  */
@@ -15,11 +15,5 @@ export declare class DataPhase<TState extends AnyAxisState, TCtx extends AxisPlo
15
15
  readonly phase: "data";
16
16
  constructor(options?: DataStageOptions<TState, TCtx>);
17
17
  run: (state: TState, ctx: TCtx) => TState;
18
- /**
19
- * The key a plot's rows split into series by: its `grouping`, or its `color`
20
- * when that names categories. A numeric `color` is a continuous value (a
21
- * heatmap's cells), so it colors rows without grouping them.
22
- */
23
- static buildGrouping(state: Pick<AnyAxisState, 'grouping' | 'color' | 'data'>): Accessor<any, unknown> | undefined;
24
18
  }
25
19
  export {};