@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
@@ -104,7 +104,7 @@ export class PublicTimeline {
104
104
  }
105
105
  /** Every step it moves across, ascending. */
106
106
  get steps() {
107
- return this.#state.steps;
107
+ return this.#state.steps.map((step) => this.#state.step(step));
108
108
  }
109
109
  /** The position across the steps, `0`-`1`. */
110
110
  get progress() {
@@ -112,7 +112,8 @@ export class PublicTimeline {
112
112
  }
113
113
  /** The step the position lands on, or `undefined` without steps. */
114
114
  get value() {
115
- return this.#state.value;
115
+ const { value } = this.#state;
116
+ return value === undefined ? undefined : this.#state.step(value);
116
117
  }
117
118
  get index() {
118
119
  return this.#state.index;
@@ -139,8 +140,11 @@ export class PublicTimeline {
139
140
  seekIndex(index) {
140
141
  this.#state.seekIndex(index);
141
142
  }
143
+ /** Moves to the step closest to `value`. */
142
144
  seekValue(value) {
143
- this.#state.seekValue(value);
145
+ const time = this.#state.timeOf(value);
146
+ if (time !== undefined)
147
+ this.#state.seekValue(time);
144
148
  }
145
149
  next() {
146
150
  this.#state.next();
@@ -1,6 +1,7 @@
1
1
  import { resolveAccessor } from '../../plots/utils/accessors';
2
2
  import { merge } from '../../utils/merge';
3
3
  import { TimelineSteps } from '../timeline/TimelineSteps';
4
+ import { TimelineTime } from '../timeline/TimelineTime';
4
5
  import { buildFacetAxesDisplay } from '../facet/axes';
5
6
  import { buildFacetPlaceKey, isFacetWrap } from '../facet/config';
6
7
  /**
@@ -55,10 +56,11 @@ export class Slice {
55
56
  };
56
57
  if (timeline && place.time !== undefined) {
57
58
  const get = resolveAccessor(timeline.accessor);
59
+ const reader = new TimelineTime(timeline.dateFormat);
58
60
  const time = place.time;
59
61
  tests.push((row) => {
60
- const value = get(row);
61
- if (value == null)
62
+ const value = reader.toTime(get(row));
63
+ if (value === undefined)
62
64
  return true;
63
65
  return TimelineSteps.matches(value, time, this.filterMode);
64
66
  });
@@ -0,0 +1,39 @@
1
+ import type { AxisChannelName } from '../../types/layout/mapping';
2
+ import type { AxisScale, EdgeTicksOptions } from '../../types/layout/scales';
3
+ /** What {@link EdgeTicks.build} measures an axis with. */
4
+ export type EdgeTicksInput = {
5
+ /** The axis measured: labels sit side by side along x, stacked along y. */
6
+ axis: AxisChannelName;
7
+ /** The axis' numeric values; their extent is the axis' domain. */
8
+ values: number[];
9
+ /** The axis' length in pixels. */
10
+ span: number;
11
+ /** The pixels between ticks the axis aims for, and the fewest ticks it takes. */
12
+ spacing: number;
13
+ min: number;
14
+ /** The tick labels' font size, in pixels. */
15
+ fontSize: number;
16
+ options: EdgeTicksOptions;
17
+ };
18
+ /**
19
+ * The ticks of a continuous axis: its nice ticks plus the data's first and
20
+ * last values, so a reader sees where the data starts and ends. When an edge
21
+ * tick lands too close to the nice tick next to it for both labels to fit,
22
+ * `options.collision` decides which gives way, or widens the axis instead.
23
+ */
24
+ export declare class EdgeTicks {
25
+ /** The share of the font size one label character takes, on average. */
26
+ static readonly CHARACTER_WIDTH = 0.6;
27
+ /** The pixels left between two neighbouring labels. */
28
+ static readonly LABEL_GAP = 6;
29
+ /** A plot's `scales.<axis>.edgeTicks` over the configuration's `axis.edgeTicks.<axis>`. */
30
+ static resolve(option: AxisScale['edgeTicks'], configured: EdgeTicksOptions): EdgeTicksOptions;
31
+ /**
32
+ * The axis' ticks, and the domain it widens to when `'extend'` resolved a
33
+ * collision. `undefined` when edge ticks are off or the values span nothing.
34
+ */
35
+ static build({ axis, values, span, spacing, min, fontSize, options }: EdgeTicksInput): {
36
+ ticks: number[];
37
+ domain?: [number, number];
38
+ } | undefined;
39
+ }
@@ -0,0 +1,82 @@
1
+ import { NiceTicks } from './NiceTicks';
2
+ /**
3
+ * The ticks of a continuous axis: its nice ticks plus the data's first and
4
+ * last values, so a reader sees where the data starts and ends. When an edge
5
+ * tick lands too close to the nice tick next to it for both labels to fit,
6
+ * `options.collision` decides which gives way, or widens the axis instead.
7
+ */
8
+ export class EdgeTicks {
9
+ /** The share of the font size one label character takes, on average. */
10
+ static CHARACTER_WIDTH = 0.6;
11
+ /** The pixels left between two neighbouring labels. */
12
+ static LABEL_GAP = 6;
13
+ /** A plot's `scales.<axis>.edgeTicks` over the configuration's `axis.edgeTicks.<axis>`. */
14
+ static resolve(option, configured) {
15
+ if (option === undefined)
16
+ return configured;
17
+ if (typeof option === 'boolean')
18
+ return { ...configured, enabled: option };
19
+ return { enabled: true, collision: option.collision ?? configured.collision };
20
+ }
21
+ /**
22
+ * The axis' ticks, and the domain it widens to when `'extend'` resolved a
23
+ * collision. `undefined` when edge ticks are off or the values span nothing.
24
+ */
25
+ static build({ axis, values, span, spacing, min, fontSize, options }) {
26
+ if (!options.enabled || values.length < 2)
27
+ return undefined;
28
+ const lo = Math.min(...values);
29
+ const hi = Math.max(...values);
30
+ if (!(hi > lo))
31
+ return undefined;
32
+ const count = NiceTicks.count(span, spacing, min);
33
+ // Whole-number values (years, counts) never get ticks between them.
34
+ const step = values.every(Number.isInteger)
35
+ ? Math.max(1, NiceTicks.step(lo, hi, count))
36
+ : NiceTicks.step(lo, hi, count);
37
+ const nice = NiceTicks.inRange(lo, hi, step);
38
+ if (!nice.length)
39
+ return { ticks: [lo, hi] };
40
+ const eps = step * 1e-6;
41
+ const pixels = (value) => ((value - lo) / (hi - lo)) * span;
42
+ // Side by side, two labels need half of each one's width; stacked, one line's height.
43
+ const labelWidth = (value) => String(value).length * fontSize * EdgeTicks.CHARACTER_WIDTH;
44
+ const room = (edge, neighbour) => axis === 'x' ? (labelWidth(edge) + labelWidth(neighbour)) / 2 : fontSize;
45
+ const crowds = (edge, neighbour) => Math.abs(pixels(edge) - pixels(neighbour)) < room(edge, neighbour) + EdgeTicks.LABEL_GAP;
46
+ const inner = [...nice];
47
+ const before = [];
48
+ const after = [];
49
+ let domainLo = lo;
50
+ let domainHi = hi;
51
+ const first = nice[0];
52
+ if (first - lo > eps) {
53
+ if (!crowds(lo, first))
54
+ before.push(lo);
55
+ else if (options.collision === 'drop-nice') {
56
+ before.push(lo);
57
+ inner.shift();
58
+ }
59
+ else if (options.collision === 'extend') {
60
+ domainLo = first - step;
61
+ before.push(domainLo);
62
+ }
63
+ }
64
+ const last = nice[nice.length - 1];
65
+ if (hi - last > eps) {
66
+ if (!crowds(hi, last))
67
+ after.push(hi);
68
+ else if (options.collision === 'drop-nice') {
69
+ after.push(hi);
70
+ if (inner[inner.length - 1] === last)
71
+ inner.pop();
72
+ }
73
+ else if (options.collision === 'extend') {
74
+ domainHi = last + step;
75
+ after.push(domainHi);
76
+ }
77
+ }
78
+ const ticks = [...before, ...inner, ...after];
79
+ const widened = domainLo !== lo || domainHi !== hi;
80
+ return widened ? { ticks, domain: [domainLo, domainHi] } : { ticks };
81
+ }
82
+ }
@@ -0,0 +1,40 @@
1
+ import type { AxisChannelName } from '../../types/layout/mapping';
2
+ /**
3
+ * "Nice" (1/2/5×10ⁿ) tick steps and the domains they round to, compatible with
4
+ * d3-array's and reimplemented here to avoid depending on internals svelteplot
5
+ * itself pulls in (not one of our declared dependencies) - see
6
+ * https://d3js.org/d3-array/ticks.
7
+ */
8
+ export declare class NiceTicks {
9
+ /**
10
+ * How far past a tick, as a share of the tick step, a domain still ends on
11
+ * it. Plain `Math.ceil`/`Math.floor` (what d3's `nice()` uses) has no
12
+ * tolerance: parts that add up to `100`, but come out `100.00000000000001`
13
+ * after summing or `100.2` after each was rounded at the source, round a
14
+ * full step further (`100` → `120`). 5% of the step catches that noise and
15
+ * lets a mark pass the last tick by a few pixels at most.
16
+ */
17
+ static readonly TOLERANCE = 0.05;
18
+ /** The pixels between ticks an axis aims for, along x and along y. */
19
+ static readonly SPACING: {
20
+ readonly x: 80;
21
+ readonly y: 50;
22
+ };
23
+ /** The step between nice ticks for an approximate `count` over `[start, stop]`. */
24
+ static step(start: number, stop: number, count: number): number;
25
+ /** Multiples of `step` covering `[start, stop]`, inclusive of either endpoint when it lands exactly on one. */
26
+ static inRange(start: number, stop: number, step: number): number[];
27
+ /** How many ticks fit `span` pixels at one per `spacing`, and at least `min`. */
28
+ static count(span: number, spacing: number, min: number): number;
29
+ /**
30
+ * `[lo, hi]` widened outward to the nearest nice step for about `count`
31
+ * ticks, like d3's `scale.nice(count)` - except a value within
32
+ * `tolerance × step` past a tick stays on it (see {@link NiceTicks.TOLERANCE}).
33
+ */
34
+ static domain(lo: number, hi: number, count?: number, tolerance?: number): [number, number];
35
+ /** {@link NiceTicks.domain} with as many ticks as fit `axis` in a plot of this size. */
36
+ static fit(size: {
37
+ width: number;
38
+ height: number;
39
+ }, axis: AxisChannelName, lo: number, hi: number, tolerance?: number): [number, number];
40
+ }
@@ -0,0 +1,72 @@
1
+ import { Margins } from '../margins/Margins';
2
+ /**
3
+ * "Nice" (1/2/5×10ⁿ) tick steps and the domains they round to, compatible with
4
+ * d3-array's and reimplemented here to avoid depending on internals svelteplot
5
+ * itself pulls in (not one of our declared dependencies) - see
6
+ * https://d3js.org/d3-array/ticks.
7
+ */
8
+ export class NiceTicks {
9
+ /**
10
+ * How far past a tick, as a share of the tick step, a domain still ends on
11
+ * it. Plain `Math.ceil`/`Math.floor` (what d3's `nice()` uses) has no
12
+ * tolerance: parts that add up to `100`, but come out `100.00000000000001`
13
+ * after summing or `100.2` after each was rounded at the source, round a
14
+ * full step further (`100` → `120`). 5% of the step catches that noise and
15
+ * lets a mark pass the last tick by a few pixels at most.
16
+ */
17
+ static TOLERANCE = 0.05;
18
+ /** The pixels between ticks an axis aims for, along x and along y. */
19
+ static SPACING = { x: 80, y: 50 };
20
+ /** The step between nice ticks for an approximate `count` over `[start, stop]`. */
21
+ static step(start, stop, count) {
22
+ const e10 = Math.sqrt(50);
23
+ const e5 = Math.sqrt(10);
24
+ const e2 = Math.sqrt(2);
25
+ const step0 = Math.abs(stop - start) / Math.max(0, count);
26
+ let step1 = 10 ** Math.floor(Math.log10(step0));
27
+ const error = step0 / step1;
28
+ if (error >= e10)
29
+ step1 *= 10;
30
+ else if (error >= e5)
31
+ step1 *= 5;
32
+ else if (error >= e2)
33
+ step1 *= 2;
34
+ return step1;
35
+ }
36
+ /** Multiples of `step` covering `[start, stop]`, inclusive of either endpoint when it lands exactly on one. */
37
+ static inRange(start, stop, step) {
38
+ if (!(step > 0) || !Number.isFinite(step))
39
+ return [];
40
+ const lo = Math.ceil(start / step);
41
+ const hi = Math.floor(stop / step);
42
+ const out = [];
43
+ for (let i = lo; i <= hi; i++)
44
+ out.push(i * step);
45
+ return out;
46
+ }
47
+ /** How many ticks fit `span` pixels at one per `spacing`, and at least `min`. */
48
+ static count(span, spacing, min) {
49
+ return Math.max(min, Math.round(span / spacing));
50
+ }
51
+ /**
52
+ * `[lo, hi]` widened outward to the nearest nice step for about `count`
53
+ * ticks, like d3's `scale.nice(count)` - except a value within
54
+ * `tolerance × step` past a tick stays on it (see {@link NiceTicks.TOLERANCE}).
55
+ */
56
+ static domain(lo, hi, count = 5, tolerance = NiceTicks.TOLERANCE) {
57
+ if (!(hi > lo))
58
+ return [lo, hi];
59
+ const step = NiceTicks.step(lo, hi, count);
60
+ const slack = step * tolerance;
61
+ const niceLo = Math.floor((lo + slack) / step) * step;
62
+ const niceHi = Math.ceil((hi - slack) / step) * step;
63
+ // `|| 0` turns a `-0` (a bound at zero, rounded toward it) into `0`, which an axis writes without a sign.
64
+ return [niceLo || 0, niceHi || 0];
65
+ }
66
+ /** {@link NiceTicks.domain} with as many ticks as fit `axis` in a plot of this size. */
67
+ static fit(size, axis, lo, hi, tolerance) {
68
+ const box = Margins.AXIS_ESTIMATE.contentBox(size.width, size.height);
69
+ const span = axis === 'x' ? box.width : box.height;
70
+ return NiceTicks.domain(lo, hi, NiceTicks.count(span, NiceTicks.SPACING[axis], 2), tolerance);
71
+ }
72
+ }
@@ -4,7 +4,8 @@
4
4
  import { AxisX, AxisY, GridX, GridY } from 'svelteplot';
5
5
  import { usePlot } from '../plot/usePlot';
6
6
  import { getConfiguration } from '../../configuration/config.svelte';
7
- import { tickStep, ticksInRange, naturalTickCount } from './niceDomain';
7
+ import { NiceTicks } from './NiceTicks';
8
+ import { EdgeTicks } from './EdgeTicks';
8
9
  import { DISCRETE_SCALE_TYPES } from '../../types/layout/scales';
9
10
  import type { AxisBasedScalesConfig } from '../../types/layout/scales';
10
11
  import type { AxisValue } from '../../types/layout/mapping';
@@ -17,7 +18,8 @@
17
18
  width,
18
19
  height,
19
20
  numericMargins,
20
- scales
21
+ scales,
22
+ edgeDomains = undefined
21
23
  }: {
22
24
  data: TData[];
23
25
  getX: (d: TData) => AxisValue;
@@ -26,6 +28,11 @@
26
28
  height: number;
27
29
  numericMargins: { top: number; right: number; bottom: number; left: number };
28
30
  scales?: AxisBasedScalesConfig<TData>;
31
+ /**
32
+ * Where to leave the domain an axis widens to when an edge tick's
33
+ * `'extend'` resolved a collision (see `EdgeTicks`), for the plot to give its scale.
34
+ */
35
+ edgeDomains?: Map<'x' | 'y', [number, number]>;
29
36
  } = $props();
30
37
 
31
38
  const cfg = $derived(getConfiguration());
@@ -50,7 +57,7 @@
50
57
  return hi > lo ? [lo, hi] : undefined;
51
58
  }
52
59
 
53
- /** An axis paired with a discrete scale plays the "index" role for the pair, so *this* axis is the plain magnitude/value one (see `autoTickCount`, `edgeInclusiveTicks`). */
60
+ /** An axis paired with a discrete scale plays the "index" role for the pair, so *this* axis is the plain magnitude/value one (see `autoTickCount`, `edgeTicksOf`). */
54
61
  const isDiscreteType = (type: string) =>
55
62
  (DISCRETE_SCALE_TYPES as readonly string[]).includes(type);
56
63
 
@@ -75,7 +82,7 @@
75
82
  return undefined;
76
83
  if (oppositeType != null && isDiscreteType(oppositeType)) return undefined;
77
84
  const uniqueCount = new Set(values.map(String)).size;
78
- const natural = naturalTickCount(span, spacing, min);
85
+ const natural = NiceTicks.count(span, spacing, min);
79
86
  return uniqueCount >= 2 && uniqueCount < natural ? uniqueCount : undefined;
80
87
  }
81
88
 
@@ -94,32 +101,40 @@
94
101
  if (!extent) return undefined;
95
102
  const [lo, hi] = extent;
96
103
  if (!Number.isInteger(lo) || !Number.isInteger(hi)) return undefined;
97
- return hi - lo < naturalTickCount(span, spacing, min) ? 1 : undefined;
104
+ return hi - lo < NiceTicks.count(span, spacing, min) ? 1 : undefined;
98
105
  }
99
106
 
100
107
  const xSpan = $derived(Math.max(0, width - numericMargins.left - numericMargins.right));
101
108
  const ySpan = $derived(Math.max(0, height - numericMargins.top - numericMargins.bottom));
102
109
 
103
- const xTickCount = $derived(autoTickCount(plot.scales.y.type, scales?.x, xValues, xSpan, 80, 3));
104
- const yTickCount = $derived(autoTickCount(plot.scales.x.type, scales?.y, yValues, ySpan, 50, 2));
110
+ const xTickCount = $derived(
111
+ autoTickCount(plot.scales.y.type, scales?.x, xValues, xSpan, NiceTicks.SPACING.x, 3)
112
+ );
113
+ const yTickCount = $derived(
114
+ autoTickCount(plot.scales.x.type, scales?.y, yValues, ySpan, NiceTicks.SPACING.y, 2)
115
+ );
105
116
 
106
- const xTickInterval = $derived(integerTickInterval(scales?.x, xValues, xSpan, 80, 3));
107
- const yTickInterval = $derived(integerTickInterval(scales?.y, yValues, ySpan, 50, 2));
117
+ const xTickInterval = $derived(
118
+ integerTickInterval(scales?.x, xValues, xSpan, NiceTicks.SPACING.x, 3)
119
+ );
120
+ const yTickInterval = $derived(
121
+ integerTickInterval(scales?.y, yValues, ySpan, NiceTicks.SPACING.y, 2)
122
+ );
108
123
 
109
124
  /**
110
- * A continuous axis' nice ticks plus its domain's endpoints, when the
111
- * opposite axis is continuous too - at least a whole unit apart when every
112
- * value is a whole number. Unset when the caller sets tick density
113
- * or the domain.
125
+ * A continuous axis' nice ticks plus the data's first and last values (see
126
+ * {@link EdgeTicks}), when its other axis is continuous too and the author
127
+ * left its density and domain to the plot. Unset otherwise.
114
128
  */
115
- function edgeInclusiveTicks(
129
+ function edgeTicksOf(
130
+ axis: 'x' | 'y',
131
+ type: string | undefined,
116
132
  oppositeType: string | undefined,
117
133
  scale: AxisBasedScalesConfig<TData>['x'],
118
134
  values: AxisValue[],
119
135
  span: number,
120
- spacing: number,
121
136
  min: number
122
- ): number[] | undefined {
137
+ ) {
123
138
  if (
124
139
  scale?.ticks != null ||
125
140
  scale?.tickSpacing != null ||
@@ -127,25 +142,43 @@
127
142
  scale?.domain != null
128
143
  )
129
144
  return undefined;
145
+ if (type != null && isDiscreteType(type)) return undefined;
130
146
  if (oppositeType != null && isDiscreteType(oppositeType)) return undefined;
131
147
  if (scale?.type != null && scale.type !== 'linear' && scale.type !== 'auto') return undefined;
132
- const extent = axisExtent(scale, values);
133
- if (!extent) return undefined;
134
- const [lo, hi] = extent;
135
- const count = naturalTickCount(span, spacing, min);
136
- // Whole-number values (years, counts) never get ticks between them.
137
- const integers = values.every((v) => typeof v !== 'number' || Number.isInteger(v));
138
- const step = integers ? Math.max(1, tickStep(lo, hi, count)) : tickStep(lo, hi, count);
139
- const base = ticksInRange(lo, hi, step);
140
- const eps = step * 1e-6;
141
- const withLo = base.length === 0 || base[0] - lo > eps ? [lo, ...base] : base;
142
- return withLo.length === 0 || hi - withLo[withLo.length - 1] > eps ? [...withLo, hi] : withLo;
148
+ const present = values.filter((v) => v != null);
149
+ if (!present.every((v) => typeof v === 'number')) return undefined;
150
+ return EdgeTicks.build({
151
+ axis,
152
+ values: (present as number[]).filter(Number.isFinite),
153
+ span,
154
+ spacing: NiceTicks.SPACING[axis],
155
+ min,
156
+ fontSize: cfg.axis.fontSize,
157
+ options: EdgeTicks.resolve(scale?.edgeTicks, cfg.axis.edgeTicks[axis])
158
+ });
143
159
  }
144
160
 
145
- const xTicksExplicit = $derived(
146
- edgeInclusiveTicks(plot.scales.y.type, scales?.x, xValues, xSpan, 80, 3)
161
+ const xEdgeTicks = $derived(
162
+ edgeTicksOf('x', plot.scales.x.type, plot.scales.y.type, scales?.x, xValues, xSpan, 3)
163
+ );
164
+ const yEdgeTicks = $derived(
165
+ edgeTicksOf('y', plot.scales.y.type, plot.scales.x.type, scales?.y, yValues, ySpan, 2)
147
166
  );
148
167
 
168
+ // Hands the plot the domain an axis widens to, only when it changes.
169
+ $effect(() => {
170
+ const next = { x: xEdgeTicks?.domain, y: yEdgeTicks?.domain };
171
+ untrack(() => {
172
+ for (const axis of ['x', 'y'] as const) {
173
+ const domain = next[axis];
174
+ const current = edgeDomains?.get(axis);
175
+ if (domain?.[0] === current?.[0] && domain?.[1] === current?.[1]) continue;
176
+ if (domain) edgeDomains?.set(axis, domain);
177
+ else edgeDomains?.delete(axis);
178
+ }
179
+ });
180
+ });
181
+
149
182
  const yTitleFontSize = $derived(scales?.y?.titleFontSize ?? cfg.axis.titleFontSize);
150
183
 
151
184
  /**
@@ -191,7 +224,7 @@
191
224
 
192
225
  {#snippet axisX()}
193
226
  <AxisX
194
- ticks={xTicksExplicit}
227
+ ticks={xEdgeTicks?.ticks}
195
228
  tickCount={xTickCount}
196
229
  interval={xTickInterval}
197
230
  tickSize={scales?.x?.tickSize ?? cfg.axis.xTickSize}
@@ -207,6 +240,7 @@
207
240
  {/if}
208
241
  {#snippet axisY()}
209
242
  <AxisY
243
+ ticks={yEdgeTicks?.ticks}
210
244
  tickCount={yTickCount}
211
245
  interval={yTickInterval}
212
246
  tickSize={scales?.y?.tickSize ?? cfg.axis.yTickSize}
@@ -14,6 +14,11 @@ declare function $$render<TData extends Record<string, unknown>>(): {
14
14
  left: number;
15
15
  };
16
16
  scales?: AxisBasedScalesConfig<TData>;
17
+ /**
18
+ * Where to leave the domain an axis widens to when an edge tick's
19
+ * `'extend'` resolved a collision (see `EdgeTicks`), for the plot to give its scale.
20
+ */
21
+ edgeDomains?: Map<"x" | "y", [number, number]>;
17
22
  };
18
23
  exports: {};
19
24
  bindings: "";
@@ -0,0 +1,15 @@
1
+ import type { AxisScale } from '../../types/layout/scales';
2
+ import type { AxisValue } from '../../types/layout/mapping';
3
+ /** An {@link AxisScale} turned into the options svelteplot's `<Plot x>`/`<Plot y>` accepts. */
4
+ export declare class SveltePlotScale {
5
+ #private;
6
+ /** Scale types the default `nice` applies to - every other type has its own tick semantics (or isn't numeric at all). */
7
+ static readonly CONTINUOUS_NUMERIC_TYPES: ReadonlySet<string>;
8
+ /**
9
+ * The scale without what the package handles itself - `grid`, `axisDisplay`
10
+ * and a vertical `label`, which `ScalesLayout` draws, and `niceTolerance`,
11
+ * which the "scales" phase applies - with a custom `sort` resolved into a
12
+ * `domain`, and `nice: true` on an unpinned numeric axis.
13
+ */
14
+ static build<TData>(scale: AxisScale<TData> | undefined, rows: TData[], getAxisValue: (d: TData) => AxisValue): AxisScale<TData> | undefined;
15
+ }
@@ -0,0 +1,79 @@
1
+ import { SvelteSet } from 'svelte/reactivity';
2
+ /** An {@link AxisScale} turned into the options svelteplot's `<Plot x>`/`<Plot y>` accepts. */
3
+ export class SveltePlotScale {
4
+ /** Scale types the default `nice` applies to - every other type has its own tick semantics (or isn't numeric at all). */
5
+ static CONTINUOUS_NUMERIC_TYPES = new Set([
6
+ 'auto',
7
+ 'linear',
8
+ 'pow',
9
+ 'sqrt',
10
+ 'log',
11
+ 'symlog'
12
+ ]);
13
+ /**
14
+ * The scale without what the package handles itself - `grid`, `axisDisplay`
15
+ * and a vertical `label`, which `ScalesLayout` draws, and `niceTolerance`,
16
+ * which the "scales" phase applies - with a custom `sort` resolved into a
17
+ * `domain`, and `nice: true` on an unpinned numeric axis.
18
+ */
19
+ static build(scale, rows, getAxisValue) {
20
+ const sorted = SveltePlotScale.#sorted(scale, rows, getAxisValue);
21
+ const wantsNice = scale?.domain == null &&
22
+ scale?.ticks == null &&
23
+ scale?.nice == null &&
24
+ (scale?.type == null || SveltePlotScale.CONTINUOUS_NUMERIC_TYPES.has(scale.type)) &&
25
+ SveltePlotScale.#isNumeric(rows, getAxisValue);
26
+ const removesAxis = scale?.axisDisplay === 'none';
27
+ if (!sorted && !wantsNice && !removesAxis)
28
+ return sorted;
29
+ const out = { ...sorted };
30
+ delete out.grid;
31
+ delete out.axisDisplay;
32
+ delete out.niceTolerance;
33
+ if (removesAxis)
34
+ out.axis = false;
35
+ if (scale?.labelOrientation === 'vertical')
36
+ out.label = false;
37
+ if (wantsNice)
38
+ out.nice = true;
39
+ return out;
40
+ }
41
+ /** svelteplot's `sort` option only does a fixed ascending sort; a custom `scale.sort` comparator is resolved into an explicit `domain` instead, which svelteplot honors as-is. */
42
+ static #sorted(scale, rows, getAxisValue) {
43
+ const sort = scale?.sort;
44
+ if (!sort || scale?.domain)
45
+ return scale;
46
+ const reps = SveltePlotScale.#uniqueRows(rows, getAxisValue);
47
+ if (reps.length === 0 || !reps.every((r) => typeof getAxisValue(r) === 'string'))
48
+ return scale;
49
+ const domain = [...reps].sort(sort).map(getAxisValue);
50
+ return { ...scale, domain };
51
+ }
52
+ /** One representative row per unique axis value (first occurrence in `rows`). */
53
+ static #uniqueRows(rows, getAxisValue) {
54
+ const seen = new SvelteSet();
55
+ const out = [];
56
+ for (const row of rows) {
57
+ const v = getAxisValue(row);
58
+ if (seen.has(v))
59
+ continue;
60
+ seen.add(v);
61
+ out.push(row);
62
+ }
63
+ return out;
64
+ }
65
+ /** Whether the axis' values are numeric; `nice` on a categorical axis would make svelteplot treat it as linear. */
66
+ static #isNumeric(rows, getAxisValue) {
67
+ let numericSeen = 0;
68
+ for (const row of rows) {
69
+ const v = getAxisValue(row);
70
+ if (v == null)
71
+ continue;
72
+ if (typeof v !== 'number')
73
+ return false;
74
+ if (++numericSeen >= 2)
75
+ return true;
76
+ }
77
+ return numericSeen > 0;
78
+ }
79
+ }
@@ -1,6 +1,7 @@
1
+ import { ProcessedPositions } from '../../phases/transforms/ProcessedPositions';
1
2
  import { Box } from '../margins/Box';
2
3
  import { resolveAccessor } from '../../plots/utils/accessors';
3
- import { SERIES_ID_KEY, SERIES_KEY, TransformsPhase } from '../../phases/transforms/TransformsPhase';
4
+ import { SERIES_ID_KEY, SERIES_KEY } from '../../phases/transforms/TransformsPhase';
4
5
  import { seriesIdOf } from '../../utils/grouping';
5
6
  import { Transforms } from '../../phases/transforms/Transforms';
6
7
  import { isGapValue } from '../../plots/utils/gaps';
@@ -139,7 +140,7 @@ export class ValueLabels {
139
140
  if (!processed)
140
141
  return [];
141
142
  const { font, anchor, format } = this;
142
- const position = TransformsPhase.processedPosition(processed);
143
+ const position = ProcessedPositions.of(processed);
143
144
  const topOf = (d) => position.y(d);
144
145
  const nameOf = (d) => String(d[SERIES_KEY]);
145
146
  const valueOf = ValueLabels.valueOf(state);
@@ -15,12 +15,15 @@
15
15
  currentValue,
16
16
  values,
17
17
  orientation = 'vertical',
18
+ format = (value: TimeValue) => String(value),
18
19
  width,
19
20
  height
20
21
  }: {
21
22
  currentValue: TimeValue;
22
23
  values: TimeValue[];
23
24
  orientation?: Orientation;
25
+ /** How a step's label reads. */
26
+ format?: (value: TimeValue) => string;
24
27
  width: number;
25
28
  height: number;
26
29
  } = $props();
@@ -72,7 +75,7 @@
72
75
  <Text
73
76
  data={baseValues}
74
77
  {...isHorizontal ? { x: identity, y: 0 } : { x: 0, y: identity }}
75
- text={(d: TimeValue) => `${d}`}
78
+ text={(d: TimeValue) => format(d)}
76
79
  {...textOffset}
77
80
  fontSize={rule.fontSize}
78
81
  fill="var(--dct-timeline-axis, currentColor)"
@@ -82,7 +85,7 @@
82
85
  <Text
83
86
  data={[currentValue]}
84
87
  {...isHorizontal ? { x: identity, y: 0 } : { x: 0, y: identity }}
85
- text={(d: TimeValue) => `${d}`}
88
+ text={(d: TimeValue) => format(d)}
86
89
  {...textOffset}
87
90
  fontSize={rule.activeFontSize}
88
91
  fontWeight={rule.activeFontWeight}
@@ -3,6 +3,8 @@ type $$ComponentProps = {
3
3
  currentValue: TimeValue;
4
4
  values: TimeValue[];
5
5
  orientation?: Orientation;
6
+ /** How a step's label reads. */
7
+ format?: (value: TimeValue) => string;
6
8
  width: number;
7
9
  height: number;
8
10
  };