@voila.dev/chart 6.0.125 → 6.0.127

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.
@@ -1 +1 @@
1
- {"version":3,"file":"axes.d.ts","sourceRoot":"","sources":["../../../src/core/guides/axes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EACX,kBAAkB,EAClB,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,UAAU,EACV,SAAS,EACT,MAAM,iBAAiB,CAAC;AAgBzB,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;CACxC;AAED,wBAAgB,SAAS,CACxB,KAAK,EAAE,kBAAkB,EACzB,OAAO,EAAE,gBAAgB,GAAG,SAAS,GACnC,aAAa,CAAC,UAAU,CAAC,CAM3B;AAED,wBAAgB,SAAS,CACxB,KAAK,EAAE,kBAAkB,EACzB,OAAO,EAAE,gBAAgB,GAAG,SAAS,EACrC,KAAK,EAAE,UAAU,GACf,MAAM,CAER;AAED,8DAA8D;AAC9D,wBAAgB,QAAQ,CACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,iBAAiB,GAC5B,MAAM,CASR;AAMD,+EAA+E;AAC/E,wBAAgB,UAAU,CACzB,KAAK,EAAE,gBAAgB,EACvB,UAAU,EAAE,MAAM,GAChB,MAAM,CAgBR;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAW5D;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAa3D;AAED,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;CAC1C;AAsBD,wBAAgB,UAAU,CACzB,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,OAAO,GACX,SAAS,CAwDX;AAED,wBAAgB,QAAQ,CACvB,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,MAAM,GAChB,SAAS,CAmEX"}
1
+ {"version":3,"file":"axes.d.ts","sourceRoot":"","sources":["../../../src/core/guides/axes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EACX,kBAAkB,EAClB,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,UAAU,EACV,SAAS,EACT,MAAM,iBAAiB,CAAC;AAgBzB,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;CACxC;AAED,wBAAgB,SAAS,CACxB,KAAK,EAAE,kBAAkB,EACzB,OAAO,EAAE,gBAAgB,GAAG,SAAS,GACnC,aAAa,CAAC,UAAU,CAAC,CAM3B;AAED,wBAAgB,SAAS,CACxB,KAAK,EAAE,kBAAkB,EACzB,OAAO,EAAE,gBAAgB,GAAG,SAAS,EACrC,KAAK,EAAE,UAAU,GACf,MAAM,CAER;AAOD,8DAA8D;AAC9D,wBAAgB,QAAQ,CACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,iBAAiB,GAC5B,MAAM,CASR;AAMD,+EAA+E;AAC/E,wBAAgB,UAAU,CACzB,KAAK,EAAE,gBAAgB,EACvB,UAAU,EAAE,MAAM,GAChB,MAAM,CAgBR;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAW5D;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAa3D;AAED,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;CAC1C;AAgCD,wBAAgB,UAAU,CACzB,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,OAAO,GACX,SAAS,CAwDX;AAED,wBAAgB,QAAQ,CACvB,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,MAAM,GAChB,SAAS,CAmEX"}
@@ -14,6 +14,9 @@ function axisTicks(scale, options) {
14
14
  function tickLabel(scale, options, value) {
15
15
  return options?.tickFormat?.(value) ?? scale.format(value);
16
16
  }
17
+ function tickKey(value) {
18
+ return value instanceof Date ? String(value.getTime()) : String(value);
19
+ }
17
20
  function truncate(text, maxWidth, fontSize, measureText) {
18
21
  if (measureText(text, fontSize) <= maxWidth) {
19
22
  return text;
@@ -63,14 +66,19 @@ function rightMargin(input) {
63
66
  const overhang = half - (scale.range[1] - scale.map(last));
64
67
  return Math.max(EDGE, overhang + 2);
65
68
  }
66
- function thinned(ticks, widths, positions) {
67
- if (ticks.length < 2) {
68
- return ticks.map((_tick, index) => index);
69
+ function thinned(texts, widths, positions) {
70
+ const distinct = texts.map((_text, index) => index).filter((index) => index === 0 || texts[index] !== texts[index - 1]);
71
+ if (distinct.length < 2) {
72
+ return distinct;
69
73
  }
70
- const spacing = Math.abs(positions[1] - positions[0]) || 1;
71
- const widest = Math.max(...widths);
74
+ const spacing = Math.min(
75
+ ...distinct.slice(1).map(
76
+ (index, at) => Math.abs(positions[index] - positions[distinct[at]])
77
+ )
78
+ ) || 1;
79
+ const widest = Math.max(...distinct.map((index) => widths[index]));
72
80
  const step = Math.max(1, Math.ceil((widest + LABEL_SPACING) / spacing));
73
- return ticks.map((_tick, index) => index).filter((index) => index % step === 0);
81
+ return distinct.filter((_index, at) => at % step === 0);
74
82
  }
75
83
  function bottomAxis(input, plot, grid) {
76
84
  const { scale, options, title, theme, measureText } = input;
@@ -78,7 +86,7 @@ function bottomAxis(input, plot, grid) {
78
86
  const positions = ticks.map((tick) => scale.center(tick));
79
87
  const gridNodes = grid ? ticks.map((tick, index) => ({
80
88
  kind: "line",
81
- key: `grid-x:${tickLabel(scale, options, tick)}`,
89
+ key: `grid-x:${tickKey(tick)}`,
82
90
  role: "grid",
83
91
  x1: positions[index],
84
92
  y1: plot.y,
@@ -92,10 +100,10 @@ function bottomAxis(input, plot, grid) {
92
100
  const texts = ticks.map((tick) => tickLabel(scale, options, tick));
93
101
  const widths = texts.map((text) => measureText(text, theme.fontSize));
94
102
  const baseline = plot.y + plot.height + TICK_GAP;
95
- const labels = thinned(ticks, widths, positions).map(
103
+ const labels = thinned(texts, widths, positions).map(
96
104
  (index) => ({
97
105
  kind: "text",
98
- key: `tick-x:${texts[index]}`,
106
+ key: `tick-x:${tickKey(ticks[index])}`,
99
107
  role: "axis",
100
108
  x: positions[index],
101
109
  y: baseline,
@@ -133,7 +141,7 @@ function leftAxis(input, plot, grid, chartWidth) {
133
141
  const positions = ticks.map((tick) => scale.center(tick));
134
142
  const gridNodes = grid ? ticks.map((tick, index) => ({
135
143
  kind: "line",
136
- key: `grid-y:${tickLabel(scale, options, tick)}`,
144
+ key: `grid-y:${tickKey(tick)}`,
137
145
  role: "grid",
138
146
  x1: plot.x,
139
147
  y1: positions[index],
@@ -155,10 +163,10 @@ function leftAxis(input, plot, grid, chartWidth) {
155
163
  )
156
164
  );
157
165
  const heights = texts.map(() => theme.fontSize);
158
- const labels = thinned(ticks, heights, positions).map(
166
+ const labels = thinned(texts, heights, positions).map(
159
167
  (index) => ({
160
168
  kind: "text",
161
- key: `tick-y:${texts[index]}`,
169
+ key: `tick-y:${tickKey(ticks[index])}`,
162
170
  role: "axis",
163
171
  x: plot.x - TICK_GAP,
164
172
  y: positions[index],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/core/guides/axes.ts"],
4
- "sourcesContent": ["import type { ChartAxisOptions } from \"#/core/define-chart.ts\";\nimport type {\n\tChartPositionScale,\n\tChartRect,\n\tChartTextMeasurer,\n\tChartTheme,\n\tChartValue,\n\tSceneNode,\n} from \"#/core/types.ts\";\n\n/**\n * The axes as scene nodes: grid lines behind the marks, tick labels and\n * titles in front. They are measured before the plot exists, so the margins\n * fit the widest label instead of a guessed constant.\n */\n\nconst TICK_GAP = 8;\nconst TITLE_GAP = 6;\nconst LABEL_SPACING = 8;\nconst EDGE = 8;\n/** A category label on a horizontal bar chart never takes more of the width than this. */\nconst MAX_CATEGORY_SHARE = 0.35;\nconst ELLIPSIS = \"\u2026\";\n\nexport interface AxisMeasureInput {\n\treadonly scale: ChartPositionScale;\n\treadonly options: ChartAxisOptions | undefined;\n\treadonly title: string | undefined;\n\treadonly theme: ChartTheme;\n\treadonly measureText: ChartTextMeasurer;\n}\n\nexport function axisTicks(\n\tscale: ChartPositionScale,\n\toptions: ChartAxisOptions | undefined,\n): ReadonlyArray<ChartValue> {\n\tconst ticks = options?.ticks;\n\tif (Array.isArray(ticks)) {\n\t\treturn ticks;\n\t}\n\treturn scale.ticks(typeof ticks === \"number\" ? ticks : undefined);\n}\n\nexport function tickLabel(\n\tscale: ChartPositionScale,\n\toptions: ChartAxisOptions | undefined,\n\tvalue: ChartValue,\n): string {\n\treturn options?.tickFormat?.(value) ?? scale.format(value);\n}\n\n/** Cuts a label down to `maxWidth`, ending on an ellipsis. */\nexport function truncate(\n\ttext: string,\n\tmaxWidth: number,\n\tfontSize: number,\n\tmeasureText: ChartTextMeasurer,\n): string {\n\tif (measureText(text, fontSize) <= maxWidth) {\n\t\treturn text;\n\t}\n\tlet kept = text;\n\twhile (kept.length > 1 && measureText(kept + ELLIPSIS, fontSize) > maxWidth) {\n\t\tkept = kept.slice(0, -1);\n\t}\n\treturn kept + ELLIPSIS;\n}\n\nfunction hidden(options: ChartAxisOptions | undefined): boolean {\n\treturn options?.axis === false;\n}\n\n/** Room the left axis needs: its widest tick label, the gap, and the title. */\nexport function leftMargin(\n\tinput: AxisMeasureInput,\n\tchartWidth: number,\n): number {\n\tconst { scale, options, title, theme, measureText } = input;\n\tif (hidden(options)) {\n\t\treturn EDGE;\n\t}\n\tconst discrete = scale.kind === \"band\" || scale.kind === \"point\";\n\tconst widest = Math.max(\n\t\t0,\n\t\t...axisTicks(scale, options).map((tick) =>\n\t\t\tmeasureText(tickLabel(scale, options, tick), theme.fontSize),\n\t\t),\n\t);\n\tconst labels = discrete\n\t\t? Math.min(widest, chartWidth * MAX_CATEGORY_SHARE)\n\t\t: widest;\n\treturn EDGE + labels + TICK_GAP + (title ? theme.fontSize + TITLE_GAP : 0);\n}\n\nexport function bottomMargin(input: AxisMeasureInput): number {\n\tconst { options, title, theme } = input;\n\tif (hidden(options)) {\n\t\treturn EDGE;\n\t}\n\treturn (\n\t\tTICK_GAP +\n\t\ttheme.fontSize +\n\t\tEDGE / 2 +\n\t\t(title ? theme.fontSize + TITLE_GAP : 0)\n\t);\n}\n\n/** Half the last x label hangs past the plot's right edge; leave it room. */\nexport function rightMargin(input: AxisMeasureInput): number {\n\tconst { scale, options, theme, measureText } = input;\n\tif (hidden(options) || scale.kind === \"band\" || scale.kind === \"point\") {\n\t\treturn EDGE;\n\t}\n\tconst ticks = axisTicks(scale, options);\n\tconst last = ticks[ticks.length - 1];\n\tif (last === undefined) {\n\t\treturn EDGE;\n\t}\n\tconst half = measureText(tickLabel(scale, options, last), theme.fontSize) / 2;\n\tconst overhang = half - (scale.range[1] - scale.map(last));\n\treturn Math.max(EDGE, overhang + 2);\n}\n\nexport interface AxisNodes {\n\treadonly grid: ReadonlyArray<SceneNode>;\n\treadonly labels: ReadonlyArray<SceneNode>;\n}\n\n/**\n * Every `step`-th label, so neighbours never overlap. The step is the same\n * across the axis: thinning by skipping single labels reads as noise.\n */\nfunction thinned(\n\tticks: ReadonlyArray<ChartValue>,\n\twidths: ReadonlyArray<number>,\n\tpositions: ReadonlyArray<number>,\n): ReadonlyArray<number> {\n\tif (ticks.length < 2) {\n\t\treturn ticks.map((_tick, index) => index);\n\t}\n\tconst spacing = Math.abs(positions[1] - positions[0]) || 1;\n\tconst widest = Math.max(...widths);\n\tconst step = Math.max(1, Math.ceil((widest + LABEL_SPACING) / spacing));\n\treturn ticks\n\t\t.map((_tick, index) => index)\n\t\t.filter((index) => index % step === 0);\n}\n\nexport function bottomAxis(\n\tinput: AxisMeasureInput,\n\tplot: ChartRect,\n\tgrid: boolean,\n): AxisNodes {\n\tconst { scale, options, title, theme, measureText } = input;\n\tconst ticks = axisTicks(scale, options);\n\tconst positions = ticks.map((tick) => scale.center(tick));\n\tconst gridNodes: SceneNode[] = grid\n\t\t? ticks.map((tick, index) => ({\n\t\t\t\tkind: \"line\",\n\t\t\t\tkey: `grid-x:${tickLabel(scale, options, tick)}`,\n\t\t\t\trole: \"grid\",\n\t\t\t\tx1: positions[index],\n\t\t\t\ty1: plot.y,\n\t\t\t\tx2: positions[index],\n\t\t\t\ty2: plot.y + plot.height,\n\t\t\t\tpaint: { stroke: theme.grid, strokeWidth: 1 },\n\t\t\t}))\n\t\t: [];\n\tif (hidden(options)) {\n\t\treturn { grid: gridNodes, labels: [] };\n\t}\n\tconst texts = ticks.map((tick) => tickLabel(scale, options, tick));\n\tconst widths = texts.map((text) => measureText(text, theme.fontSize));\n\tconst baseline = plot.y + plot.height + TICK_GAP;\n\tconst labels: SceneNode[] = thinned(ticks, widths, positions).map(\n\t\t(index) => ({\n\t\t\tkind: \"text\",\n\t\t\tkey: `tick-x:${texts[index]}`,\n\t\t\trole: \"axis\",\n\t\t\tx: positions[index],\n\t\t\ty: baseline,\n\t\t\ttext: texts[index],\n\t\t\tpaint: {\n\t\t\t\tfill: theme.muted,\n\t\t\t\tfontSize: theme.fontSize,\n\t\t\t\ttextAnchor: \"middle\",\n\t\t\t\tbaseline: \"top\",\n\t\t\t},\n\t\t}),\n\t);\n\tif (title) {\n\t\tlabels.push({\n\t\t\tkind: \"text\",\n\t\t\tkey: \"title-x\",\n\t\t\trole: \"axis-title\",\n\t\t\tx: plot.x + plot.width / 2,\n\t\t\ty: baseline + theme.fontSize + TITLE_GAP,\n\t\t\ttext: title,\n\t\t\tpaint: {\n\t\t\t\tfill: theme.muted,\n\t\t\t\tfontSize: theme.fontSize,\n\t\t\t\tfontWeight: 500,\n\t\t\t\ttextAnchor: \"middle\",\n\t\t\t\tbaseline: \"top\",\n\t\t\t},\n\t\t});\n\t}\n\treturn { grid: gridNodes, labels };\n}\n\nexport function leftAxis(\n\tinput: AxisMeasureInput,\n\tplot: ChartRect,\n\tgrid: boolean,\n\tchartWidth: number,\n): AxisNodes {\n\tconst { scale, options, title, theme, measureText } = input;\n\tconst ticks = axisTicks(scale, options);\n\tconst positions = ticks.map((tick) => scale.center(tick));\n\tconst gridNodes: SceneNode[] = grid\n\t\t? ticks.map((tick, index) => ({\n\t\t\t\tkind: \"line\",\n\t\t\t\tkey: `grid-y:${tickLabel(scale, options, tick)}`,\n\t\t\t\trole: \"grid\",\n\t\t\t\tx1: plot.x,\n\t\t\t\ty1: positions[index],\n\t\t\t\tx2: plot.x + plot.width,\n\t\t\t\ty2: positions[index],\n\t\t\t\tpaint: { stroke: theme.grid, strokeWidth: 1 },\n\t\t\t}))\n\t\t: [];\n\tif (hidden(options)) {\n\t\treturn { grid: gridNodes, labels: [] };\n\t}\n\tconst discrete = scale.kind === \"band\" || scale.kind === \"point\";\n\tconst maxWidth = discrete\n\t\t? chartWidth * MAX_CATEGORY_SHARE\n\t\t: Number.POSITIVE_INFINITY;\n\tconst texts = ticks.map((tick) =>\n\t\ttruncate(\n\t\t\ttickLabel(scale, options, tick),\n\t\t\tmaxWidth,\n\t\t\ttheme.fontSize,\n\t\t\tmeasureText,\n\t\t),\n\t);\n\tconst heights = texts.map(() => theme.fontSize);\n\tconst labels: SceneNode[] = thinned(ticks, heights, positions).map(\n\t\t(index) => ({\n\t\t\tkind: \"text\",\n\t\t\tkey: `tick-y:${texts[index]}`,\n\t\t\trole: \"axis\",\n\t\t\tx: plot.x - TICK_GAP,\n\t\t\ty: positions[index],\n\t\t\ttext: texts[index],\n\t\t\tpaint: {\n\t\t\t\tfill: theme.muted,\n\t\t\t\tfontSize: theme.fontSize,\n\t\t\t\ttextAnchor: \"end\",\n\t\t\t\tbaseline: \"middle\",\n\t\t\t},\n\t\t}),\n\t);\n\tif (title) {\n\t\tlabels.push({\n\t\t\tkind: \"text\",\n\t\t\tkey: \"title-y\",\n\t\t\trole: \"axis-title\",\n\t\t\tx: EDGE / 2 + theme.fontSize / 2,\n\t\t\ty: plot.y + plot.height / 2,\n\t\t\ttext: title,\n\t\t\trotate: -90,\n\t\t\tpaint: {\n\t\t\t\tfill: theme.muted,\n\t\t\t\tfontSize: theme.fontSize,\n\t\t\t\tfontWeight: 500,\n\t\t\t\ttextAnchor: \"middle\",\n\t\t\t\tbaseline: \"middle\",\n\t\t\t},\n\t\t});\n\t}\n\treturn { grid: gridNodes, labels };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { ChartAxisOptions } from \"#/core/define-chart.ts\";\nimport type {\n\tChartPositionScale,\n\tChartRect,\n\tChartTextMeasurer,\n\tChartTheme,\n\tChartValue,\n\tSceneNode,\n} from \"#/core/types.ts\";\n\n/**\n * The axes as scene nodes: grid lines behind the marks, tick labels and\n * titles in front. They are measured before the plot exists, so the margins\n * fit the widest label instead of a guessed constant.\n */\n\nconst TICK_GAP = 8;\nconst TITLE_GAP = 6;\nconst LABEL_SPACING = 8;\nconst EDGE = 8;\n/** A category label on a horizontal bar chart never takes more of the width than this. */\nconst MAX_CATEGORY_SHARE = 0.35;\nconst ELLIPSIS = \"\u2026\";\n\nexport interface AxisMeasureInput {\n\treadonly scale: ChartPositionScale;\n\treadonly options: ChartAxisOptions | undefined;\n\treadonly title: string | undefined;\n\treadonly theme: ChartTheme;\n\treadonly measureText: ChartTextMeasurer;\n}\n\nexport function axisTicks(\n\tscale: ChartPositionScale,\n\toptions: ChartAxisOptions | undefined,\n): ReadonlyArray<ChartValue> {\n\tconst ticks = options?.ticks;\n\tif (Array.isArray(ticks)) {\n\t\treturn ticks;\n\t}\n\treturn scale.ticks(typeof ticks === \"number\" ? ticks : undefined);\n}\n\nexport function tickLabel(\n\tscale: ChartPositionScale,\n\toptions: ChartAxisOptions | undefined,\n\tvalue: ChartValue,\n): string {\n\treturn options?.tickFormat?.(value) ?? scale.format(value);\n}\n\n/** Two ticks can share a label (\"11:00\" on two days), never a value. */\nfunction tickKey(value: ChartValue): string {\n\treturn value instanceof Date ? String(value.getTime()) : String(value);\n}\n\n/** Cuts a label down to `maxWidth`, ending on an ellipsis. */\nexport function truncate(\n\ttext: string,\n\tmaxWidth: number,\n\tfontSize: number,\n\tmeasureText: ChartTextMeasurer,\n): string {\n\tif (measureText(text, fontSize) <= maxWidth) {\n\t\treturn text;\n\t}\n\tlet kept = text;\n\twhile (kept.length > 1 && measureText(kept + ELLIPSIS, fontSize) > maxWidth) {\n\t\tkept = kept.slice(0, -1);\n\t}\n\treturn kept + ELLIPSIS;\n}\n\nfunction hidden(options: ChartAxisOptions | undefined): boolean {\n\treturn options?.axis === false;\n}\n\n/** Room the left axis needs: its widest tick label, the gap, and the title. */\nexport function leftMargin(\n\tinput: AxisMeasureInput,\n\tchartWidth: number,\n): number {\n\tconst { scale, options, title, theme, measureText } = input;\n\tif (hidden(options)) {\n\t\treturn EDGE;\n\t}\n\tconst discrete = scale.kind === \"band\" || scale.kind === \"point\";\n\tconst widest = Math.max(\n\t\t0,\n\t\t...axisTicks(scale, options).map((tick) =>\n\t\t\tmeasureText(tickLabel(scale, options, tick), theme.fontSize),\n\t\t),\n\t);\n\tconst labels = discrete\n\t\t? Math.min(widest, chartWidth * MAX_CATEGORY_SHARE)\n\t\t: widest;\n\treturn EDGE + labels + TICK_GAP + (title ? theme.fontSize + TITLE_GAP : 0);\n}\n\nexport function bottomMargin(input: AxisMeasureInput): number {\n\tconst { options, title, theme } = input;\n\tif (hidden(options)) {\n\t\treturn EDGE;\n\t}\n\treturn (\n\t\tTICK_GAP +\n\t\ttheme.fontSize +\n\t\tEDGE / 2 +\n\t\t(title ? theme.fontSize + TITLE_GAP : 0)\n\t);\n}\n\n/** Half the last x label hangs past the plot's right edge; leave it room. */\nexport function rightMargin(input: AxisMeasureInput): number {\n\tconst { scale, options, theme, measureText } = input;\n\tif (hidden(options) || scale.kind === \"band\" || scale.kind === \"point\") {\n\t\treturn EDGE;\n\t}\n\tconst ticks = axisTicks(scale, options);\n\tconst last = ticks[ticks.length - 1];\n\tif (last === undefined) {\n\t\treturn EDGE;\n\t}\n\tconst half = measureText(tickLabel(scale, options, last), theme.fontSize) / 2;\n\tconst overhang = half - (scale.range[1] - scale.map(last));\n\treturn Math.max(EDGE, overhang + 2);\n}\n\nexport interface AxisNodes {\n\treadonly grid: ReadonlyArray<SceneNode>;\n\treadonly labels: ReadonlyArray<SceneNode>;\n}\n\n/**\n * The first tick of each run that reads the same (\"9 oct.\" on four ticks six\n * hours apart says the day once), then every `step`-th of those, so neighbours\n * never overlap. The step is the same across the axis: thinning by skipping\n * single labels reads as noise.\n */\nfunction thinned(\n\ttexts: ReadonlyArray<string>,\n\twidths: ReadonlyArray<number>,\n\tpositions: ReadonlyArray<number>,\n): ReadonlyArray<number> {\n\tconst distinct = texts\n\t\t.map((_text, index) => index)\n\t\t.filter((index) => index === 0 || texts[index] !== texts[index - 1]);\n\tif (distinct.length < 2) {\n\t\treturn distinct;\n\t}\n\tconst spacing =\n\t\tMath.min(\n\t\t\t...distinct\n\t\t\t\t.slice(1)\n\t\t\t\t.map((index, at) =>\n\t\t\t\t\tMath.abs(positions[index] - positions[distinct[at]]),\n\t\t\t\t),\n\t\t) || 1;\n\tconst widest = Math.max(...distinct.map((index) => widths[index]));\n\tconst step = Math.max(1, Math.ceil((widest + LABEL_SPACING) / spacing));\n\treturn distinct.filter((_index, at) => at % step === 0);\n}\n\nexport function bottomAxis(\n\tinput: AxisMeasureInput,\n\tplot: ChartRect,\n\tgrid: boolean,\n): AxisNodes {\n\tconst { scale, options, title, theme, measureText } = input;\n\tconst ticks = axisTicks(scale, options);\n\tconst positions = ticks.map((tick) => scale.center(tick));\n\tconst gridNodes: SceneNode[] = grid\n\t\t? ticks.map((tick, index) => ({\n\t\t\t\tkind: \"line\",\n\t\t\t\tkey: `grid-x:${tickKey(tick)}`,\n\t\t\t\trole: \"grid\",\n\t\t\t\tx1: positions[index],\n\t\t\t\ty1: plot.y,\n\t\t\t\tx2: positions[index],\n\t\t\t\ty2: plot.y + plot.height,\n\t\t\t\tpaint: { stroke: theme.grid, strokeWidth: 1 },\n\t\t\t}))\n\t\t: [];\n\tif (hidden(options)) {\n\t\treturn { grid: gridNodes, labels: [] };\n\t}\n\tconst texts = ticks.map((tick) => tickLabel(scale, options, tick));\n\tconst widths = texts.map((text) => measureText(text, theme.fontSize));\n\tconst baseline = plot.y + plot.height + TICK_GAP;\n\tconst labels: SceneNode[] = thinned(texts, widths, positions).map(\n\t\t(index) => ({\n\t\t\tkind: \"text\",\n\t\t\tkey: `tick-x:${tickKey(ticks[index])}`,\n\t\t\trole: \"axis\",\n\t\t\tx: positions[index],\n\t\t\ty: baseline,\n\t\t\ttext: texts[index],\n\t\t\tpaint: {\n\t\t\t\tfill: theme.muted,\n\t\t\t\tfontSize: theme.fontSize,\n\t\t\t\ttextAnchor: \"middle\",\n\t\t\t\tbaseline: \"top\",\n\t\t\t},\n\t\t}),\n\t);\n\tif (title) {\n\t\tlabels.push({\n\t\t\tkind: \"text\",\n\t\t\tkey: \"title-x\",\n\t\t\trole: \"axis-title\",\n\t\t\tx: plot.x + plot.width / 2,\n\t\t\ty: baseline + theme.fontSize + TITLE_GAP,\n\t\t\ttext: title,\n\t\t\tpaint: {\n\t\t\t\tfill: theme.muted,\n\t\t\t\tfontSize: theme.fontSize,\n\t\t\t\tfontWeight: 500,\n\t\t\t\ttextAnchor: \"middle\",\n\t\t\t\tbaseline: \"top\",\n\t\t\t},\n\t\t});\n\t}\n\treturn { grid: gridNodes, labels };\n}\n\nexport function leftAxis(\n\tinput: AxisMeasureInput,\n\tplot: ChartRect,\n\tgrid: boolean,\n\tchartWidth: number,\n): AxisNodes {\n\tconst { scale, options, title, theme, measureText } = input;\n\tconst ticks = axisTicks(scale, options);\n\tconst positions = ticks.map((tick) => scale.center(tick));\n\tconst gridNodes: SceneNode[] = grid\n\t\t? ticks.map((tick, index) => ({\n\t\t\t\tkind: \"line\",\n\t\t\t\tkey: `grid-y:${tickKey(tick)}`,\n\t\t\t\trole: \"grid\",\n\t\t\t\tx1: plot.x,\n\t\t\t\ty1: positions[index],\n\t\t\t\tx2: plot.x + plot.width,\n\t\t\t\ty2: positions[index],\n\t\t\t\tpaint: { stroke: theme.grid, strokeWidth: 1 },\n\t\t\t}))\n\t\t: [];\n\tif (hidden(options)) {\n\t\treturn { grid: gridNodes, labels: [] };\n\t}\n\tconst discrete = scale.kind === \"band\" || scale.kind === \"point\";\n\tconst maxWidth = discrete\n\t\t? chartWidth * MAX_CATEGORY_SHARE\n\t\t: Number.POSITIVE_INFINITY;\n\tconst texts = ticks.map((tick) =>\n\t\ttruncate(\n\t\t\ttickLabel(scale, options, tick),\n\t\t\tmaxWidth,\n\t\t\ttheme.fontSize,\n\t\t\tmeasureText,\n\t\t),\n\t);\n\tconst heights = texts.map(() => theme.fontSize);\n\tconst labels: SceneNode[] = thinned(texts, heights, positions).map(\n\t\t(index) => ({\n\t\t\tkind: \"text\",\n\t\t\tkey: `tick-y:${tickKey(ticks[index])}`,\n\t\t\trole: \"axis\",\n\t\t\tx: plot.x - TICK_GAP,\n\t\t\ty: positions[index],\n\t\t\ttext: texts[index],\n\t\t\tpaint: {\n\t\t\t\tfill: theme.muted,\n\t\t\t\tfontSize: theme.fontSize,\n\t\t\t\ttextAnchor: \"end\",\n\t\t\t\tbaseline: \"middle\",\n\t\t\t},\n\t\t}),\n\t);\n\tif (title) {\n\t\tlabels.push({\n\t\t\tkind: \"text\",\n\t\t\tkey: \"title-y\",\n\t\t\trole: \"axis-title\",\n\t\t\tx: EDGE / 2 + theme.fontSize / 2,\n\t\t\ty: plot.y + plot.height / 2,\n\t\t\ttext: title,\n\t\t\trotate: -90,\n\t\t\tpaint: {\n\t\t\t\tfill: theme.muted,\n\t\t\t\tfontSize: theme.fontSize,\n\t\t\t\tfontWeight: 500,\n\t\t\t\ttextAnchor: \"middle\",\n\t\t\t\tbaseline: \"middle\",\n\t\t\t},\n\t\t});\n\t}\n\treturn { grid: gridNodes, labels };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voila.dev/chart",
3
- "version": "6.0.125",
3
+ "version": "6.0.127",
4
4
  "type": "module",
5
5
  "description": "Charts as data: a definition of marks, channels and scales, drawn to SVG or Canvas, keyboard and screen-reader accessible. The core has no dependencies; the heavy layouts (geo, hierarchy, sankey…) sit behind their own subpaths.",
6
6
  "license": "MIT",
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "sideEffects": false,
66
66
  "dependencies": {
67
- "@voila.dev/motion": "6.0.125",
67
+ "@voila.dev/motion": "6.0.127",
68
68
  "d3-contour": "4.0.2",
69
69
  "d3-delaunay": "6.0.4",
70
70
  "d3-force": "3.0.0",
@@ -49,6 +49,11 @@ export function tickLabel(
49
49
  return options?.tickFormat?.(value) ?? scale.format(value);
50
50
  }
51
51
 
52
+ /** Two ticks can share a label ("11:00" on two days), never a value. */
53
+ function tickKey(value: ChartValue): string {
54
+ return value instanceof Date ? String(value.getTime()) : String(value);
55
+ }
56
+
52
57
  /** Cuts a label down to `maxWidth`, ending on an ellipsis. */
53
58
  export function truncate(
54
59
  text: string,
@@ -127,23 +132,33 @@ export interface AxisNodes {
127
132
  }
128
133
 
129
134
  /**
130
- * Every `step`-th label, so neighbours never overlap. The step is the same
131
- * across the axis: thinning by skipping single labels reads as noise.
135
+ * The first tick of each run that reads the same ("9 oct." on four ticks six
136
+ * hours apart says the day once), then every `step`-th of those, so neighbours
137
+ * never overlap. The step is the same across the axis: thinning by skipping
138
+ * single labels reads as noise.
132
139
  */
133
140
  function thinned(
134
- ticks: ReadonlyArray<ChartValue>,
141
+ texts: ReadonlyArray<string>,
135
142
  widths: ReadonlyArray<number>,
136
143
  positions: ReadonlyArray<number>,
137
144
  ): ReadonlyArray<number> {
138
- if (ticks.length < 2) {
139
- return ticks.map((_tick, index) => index);
145
+ const distinct = texts
146
+ .map((_text, index) => index)
147
+ .filter((index) => index === 0 || texts[index] !== texts[index - 1]);
148
+ if (distinct.length < 2) {
149
+ return distinct;
140
150
  }
141
- const spacing = Math.abs(positions[1] - positions[0]) || 1;
142
- const widest = Math.max(...widths);
151
+ const spacing =
152
+ Math.min(
153
+ ...distinct
154
+ .slice(1)
155
+ .map((index, at) =>
156
+ Math.abs(positions[index] - positions[distinct[at]]),
157
+ ),
158
+ ) || 1;
159
+ const widest = Math.max(...distinct.map((index) => widths[index]));
143
160
  const step = Math.max(1, Math.ceil((widest + LABEL_SPACING) / spacing));
144
- return ticks
145
- .map((_tick, index) => index)
146
- .filter((index) => index % step === 0);
161
+ return distinct.filter((_index, at) => at % step === 0);
147
162
  }
148
163
 
149
164
  export function bottomAxis(
@@ -157,7 +172,7 @@ export function bottomAxis(
157
172
  const gridNodes: SceneNode[] = grid
158
173
  ? ticks.map((tick, index) => ({
159
174
  kind: "line",
160
- key: `grid-x:${tickLabel(scale, options, tick)}`,
175
+ key: `grid-x:${tickKey(tick)}`,
161
176
  role: "grid",
162
177
  x1: positions[index],
163
178
  y1: plot.y,
@@ -172,10 +187,10 @@ export function bottomAxis(
172
187
  const texts = ticks.map((tick) => tickLabel(scale, options, tick));
173
188
  const widths = texts.map((text) => measureText(text, theme.fontSize));
174
189
  const baseline = plot.y + plot.height + TICK_GAP;
175
- const labels: SceneNode[] = thinned(ticks, widths, positions).map(
190
+ const labels: SceneNode[] = thinned(texts, widths, positions).map(
176
191
  (index) => ({
177
192
  kind: "text",
178
- key: `tick-x:${texts[index]}`,
193
+ key: `tick-x:${tickKey(ticks[index])}`,
179
194
  role: "axis",
180
195
  x: positions[index],
181
196
  y: baseline,
@@ -220,7 +235,7 @@ export function leftAxis(
220
235
  const gridNodes: SceneNode[] = grid
221
236
  ? ticks.map((tick, index) => ({
222
237
  kind: "line",
223
- key: `grid-y:${tickLabel(scale, options, tick)}`,
238
+ key: `grid-y:${tickKey(tick)}`,
224
239
  role: "grid",
225
240
  x1: plot.x,
226
241
  y1: positions[index],
@@ -245,10 +260,10 @@ export function leftAxis(
245
260
  ),
246
261
  );
247
262
  const heights = texts.map(() => theme.fontSize);
248
- const labels: SceneNode[] = thinned(ticks, heights, positions).map(
263
+ const labels: SceneNode[] = thinned(texts, heights, positions).map(
249
264
  (index) => ({
250
265
  kind: "text",
251
- key: `tick-y:${texts[index]}`,
266
+ key: `tick-y:${tickKey(ticks[index])}`,
252
267
  role: "axis",
253
268
  x: plot.x - TICK_GAP,
254
269
  y: positions[index],