@fundar/data-chart-telling 0.0.43 → 0.0.45

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.
@@ -20,9 +20,12 @@
20
20
  *
21
21
  * container ▸ timeline (one shared scrubber) ▸ facet (split per small-multiple) ▸ plot
22
22
  *
23
- * The leaf snippet receives `{ data, width, height, facetValue?, selectedValue? }`
24
- * and knows nothing about how that box or that data slice was produced, which
25
- * is what lets any plot (line, bar, heatmap, …) drop in unchanged.
23
+ * The leaf snippet receives `{ data, width, height, facetValue?, selectedValue?,
24
+ * margins? }` and knows nothing about how that box or that data slice was
25
+ * produced, which is what lets any plot (line, bar, heatmap, …) drop in
26
+ * unchanged. `margins` is only present when `facet.labelsForMargin` is
27
+ * given — forward it to the plot's own `margins` prop so every facet
28
+ * shares the same content box.
26
29
  */
27
30
  let {
28
31
  width,
@@ -83,17 +86,19 @@
83
86
  formatIndicator={facet.formatIndicator}
84
87
  indicator={facet.indicator}
85
88
  navButton={facet.navButton}
89
+ labelsForMargin={facet.labelsForMargin}
86
90
  width={w}
87
91
  height={h}
88
92
  >
89
- {#snippet cell({ facetValue, data: cellData, width: cw, height: ch })}
93
+ {#snippet cell({ facetValue, data: cellData, width: cw, height: ch, margins })}
90
94
  {@render plot({
91
95
  data: cellData,
92
96
  width: cw,
93
97
  height: ch,
94
98
  facetValue,
95
99
  selectedValue,
96
- legend: legendPlotContext
100
+ legend: legendPlotContext,
101
+ margins
97
102
  })}
98
103
  {/snippet}
99
104
  </FacetLayout>
@@ -32,20 +32,53 @@
32
32
  const xValues = $derived(data.map(getX));
33
33
  const yValues = $derived(data.map(getY));
34
34
 
35
- /** Caps tick count to the axis's distinct value count, never above what fits the pixel span. Skipped once the caller sets tick density directly. */
35
+ function naturalTickCount(span: number, spacing: number, min: number): number {
36
+ return Math.max(min, Math.round(span / spacing));
37
+ }
38
+
39
+ /** `[min, max]` of a pinned `scale.domain` (if numeric), else the extent of `values`. */
40
+ function axisExtent(scale: AxisBasedScalesConfig<TData>['x'], values: AxisValue[]): [number, number] | undefined {
41
+ const domainNumbers = scale?.domain?.filter((d): d is number => typeof d === 'number');
42
+ const source = domainNumbers && domainNumbers.length >= 2 ? domainNumbers : values.filter((v): v is number => typeof v === 'number');
43
+ if (source.length < 2) return undefined;
44
+ const lo = Math.min(...source);
45
+ const hi = Math.max(...source);
46
+ return hi > lo ? [lo, hi] : undefined;
47
+ }
48
+
49
+ /** Caps tick count to the axis's distinct value count, never above what fits the pixel span. Skipped once the caller sets tick density directly, or pins the domain. */
36
50
  function autoTickCount(scale: AxisBasedScalesConfig<TData>['x'], values: AxisValue[], span: number, spacing: number, min: number): number | undefined {
37
- if (scale?.ticks != null || scale?.tickSpacing != null || scale?.interval != null) return undefined;
51
+ if (scale?.ticks != null || scale?.tickSpacing != null || scale?.interval != null || scale?.domain != null) return undefined;
38
52
  const uniqueCount = new Set(values.map(String)).size;
39
- const natural = Math.max(min, Math.round(span / spacing));
53
+ const natural = naturalTickCount(span, spacing, min);
40
54
  return uniqueCount >= 2 && uniqueCount < natural ? uniqueCount : undefined;
41
55
  }
42
56
 
43
- const xTickCount = $derived(
44
- autoTickCount(scales?.x, xValues, Math.max(0, width - numericMargins.left - numericMargins.right), 80, 3),
45
- );
46
- const yTickCount = $derived(
47
- autoTickCount(scales?.y, yValues, Math.max(0, height - numericMargins.top - numericMargins.bottom), 50, 2),
48
- );
57
+ /** Forces a whole-unit tick interval on an integer domain that spans fewer integers than the pixel span would naturally want ticks for. */
58
+ function integerTickInterval(
59
+ scale: AxisBasedScalesConfig<TData>['x'],
60
+ values: AxisValue[],
61
+ span: number,
62
+ spacing: number,
63
+ min: number
64
+ ): number | undefined {
65
+ if (scale?.ticks != null || scale?.tickSpacing != null || scale?.interval != null) return undefined;
66
+ if (scale?.type != null && scale.type !== 'linear' && scale.type !== 'auto') return undefined;
67
+ const extent = axisExtent(scale, values);
68
+ if (!extent) return undefined;
69
+ const [lo, hi] = extent;
70
+ if (!Number.isInteger(lo) || !Number.isInteger(hi)) return undefined;
71
+ return hi - lo < naturalTickCount(span, spacing, min) ? 1 : undefined;
72
+ }
73
+
74
+ const xSpan = $derived(Math.max(0, width - numericMargins.left - numericMargins.right));
75
+ const ySpan = $derived(Math.max(0, height - numericMargins.top - numericMargins.bottom));
76
+
77
+ const xTickCount = $derived(autoTickCount(scales?.x, xValues, xSpan, 80, 3));
78
+ const yTickCount = $derived(autoTickCount(scales?.y, yValues, ySpan, 50, 2));
79
+
80
+ const xTickInterval = $derived(integerTickInterval(scales?.x, xValues, xSpan, 80, 3));
81
+ const yTickInterval = $derived(integerTickInterval(scales?.y, yValues, ySpan, 50, 2));
49
82
 
50
83
  /** Vertical Y-axis title placement, when `scales.y.labelOrientation === 'vertical'`. */
51
84
  const verticalYLabel = $derived.by(() => {
@@ -61,6 +94,7 @@
61
94
 
62
95
  <AxisX
63
96
  tickCount={xTickCount}
97
+ interval={xTickInterval}
64
98
  tickSize={scales?.x?.tickSize ?? cfg.axis.xTickSize}
65
99
  tickPadding={scales?.x?.tickPadding ?? cfg.axis.xTickPadding}
66
100
  tickFontSize={cfg.axis.fontSize}
@@ -68,6 +102,7 @@
68
102
  />
69
103
  <AxisY
70
104
  tickCount={yTickCount}
105
+ interval={yTickInterval}
71
106
  tickSize={scales?.y?.tickSize ?? cfg.axis.yTickSize}
72
107
  tickPadding={scales?.y?.tickPadding ?? cfg.axis.yTickPadding}
73
108
  tickFontSize={cfg.axis.fontSize}
@@ -1,6 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { TimeValue } from '../layout/timeline';
3
3
  import type { LegendPlotContext } from '../layout/legend';
4
+ import type { MarginConfig } from '../plots/props';
4
5
  export type ChartBoxContext = {
5
6
  width: number;
6
7
  height: number;
@@ -13,6 +14,8 @@ export type ChartPlotContext<TRow extends Record<string, unknown>> = {
13
14
  facetValue?: string;
14
15
  selectedValue?: TimeValue;
15
16
  legend?: LegendPlotContext;
17
+ /** Present only when faceted with `facet.labelsForMargin` — forward to the plot's own `margins` prop so every facet shares the same content box. */
18
+ margins?: MarginConfig;
16
19
  };
17
20
  export type ChartPlotSnippet<TRow extends Record<string, unknown>> = Snippet<[
18
21
  ChartPlotContext<TRow>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fundar/data-chart-telling",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"