@motionscript/charts 0.1.0 → 0.1.1-alpha.1

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 (215) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +98 -28
  3. package/dist/bar-chart/bar-chart.d.ts +242 -119
  4. package/dist/bar-chart/bar-chart.d.ts.map +1 -1
  5. package/dist/bar-chart/bar-chart.js +231 -260
  6. package/dist/bar-chart/bar-chart.js.map +1 -1
  7. package/dist/bar-chart/category-axis.d.ts +16 -38
  8. package/dist/bar-chart/category-axis.d.ts.map +1 -1
  9. package/dist/bar-chart/category-axis.js +59 -68
  10. package/dist/bar-chart/category-axis.js.map +1 -1
  11. package/dist/bar-chart/chart-bars.d.ts +60 -91
  12. package/dist/bar-chart/chart-bars.d.ts.map +1 -1
  13. package/dist/bar-chart/chart-bars.js +88 -125
  14. package/dist/bar-chart/chart-bars.js.map +1 -1
  15. package/dist/bar-chart/index.d.ts +5 -13
  16. package/dist/bar-chart/index.d.ts.map +1 -1
  17. package/dist/bar-chart/index.js +2 -9
  18. package/dist/bar-chart/index.js.map +1 -1
  19. package/dist/bar-chart/shared.d.ts +40 -132
  20. package/dist/bar-chart/shared.d.ts.map +1 -1
  21. package/dist/bar-chart/shared.js +70 -158
  22. package/dist/bar-chart/shared.js.map +1 -1
  23. package/dist/browser/chunks/chunk-R3Q5EE6E.js +2 -0
  24. package/dist/browser/chunks/chunk-R3Q5EE6E.js.map +7 -0
  25. package/dist/browser/index.js +1 -1
  26. package/dist/browser/index.js.map +4 -4
  27. package/dist/browser/kit.js +1 -1
  28. package/dist/browser/manifest.json +1 -1
  29. package/dist/engine.d.ts +8 -17
  30. package/dist/engine.d.ts.map +1 -1
  31. package/dist/engine.js +8 -17
  32. package/dist/engine.js.map +1 -1
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/kit/cartesian.d.ts +49 -35
  38. package/dist/kit/cartesian.d.ts.map +1 -1
  39. package/dist/kit/cartesian.js +80 -42
  40. package/dist/kit/cartesian.js.map +1 -1
  41. package/dist/kit/chart-canvas.d.ts +37 -40
  42. package/dist/kit/chart-canvas.d.ts.map +1 -1
  43. package/dist/kit/chart-canvas.js +53 -47
  44. package/dist/kit/chart-canvas.js.map +1 -1
  45. package/dist/kit/chart-legend.d.ts +16 -59
  46. package/dist/kit/chart-legend.d.ts.map +1 -1
  47. package/dist/kit/chart-legend.js +38 -76
  48. package/dist/kit/chart-legend.js.map +1 -1
  49. package/dist/kit/entries.d.ts +2 -0
  50. package/dist/kit/entries.d.ts.map +1 -1
  51. package/dist/kit/entries.js +2 -0
  52. package/dist/kit/entries.js.map +1 -1
  53. package/dist/kit/horizontal-axis.d.ts +77 -0
  54. package/dist/kit/horizontal-axis.d.ts.map +1 -0
  55. package/dist/kit/horizontal-axis.js +99 -0
  56. package/dist/kit/horizontal-axis.js.map +1 -0
  57. package/dist/kit/motion.d.ts +16 -3
  58. package/dist/kit/motion.d.ts.map +1 -1
  59. package/dist/kit/motion.js +15 -1
  60. package/dist/kit/motion.js.map +1 -1
  61. package/dist/{pie-chart → kit}/pie-legend.d.ts +14 -29
  62. package/dist/kit/pie-legend.d.ts.map +1 -0
  63. package/dist/kit/pie-legend.js +93 -0
  64. package/dist/kit/pie-legend.js.map +1 -0
  65. package/dist/kit/polar.d.ts +31 -0
  66. package/dist/kit/polar.d.ts.map +1 -0
  67. package/dist/kit/polar.js +33 -0
  68. package/dist/kit/polar.js.map +1 -0
  69. package/dist/kit/shared.d.ts +60 -214
  70. package/dist/kit/shared.d.ts.map +1 -1
  71. package/dist/kit/shared.js +91 -291
  72. package/dist/kit/shared.js.map +1 -1
  73. package/dist/kit/style.d.ts +254 -0
  74. package/dist/kit/style.d.ts.map +1 -0
  75. package/dist/kit/style.js +197 -0
  76. package/dist/kit/style.js.map +1 -0
  77. package/dist/kit/vertical-axis.d.ts +36 -45
  78. package/dist/kit/vertical-axis.d.ts.map +1 -1
  79. package/dist/kit/vertical-axis.js +69 -81
  80. package/dist/kit/vertical-axis.js.map +1 -1
  81. package/dist/line-chart/chart-line.d.ts +107 -40
  82. package/dist/line-chart/chart-line.d.ts.map +1 -1
  83. package/dist/line-chart/chart-line.js +172 -89
  84. package/dist/line-chart/chart-line.js.map +1 -1
  85. package/dist/line-chart/chart-region.d.ts +14 -27
  86. package/dist/line-chart/chart-region.d.ts.map +1 -1
  87. package/dist/line-chart/chart-region.js +13 -28
  88. package/dist/line-chart/chart-region.js.map +1 -1
  89. package/dist/line-chart/index.d.ts +5 -18
  90. package/dist/line-chart/index.d.ts.map +1 -1
  91. package/dist/line-chart/index.js +3 -16
  92. package/dist/line-chart/index.js.map +1 -1
  93. package/dist/line-chart/line-chart.d.ts +272 -119
  94. package/dist/line-chart/line-chart.d.ts.map +1 -1
  95. package/dist/line-chart/line-chart.js +309 -257
  96. package/dist/line-chart/line-chart.js.map +1 -1
  97. package/dist/line-chart/shared.d.ts +73 -130
  98. package/dist/line-chart/shared.d.ts.map +1 -1
  99. package/dist/line-chart/shared.js +195 -156
  100. package/dist/line-chart/shared.js.map +1 -1
  101. package/dist/line-chart/x-scale.d.ts +16 -55
  102. package/dist/line-chart/x-scale.d.ts.map +1 -1
  103. package/dist/line-chart/x-scale.js +44 -111
  104. package/dist/line-chart/x-scale.js.map +1 -1
  105. package/dist/nodes.d.ts +3 -13
  106. package/dist/nodes.d.ts.map +1 -1
  107. package/dist/nodes.js +9 -13
  108. package/dist/nodes.js.map +1 -1
  109. package/dist/pie-chart/index.d.ts +3 -15
  110. package/dist/pie-chart/index.d.ts.map +1 -1
  111. package/dist/pie-chart/index.js +1 -11
  112. package/dist/pie-chart/index.js.map +1 -1
  113. package/dist/pie-chart/pie-chart.d.ts +147 -85
  114. package/dist/pie-chart/pie-chart.d.ts.map +1 -1
  115. package/dist/pie-chart/pie-chart.js +65 -86
  116. package/dist/pie-chart/pie-chart.js.map +1 -1
  117. package/dist/pie-chart/pie-ring.d.ts +65 -196
  118. package/dist/pie-chart/pie-ring.d.ts.map +1 -1
  119. package/dist/pie-chart/pie-ring.js +154 -292
  120. package/dist/pie-chart/pie-ring.js.map +1 -1
  121. package/dist/pie-chart/shared.d.ts +78 -172
  122. package/dist/pie-chart/shared.d.ts.map +1 -1
  123. package/dist/pie-chart/shared.js +54 -177
  124. package/dist/pie-chart/shared.js.map +1 -1
  125. package/dist/radar-chart/index.d.ts +5 -0
  126. package/dist/radar-chart/index.d.ts.map +1 -0
  127. package/dist/radar-chart/index.js +5 -0
  128. package/dist/radar-chart/index.js.map +1 -0
  129. package/dist/radar-chart/radar-chart.d.ts +218 -0
  130. package/dist/radar-chart/radar-chart.d.ts.map +1 -0
  131. package/dist/radar-chart/radar-chart.js +224 -0
  132. package/dist/radar-chart/radar-chart.js.map +1 -0
  133. package/dist/radar-chart/radar-grid.d.ts +43 -0
  134. package/dist/radar-chart/radar-grid.d.ts.map +1 -0
  135. package/dist/radar-chart/radar-grid.js +61 -0
  136. package/dist/radar-chart/radar-grid.js.map +1 -0
  137. package/dist/radar-chart/radar-series.d.ts +59 -0
  138. package/dist/radar-chart/radar-series.d.ts.map +1 -0
  139. package/dist/radar-chart/radar-series.js +71 -0
  140. package/dist/radar-chart/radar-series.js.map +1 -0
  141. package/dist/radar-chart/shared.d.ts +73 -0
  142. package/dist/radar-chart/shared.d.ts.map +1 -0
  143. package/dist/radar-chart/shared.js +123 -0
  144. package/dist/radar-chart/shared.js.map +1 -0
  145. package/dist/radial-chart/index.d.ts +6 -0
  146. package/dist/radial-chart/index.d.ts.map +1 -0
  147. package/dist/radial-chart/index.js +4 -0
  148. package/dist/radial-chart/index.js.map +1 -0
  149. package/dist/radial-chart/radial-chart.d.ts +216 -0
  150. package/dist/radial-chart/radial-chart.d.ts.map +1 -0
  151. package/dist/radial-chart/radial-chart.js +174 -0
  152. package/dist/radial-chart/radial-chart.js.map +1 -0
  153. package/dist/radial-chart/radial-rings.d.ts +121 -0
  154. package/dist/radial-chart/radial-rings.d.ts.map +1 -0
  155. package/dist/radial-chart/radial-rings.js +273 -0
  156. package/dist/radial-chart/radial-rings.js.map +1 -0
  157. package/dist/radial-chart/shared.d.ts +73 -0
  158. package/dist/radial-chart/shared.d.ts.map +1 -0
  159. package/dist/radial-chart/shared.js +79 -0
  160. package/dist/radial-chart/shared.js.map +1 -0
  161. package/package.json +4 -4
  162. package/registry.json +21 -6
  163. package/src/bar-chart/bar-chart.ts +274 -315
  164. package/src/bar-chart/category-axis.ts +53 -88
  165. package/src/bar-chart/chart-bars.ts +108 -205
  166. package/src/bar-chart/index.ts +8 -19
  167. package/src/bar-chart/shared.ts +81 -309
  168. package/src/engine.ts +8 -17
  169. package/src/index.ts +2 -0
  170. package/src/kit/cartesian.ts +118 -55
  171. package/src/kit/chart-canvas.ts +50 -80
  172. package/src/kit/chart-legend.ts +36 -97
  173. package/src/kit/entries.ts +4 -1
  174. package/src/kit/horizontal-axis.ts +114 -0
  175. package/src/kit/motion.ts +18 -2
  176. package/src/{pie-chart → kit}/pie-legend.ts +25 -65
  177. package/src/kit/polar.ts +54 -0
  178. package/src/kit/shared.ts +104 -487
  179. package/src/kit/style.ts +257 -0
  180. package/src/kit/vertical-axis.ts +81 -115
  181. package/src/line-chart/chart-line.ts +185 -91
  182. package/src/line-chart/chart-region.ts +14 -31
  183. package/src/line-chart/index.ts +14 -22
  184. package/src/line-chart/line-chart.ts +377 -315
  185. package/src/line-chart/shared.ts +229 -304
  186. package/src/line-chart/x-scale.ts +44 -121
  187. package/src/nodes.ts +9 -13
  188. package/src/pie-chart/index.ts +3 -35
  189. package/src/pie-chart/pie-chart.ts +83 -134
  190. package/src/pie-chart/pie-ring.ts +162 -368
  191. package/src/pie-chart/shared.ts +92 -379
  192. package/src/radar-chart/index.ts +4 -0
  193. package/src/radar-chart/radar-chart.ts +287 -0
  194. package/src/radar-chart/radar-grid.ts +78 -0
  195. package/src/radar-chart/radar-series.ts +88 -0
  196. package/src/radar-chart/shared.ts +146 -0
  197. package/src/radial-chart/index.ts +5 -0
  198. package/src/radial-chart/radial-chart.ts +210 -0
  199. package/src/radial-chart/radial-rings.ts +328 -0
  200. package/src/radial-chart/shared.ts +114 -0
  201. package/dist/browser/chunks/chunk-TJNHLQGV.js +0 -2
  202. package/dist/browser/chunks/chunk-TJNHLQGV.js.map +0 -7
  203. package/dist/kit/chart-border.d.ts +0 -27
  204. package/dist/kit/chart-border.d.ts.map +0 -1
  205. package/dist/kit/chart-border.js +0 -28
  206. package/dist/kit/chart-border.js.map +0 -1
  207. package/dist/line-chart/horizontal-axis.d.ts +0 -79
  208. package/dist/line-chart/horizontal-axis.d.ts.map +0 -1
  209. package/dist/line-chart/horizontal-axis.js +0 -129
  210. package/dist/line-chart/horizontal-axis.js.map +0 -1
  211. package/dist/pie-chart/pie-legend.d.ts.map +0 -1
  212. package/dist/pie-chart/pie-legend.js +0 -117
  213. package/dist/pie-chart/pie-legend.js.map +0 -1
  214. package/src/kit/chart-border.ts +0 -50
  215. package/src/line-chart/horizontal-axis.ts +0 -151
@@ -1,77 +1,38 @@
1
1
  /**
2
- * What a line chart's x column *means* — the one question that has to be
3
- * answered before a row can be given a position along the bottom of the plot.
2
+ * What a line chart's x column means: a number (plotted as is), an instant
3
+ * (`Jan`, `2026-W12`; plotted by time, so a gap in the data reads as a gap), or
4
+ * anything else (the row's position, evenly spaced).
4
5
  *
5
- * A y column is always a magnitude. An x column is one of three things, and
6
- * which one is a property of the data rather than something the author states:
7
- *
8
- * - **numeric** — `0, 5, 10` — plot the number.
9
- * - **time** — `Jan`, `2026-03`, `2026-W12`, `2026-03-14` — plot the instant,
10
- * so samples a month apart sit a month apart and a gap in the data reads as a
11
- * gap.
12
- * - **ordinal** — anything else — plot the row's position, evenly spaced.
13
- *
14
- * Before this, every x column went through `num()`, which reads a non-numeric
15
- * cell as `0`. A `Month` column of `Jan…Dec` therefore stacked all twelve
16
- * points on one x and drew a vertical line — no error, no empty chart, just a
17
- * wrong picture. That is the failure this module exists to remove: every column
18
- * now plots as *something*, and the something is the reading a person would
19
- * give it.
20
- *
21
- * The captions come back off the rows rather than out of a formatter. A tick on
22
- * a time or ordinal scale is chosen *from the data* (see {@link XScale.ticks}),
23
- * so the label it wants is the cell the author wrote — `Jan`, not `1970-01-01`,
24
- * and `2026-W12`, not the Monday it resolves to. Only a tick that is not a
25
- * sample — an author-set range, or a `zoomTo` mid-flight — needs formatting,
26
- * and that is the one case {@link formatInstant} covers.
6
+ * A tick on a time or ordinal scale is a sample, so its caption is the cell the
7
+ * author wrote; only a tick that is not (an author-set range, a `zoomTo`
8
+ * mid-flight) is formatted, by {@link formatInstant}.
27
9
  */
28
10
  import type { DataRecord } from "@motionscript/sdk";
29
11
  import { type Range } from "../kit/shared.js";
30
12
  /** How an x column's cells become positions along the axis. */
31
13
  export type XScaleKind = "numeric" | "time" | "ordinal";
32
- /**
33
- * An x column resolved into positions, captions and ticks.
34
- *
35
- * The three kinds differ in all three answers, so they are three closures built
36
- * once per (data, column) rather than a kind tag every caller switches on.
37
- */
14
+ /** An x column resolved into positions, captions and ticks: three closures built once per (data, column), not a tag callers switch on. */
38
15
  export interface XScale {
39
16
  kind: XScaleKind;
40
17
  /** Position of row `index` along the axis, in this scale's own units. */
41
18
  at(index: number): number;
42
- /** `[min, max]` across the rows — the scale when no range is set. */
19
+ /** `[min, max]` across the rows: the scale when no range is set. */
43
20
  extent: Range;
44
- /** The caption for a tick at `value`. */
45
21
  label(value: number): string;
46
22
  /** `divisions + 1` tick values spanning `range`, ends included. */
47
23
  ticks(range: Range, divisions: number): number[];
48
24
  }
49
25
  /**
50
- * One cell read as an instant, or `null` when it isn't one.
51
- *
52
- * Deliberately **not** `Date.parse`. That accepts `"5"`, `"Q3"` and a good deal
53
- * else besides, differs between engines on anything non-ISO, and reads a bare
54
- * `2026-03-14` as UTC but `2026/03/14` as local — three ways for a column to be
55
- * silently misread as time. Every form below is matched explicitly, and an
56
- * ambiguous all-numeric date (`03/04/2026` — March or April, depending on where
57
- * the author lives) is deliberately not one of them.
26
+ * One cell read as an instant, or `null`. Not `Date.parse`, which accepts `"5"`
27
+ * and `"Q3"`, differs between engines off ISO, and reads `2026-03-14` as UTC but
28
+ * `2026/03/14` as local. Every form is matched explicitly, and an ambiguous
29
+ * all-numeric date (`03/04/2026`) is not one of them.
58
30
  */
59
31
  export declare function parseInstant(raw: unknown): number | null;
60
- /**
61
- * An instant captioned at the coarsest granularity the axis's own span can tell
62
- * apart — a decade of samples reads as years, a year of them as months.
63
- *
64
- * Only reached for a tick that is *not* one of the rows (an author-set range, a
65
- * `zoomTo` part-way through). A tick that is a sample is captioned with the
66
- * cell the author wrote; see the note at the top of this file.
67
- */
32
+ /** An instant captioned at the coarsest granularity the span tells apart: a decade reads as years, a year as months. */
68
33
  export declare function formatInstant(value: number, span: number): string;
69
- /**
70
- * How `field` positions the rows: the number it holds, the instant it names, or
71
- * failing both, the row's own place in the file.
72
- *
73
- * Kept per `data` array: a chart's bound props ask a dozen times a frame, and
74
- * parsing every row's instant each time was most of a frame.
75
- */
34
+ /** Up to this many samples in view, every one is a tick. */
35
+ export declare const EVERY_SAMPLE = 12;
36
+ /** Kept per `data` array: bound props ask a dozen times a frame, and parsing every row's instant each time was most of a frame. */
76
37
  export declare function resolveXScale(data: DataRecord[], field: string, pattern?: string): XScale;
77
38
  //# sourceMappingURL=x-scale.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"x-scale.d.ts","sourceRoot":"","sources":["../../src/line-chart/x-scale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAEnD,OAAO,EAKL,KAAK,KAAK,EACX,MAAM,eAAe,CAAA;AAGtB,+DAA+D;AAC/D,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAA;AAEvD;;;;;GAKG;AACH,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,UAAU,CAAA;IAChB,yEAAyE;IACzE,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB,qEAAqE;IACrE,MAAM,EAAE,KAAK,CAAA;IACb,yCAAyC;IACzC,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IAC5B,mEAAmE;IACnE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CACjD;AAmDD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAkDxD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAQjE;AA0HD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAAE,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,CAOR"}
1
+ {"version":3,"file":"x-scale.d.ts","sourceRoot":"","sources":["../../src/line-chart/x-scale.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAEnD,OAAO,EAKL,KAAK,KAAK,EACX,MAAM,eAAe,CAAA;AAGtB,+DAA+D;AAC/D,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAA;AAEvD,0IAA0I;AAC1I,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,UAAU,CAAA;IAChB,yEAAyE;IACzE,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB,oEAAoE;IACpE,MAAM,EAAE,KAAK,CAAA;IACb,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IAC5B,mEAAmE;IACnE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CACjD;AAmCD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAkDxD;AAED,wHAAwH;AACxH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAQjE;AAmDD,4DAA4D;AAC5D,eAAO,MAAM,YAAY,KAAK,CAAA;AAwD9B,mIAAmI;AACnI,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAAE,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,CAOR"}
@@ -1,33 +1,14 @@
1
1
  /**
2
- * What a line chart's x column *means* — the one question that has to be
3
- * answered before a row can be given a position along the bottom of the plot.
2
+ * What a line chart's x column means: a number (plotted as is), an instant
3
+ * (`Jan`, `2026-W12`; plotted by time, so a gap in the data reads as a gap), or
4
+ * anything else (the row's position, evenly spaced).
4
5
  *
5
- * A y column is always a magnitude. An x column is one of three things, and
6
- * which one is a property of the data rather than something the author states:
7
- *
8
- * - **numeric** — `0, 5, 10` — plot the number.
9
- * - **time** — `Jan`, `2026-03`, `2026-W12`, `2026-03-14` — plot the instant,
10
- * so samples a month apart sit a month apart and a gap in the data reads as a
11
- * gap.
12
- * - **ordinal** — anything else — plot the row's position, evenly spaced.
13
- *
14
- * Before this, every x column went through `num()`, which reads a non-numeric
15
- * cell as `0`. A `Month` column of `Jan…Dec` therefore stacked all twelve
16
- * points on one x and drew a vertical line — no error, no empty chart, just a
17
- * wrong picture. That is the failure this module exists to remove: every column
18
- * now plots as *something*, and the something is the reading a person would
19
- * give it.
20
- *
21
- * The captions come back off the rows rather than out of a formatter. A tick on
22
- * a time or ordinal scale is chosen *from the data* (see {@link XScale.ticks}),
23
- * so the label it wants is the cell the author wrote — `Jan`, not `1970-01-01`,
24
- * and `2026-W12`, not the Monday it resolves to. Only a tick that is not a
25
- * sample — an author-set range, or a `zoomTo` mid-flight — needs formatting,
26
- * and that is the one case {@link formatInstant} covers.
6
+ * A tick on a time or ordinal scale is a sample, so its caption is the cell the
7
+ * author wrote; only a tick that is not (an author-set range, a `zoomTo`
8
+ * mid-flight) is formatted, by {@link formatInstant}.
27
9
  */
28
10
  import { formatMarker, markerValues, num, text, } from "../kit/shared.js";
29
11
  import { formatNumber } from "@motionscript/sdk/component";
30
- // ---- Time parsing --------------------------------------------------------
31
12
  const DAY = 86_400_000;
32
13
  const MONTHS = [
33
14
  "january",
@@ -43,28 +24,14 @@ const MONTHS = [
43
24
  "november",
44
25
  "december",
45
26
  ];
46
- /**
47
- * The year a cell that names no year is dated into.
48
- *
49
- * Any year would place `Jan…Dec` in the right order at the right spacing; a
50
- * leap year is the one that also spaces February correctly against a column
51
- * that *does* carry years, so the two read alike.
52
- */
27
+ /** The year a cell naming none is dated into; a leap year, so February spaces the same as in a column that carries years. */
53
28
  const NOMINAL_YEAR = 2024;
54
- /**
55
- * Month index for any unambiguous abbreviation of a month name — `jan`, `sept`,
56
- * `january` — or -1.
57
- *
58
- * Three letters minimum, and then a prefix match, which is what lets `sept`
59
- * through without letting `janitor` through: a longer word has to still *be*
60
- * the month.
61
- */
29
+ /** Month index for `jan`, `sept` or `january`, else -1: three letters minimum, then a prefix match, so `sept` passes and `janitor` does not. */
62
30
  function monthIndex(word) {
63
31
  if (word.length < 3)
64
32
  return -1;
65
33
  return MONTHS.findIndex((month) => month.startsWith(word));
66
34
  }
67
- /** The Monday of ISO week `week` in `year`, as an epoch timestamp. */
68
35
  function isoWeekStart(year, week) {
69
36
  // ISO week 1 is the week containing 4 January, and weeks start on Monday.
70
37
  const jan4 = Date.UTC(year, 0, 4);
@@ -72,14 +39,10 @@ function isoWeekStart(year, week) {
72
39
  return jan4 - weekday * DAY + (week - 1) * 7 * DAY;
73
40
  }
74
41
  /**
75
- * One cell read as an instant, or `null` when it isn't one.
76
- *
77
- * Deliberately **not** `Date.parse`. That accepts `"5"`, `"Q3"` and a good deal
78
- * else besides, differs between engines on anything non-ISO, and reads a bare
79
- * `2026-03-14` as UTC but `2026/03/14` as local — three ways for a column to be
80
- * silently misread as time. Every form below is matched explicitly, and an
81
- * ambiguous all-numeric date (`03/04/2026` — March or April, depending on where
82
- * the author lives) is deliberately not one of them.
42
+ * One cell read as an instant, or `null`. Not `Date.parse`, which accepts `"5"`
43
+ * and `"Q3"`, differs between engines off ISO, and reads `2026-03-14` as UTC but
44
+ * `2026/03/14` as local. Every form is matched explicitly, and an ambiguous
45
+ * all-numeric date (`03/04/2026`) is not one of them.
83
46
  */
84
47
  export function parseInstant(raw) {
85
48
  if (typeof raw !== "string")
@@ -125,14 +88,7 @@ export function parseInstant(raw) {
125
88
  }
126
89
  return null;
127
90
  }
128
- /**
129
- * An instant captioned at the coarsest granularity the axis's own span can tell
130
- * apart — a decade of samples reads as years, a year of them as months.
131
- *
132
- * Only reached for a tick that is *not* one of the rows (an author-set range, a
133
- * `zoomTo` part-way through). A tick that is a sample is captioned with the
134
- * cell the author wrote; see the note at the top of this file.
135
- */
91
+ /** An instant captioned at the coarsest granularity the span tells apart: a decade reads as years, a year as months. */
136
92
  export function formatInstant(value, span) {
137
93
  const date = new Date(value);
138
94
  const year = date.getUTCFullYear();
@@ -144,15 +100,7 @@ export function formatInstant(value, span) {
144
100
  return `${label} ${year}`;
145
101
  return `${label} ${date.getUTCDate()}`;
146
102
  }
147
- // ---- Column reading ------------------------------------------------------
148
- /**
149
- * Whether every non-empty cell of `field` holds a number.
150
- *
151
- * The same rule `numericFields` applies, asked of one column: a single text
152
- * cell in an otherwise numeric column takes the whole column off the numeric
153
- * scale, because plotting that row at 0 is the failure this module is here to
154
- * stop.
155
- */
103
+ /** `numericFields`' rule for one column: a single text cell takes it off the numeric scale rather than plot that row at 0. */
156
104
  function isNumericColumn(data, field) {
157
105
  let sawValue = false;
158
106
  for (const row of data) {
@@ -166,13 +114,9 @@ function isNumericColumn(data, field) {
166
114
  return sawValue;
167
115
  }
168
116
  /**
169
- * Every non-empty cell of `field` parsed as an instant, or `null` if any one of
170
- * them isn't one — in which case the column isn't a time column at all.
171
- *
172
- * A blank cell keeps its slot, so the returned list stays index-aligned with
173
- * the rows, and takes the nearest known instant: a hole in an x column is a row
174
- * whose date the file didn't say, and stacking it on the epoch would drag the
175
- * whole scale back fifty years to make room for it.
117
+ * Every non-empty cell as an instant, or `null` if any is not one. A blank cell
118
+ * keeps its slot and takes the nearest known instant: stacking it on the epoch
119
+ * would drag the whole scale back fifty years.
176
120
  */
177
121
  function instantColumn(data, field) {
178
122
  const parsed = [];
@@ -207,30 +151,24 @@ function extentOf(values) {
207
151
  }
208
152
  return lo <= hi ? [lo, hi] : [0, 1];
209
153
  }
154
+ /** Up to this many samples in view, every one is a tick. */
155
+ export const EVERY_SAMPLE = 12;
210
156
  /**
211
- * Ticks spanning `range`, each one **a sample** — a row the author can point at
212
- * in their file — placed as near as it can be to where an evenly spaced tick
213
- * would have gone.
214
- *
215
- * Two things have to hold at once, and picking either alone gets it wrong.
216
- * Ticks must be *samples*, because a caption between two of them has nothing to
217
- * say (two-thirds of the way through March is not a month, and on a category
218
- * column it is not anything at all). And they must be *spread*, because the
219
- * labels sit side by side in a row: taking every nth sample instead puts five
220
- * dates in the two weeks a burst of rows happens to cover and prints them on
221
- * top of each other, which is what a column with a gap in it does.
222
- *
223
- * Walking the targets rather than the samples is what satisfies both. A run of
224
- * rows too close together to caption collapses to one tick — consecutive
225
- * targets pick the same sample and the duplicate is dropped — so a chart asked
226
- * for five ticks across a gappy column draws four honest ones instead of five
227
- * overlapping.
157
+ * Ticks spanning `range`, each a sample, nearest to where an evenly spaced tick
158
+ * would go. They must be samples (a caption between two has nothing to say) and
159
+ * spread (every nth sample would print five dates on top of each other across a
160
+ * burst of rows), so this walks the evenly spaced targets, not the samples.
161
+ * Targets that pick the same sample collapse to one tick, so a gappy column
162
+ * draws fewer honest ticks rather than overlapping ones.
228
163
  */
229
164
  function sampledTicks(positions, [min, max], divisions) {
230
165
  const inside = positions.filter((v) => v >= min && v <= max);
231
166
  const unique = [...new Set(inside)].sort((a, b) => a - b);
232
167
  if (unique.length === 0)
233
168
  return markerValues([min, max], divisions);
169
+ // Few and even enough to caption every one: a month left out reads as missing data.
170
+ if (unique.length <= EVERY_SAMPLE && evenlySpaced(unique))
171
+ return unique;
234
172
  const nearest = (target) => unique.reduce((best, v) => Math.abs(v - target) < Math.abs(best - target) ? v : best);
235
173
  const out = [];
236
174
  for (const target of markerValues([min, max], divisions)) {
@@ -240,23 +178,24 @@ function sampledTicks(positions, [min, max], divisions) {
240
178
  }
241
179
  return out;
242
180
  }
243
- /**
244
- * The caption a tick carries on a sample-based scale: the cell of the row it
245
- * came from, or `fallback` when the tick isn't a sample after all.
246
- */
181
+ /** Whether no two neighbouring samples sit much closer than the average gap: months do, a burst of days between weeks of nothing does not. */
182
+ function evenlySpaced(sorted) {
183
+ if (sorted.length < 3)
184
+ return true;
185
+ const mean = (sorted[sorted.length - 1] - sorted[0]) / (sorted.length - 1);
186
+ for (let i = 1; i < sorted.length; i++)
187
+ if (sorted[i] - sorted[i - 1] < mean * 0.75)
188
+ return false;
189
+ return true;
190
+ }
191
+ /** The cell of the row a tick came from, or `fallback` when it is not a sample. */
247
192
  function labelBySample(positions, captions, value, fallback) {
248
193
  const i = positions.indexOf(value);
249
194
  return i >= 0 ? captions[i] : fallback(value);
250
195
  }
251
- /** Scales already resolved, by data array, then by field and pattern. */
196
+ /** Resolved scales by data array, then by field and pattern. */
252
197
  const scales = new WeakMap();
253
- /**
254
- * How `field` positions the rows: the number it holds, the instant it names, or
255
- * failing both, the row's own place in the file.
256
- *
257
- * Kept per `data` array: a chart's bound props ask a dozen times a frame, and
258
- * parsing every row's instant each time was most of a frame.
259
- */
198
+ /** Kept per `data` array: bound props ask a dozen times a frame, and parsing every row's instant each time was most of a frame. */
260
199
  export function resolveXScale(data, field, pattern) {
261
200
  let byKey = scales.get(data);
262
201
  if (!byKey)
@@ -268,11 +207,7 @@ export function resolveXScale(data, field, pattern) {
268
207
  return scale;
269
208
  }
270
209
  function xScaleOf(data, field,
271
- /**
272
- * The number-format pattern for a *numeric* column. Ignored by the other two
273
- * kinds, and correctly so: a tick on those is captioned with the cell the
274
- * author wrote, and `Jan` has no decimal places.
275
- */
210
+ /** Number-format pattern for a numeric column only; ticks on the other kinds are the author's cells. */
276
211
  pattern) {
277
212
  if (field !== "" && isNumericColumn(data, field)) {
278
213
  const positions = data.map((row) => num(row, field));
@@ -281,8 +216,7 @@ pattern) {
281
216
  at: (i) => positions[i] ?? 0,
282
217
  extent: extentOf(positions),
283
218
  label: (value) => pattern === undefined ? formatMarker(value) : formatNumber(value, pattern),
284
- // Round numbers, evenly spaced — a continuous axis is read by its scale
285
- // rather than by which samples happen to be on it.
219
+ // A continuous axis is read by its scale, not by which samples are on it.
286
220
  ticks: markerValues,
287
221
  };
288
222
  }
@@ -303,8 +237,7 @@ pattern) {
303
237
  return {
304
238
  kind: "ordinal",
305
239
  at: (i) => i,
306
- // `[0, n-1]` rather than the tidy `[0, n]`: the first and last rows sit on
307
- // the plot's edges, which is where a reader looks for them.
240
+ // `[0, n-1]`, not `[0, n]`: the first and last rows sit on the plot's edges.
308
241
  extent: positions.length > 0 ? [0, positions.length - 1] : [0, 1],
309
242
  label: (value) => captions[Math.round(value)] ?? "",
310
243
  ticks: (range, divisions) => sampledTicks(positions, range, divisions),
@@ -1 +1 @@
1
- {"version":3,"file":"x-scale.js","sourceRoot":"","sources":["../../src/line-chart/x-scale.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a line chart's x column *means* — the one question that has to be\n * answered before a row can be given a position along the bottom of the plot.\n *\n * A y column is always a magnitude. An x column is one of three things, and\n * which one is a property of the data rather than something the author states:\n *\n * - **numeric** — `0, 5, 10` — plot the number.\n * - **time** — `Jan`, `2026-03`, `2026-W12`, `2026-03-14` — plot the instant,\n * so samples a month apart sit a month apart and a gap in the data reads as a\n * gap.\n * - **ordinal** — anything else — plot the row's position, evenly spaced.\n *\n * Before this, every x column went through `num()`, which reads a non-numeric\n * cell as `0`. A `Month` column of `Jan…Dec` therefore stacked all twelve\n * points on one x and drew a vertical line — no error, no empty chart, just a\n * wrong picture. That is the failure this module exists to remove: every column\n * now plots as *something*, and the something is the reading a person would\n * give it.\n *\n * The captions come back off the rows rather than out of a formatter. A tick on\n * a time or ordinal scale is chosen *from the data* (see {@link XScale.ticks}),\n * so the label it wants is the cell the author wrote — `Jan`, not `1970-01-01`,\n * and `2026-W12`, not the Monday it resolves to. Only a tick that is not a\n * sample — an author-set range, or a `zoomTo` mid-flight — needs formatting,\n * and that is the one case {@link formatInstant} covers.\n */\n\nimport type { DataRecord } from \"@motionscript/sdk\"\n\nimport {\n formatMarker,\n markerValues,\n num,\n text,\n type Range,\n} from \"../kit/shared\"\nimport { formatNumber } from \"@motionscript/sdk/component\"\n\n/** How an x column's cells become positions along the axis. */\nexport type XScaleKind = \"numeric\" | \"time\" | \"ordinal\"\n\n/**\n * An x column resolved into positions, captions and ticks.\n *\n * The three kinds differ in all three answers, so they are three closures built\n * once per (data, column) rather than a kind tag every caller switches on.\n */\nexport interface XScale {\n kind: XScaleKind\n /** Position of row `index` along the axis, in this scale's own units. */\n at(index: number): number\n /** `[min, max]` across the rows — the scale when no range is set. */\n extent: Range\n /** The caption for a tick at `value`. */\n label(value: number): string\n /** `divisions + 1` tick values spanning `range`, ends included. */\n ticks(range: Range, divisions: number): number[]\n}\n\n// ---- Time parsing --------------------------------------------------------\n\nconst DAY = 86_400_000\n\nconst MONTHS = [\n \"january\",\n \"february\",\n \"march\",\n \"april\",\n \"may\",\n \"june\",\n \"july\",\n \"august\",\n \"september\",\n \"october\",\n \"november\",\n \"december\",\n]\n\n/**\n * The year a cell that names no year is dated into.\n *\n * Any year would place `Jan…Dec` in the right order at the right spacing; a\n * leap year is the one that also spaces February correctly against a column\n * that *does* carry years, so the two read alike.\n */\nconst NOMINAL_YEAR = 2024\n\n/**\n * Month index for any unambiguous abbreviation of a month name — `jan`, `sept`,\n * `january` — or -1.\n *\n * Three letters minimum, and then a prefix match, which is what lets `sept`\n * through without letting `janitor` through: a longer word has to still *be*\n * the month.\n */\nfunction monthIndex(word: string): number {\n if (word.length < 3) return -1\n return MONTHS.findIndex((month) => month.startsWith(word))\n}\n\n/** The Monday of ISO week `week` in `year`, as an epoch timestamp. */\nfunction isoWeekStart(year: number, week: number): number {\n // ISO week 1 is the week containing 4 January, and weeks start on Monday.\n const jan4 = Date.UTC(year, 0, 4)\n const weekday = (new Date(jan4).getUTCDay() + 6) % 7 // Monday = 0\n return jan4 - weekday * DAY + (week - 1) * 7 * DAY\n}\n\n/**\n * One cell read as an instant, or `null` when it isn't one.\n *\n * Deliberately **not** `Date.parse`. That accepts `\"5\"`, `\"Q3\"` and a good deal\n * else besides, differs between engines on anything non-ISO, and reads a bare\n * `2026-03-14` as UTC but `2026/03/14` as local — three ways for a column to be\n * silently misread as time. Every form below is matched explicitly, and an\n * ambiguous all-numeric date (`03/04/2026` — March or April, depending on where\n * the author lives) is deliberately not one of them.\n */\nexport function parseInstant(raw: unknown): number | null {\n if (typeof raw !== \"string\") return null\n const s = raw.trim().toLowerCase()\n if (s === \"\") return null\n\n // 2026-03-14 · 2026/03/14 · 2026-03 · 2026/03\n const ymd = /^(\\d{4})[-/](\\d{1,2})(?:[-/](\\d{1,2}))?$/.exec(s)\n if (ymd) {\n const month = Number(ymd[2])\n const day = ymd[3] === undefined ? 1 : Number(ymd[3])\n if (month < 1 || month > 12 || day < 1 || day > 31) return null\n return Date.UTC(Number(ymd[1]), month - 1, day)\n }\n\n // 2026-W12 · 2026W12 · w12 · week 12\n const week = /^(?:(\\d{4})[-\\s]?)?w(?:ee)?k?[-\\s]?(\\d{1,2})$/.exec(s)\n if (week) {\n const n = Number(week[2])\n if (n < 1 || n > 53) return null\n return isoWeekStart(\n week[1] === undefined ? NOMINAL_YEAR : Number(week[1]),\n n\n )\n }\n\n // jan · january · jan 2026 · january-2026 · 2026 jan\n const named = /^(?:(\\d{4})[-\\s]+)?([a-z]+)(?:[-\\s]+(\\d{4}))?$/.exec(s)\n if (named) {\n const month = monthIndex(named[2])\n if (month >= 0) {\n const year = named[1] ?? named[3]\n const y = year === undefined ? NOMINAL_YEAR : Number(year)\n return Date.UTC(y, month, 1)\n }\n }\n\n // 14 mar 2026 · mar 14 2026 · mar 14, 2026\n const dayNamed =\n /^(?:(\\d{1,2})[-\\s]+([a-z]+)|([a-z]+)[-\\s]+(\\d{1,2}),?)[-\\s]+(\\d{4})$/.exec(\n s\n )\n if (dayNamed) {\n const month = monthIndex(dayNamed[2] ?? dayNamed[3])\n const day = Number(dayNamed[1] ?? dayNamed[4])\n if (month >= 0 && day >= 1 && day <= 31) {\n return Date.UTC(Number(dayNamed[5]), month, day)\n }\n }\n\n return null\n}\n\n/**\n * An instant captioned at the coarsest granularity the axis's own span can tell\n * apart — a decade of samples reads as years, a year of them as months.\n *\n * Only reached for a tick that is *not* one of the rows (an author-set range, a\n * `zoomTo` part-way through). A tick that is a sample is captioned with the\n * cell the author wrote; see the note at the top of this file.\n */\nexport function formatInstant(value: number, span: number): string {\n const date = new Date(value)\n const year = date.getUTCFullYear()\n const month = MONTHS[date.getUTCMonth()].slice(0, 3)\n const label = month.charAt(0).toUpperCase() + month.slice(1)\n if (span > 4 * 365 * DAY) return String(year)\n if (span > 60 * DAY) return `${label} ${year}`\n return `${label} ${date.getUTCDate()}`\n}\n\n// ---- Column reading ------------------------------------------------------\n\n/**\n * Whether every non-empty cell of `field` holds a number.\n *\n * The same rule `numericFields` applies, asked of one column: a single text\n * cell in an otherwise numeric column takes the whole column off the numeric\n * scale, because plotting that row at 0 is the failure this module is here to\n * stop.\n */\nfunction isNumericColumn(data: DataRecord[], field: string): boolean {\n let sawValue = false\n for (const row of data) {\n const v = row[field]\n if (v === undefined || v === null || v === \"\") continue\n if (typeof v !== \"number\") return false\n sawValue = true\n }\n return sawValue\n}\n\n/**\n * Every non-empty cell of `field` parsed as an instant, or `null` if any one of\n * them isn't one — in which case the column isn't a time column at all.\n *\n * A blank cell keeps its slot, so the returned list stays index-aligned with\n * the rows, and takes the nearest known instant: a hole in an x column is a row\n * whose date the file didn't say, and stacking it on the epoch would drag the\n * whole scale back fifty years to make room for it.\n */\nfunction instantColumn(data: DataRecord[], field: string): number[] | null {\n const parsed: (number | null)[] = []\n let sawValue = false\n for (const row of data) {\n const raw = row[field]\n if (raw === undefined || raw === null || raw === \"\") {\n parsed.push(null)\n continue\n }\n const t = parseInstant(raw)\n if (t === null) return null\n parsed.push(t)\n sawValue = true\n }\n if (!sawValue) return null\n\n const first = parsed.find((t): t is number => t !== null) as number\n let last = first\n return parsed.map((t) => (t === null ? last : (last = t)))\n}\n\n/** `[min, max]` of `values`, or `[0, 1]` when there is nothing to span. */\nfunction extentOf(values: number[]): Range {\n let lo = Infinity\n let hi = -Infinity\n for (const v of values) {\n if (v < lo) lo = v\n if (v > hi) hi = v\n }\n return lo <= hi ? [lo, hi] : [0, 1]\n}\n\n/**\n * Ticks spanning `range`, each one **a sample** — a row the author can point at\n * in their file — placed as near as it can be to where an evenly spaced tick\n * would have gone.\n *\n * Two things have to hold at once, and picking either alone gets it wrong.\n * Ticks must be *samples*, because a caption between two of them has nothing to\n * say (two-thirds of the way through March is not a month, and on a category\n * column it is not anything at all). And they must be *spread*, because the\n * labels sit side by side in a row: taking every nth sample instead puts five\n * dates in the two weeks a burst of rows happens to cover and prints them on\n * top of each other, which is what a column with a gap in it does.\n *\n * Walking the targets rather than the samples is what satisfies both. A run of\n * rows too close together to caption collapses to one tick — consecutive\n * targets pick the same sample and the duplicate is dropped — so a chart asked\n * for five ticks across a gappy column draws four honest ones instead of five\n * overlapping.\n */\nfunction sampledTicks(\n positions: number[],\n [min, max]: Range,\n divisions: number\n): number[] {\n const inside = positions.filter((v) => v >= min && v <= max)\n const unique = [...new Set(inside)].sort((a, b) => a - b)\n if (unique.length === 0) return markerValues([min, max], divisions)\n\n const nearest = (target: number) =>\n unique.reduce((best, v) =>\n Math.abs(v - target) < Math.abs(best - target) ? v : best\n )\n\n const out: number[] = []\n for (const target of markerValues([min, max], divisions)) {\n const tick = nearest(target)\n if (out[out.length - 1] !== tick) out.push(tick)\n }\n return out\n}\n\n/**\n * The caption a tick carries on a sample-based scale: the cell of the row it\n * came from, or `fallback` when the tick isn't a sample after all.\n */\nfunction labelBySample(\n positions: number[],\n captions: string[],\n value: number,\n fallback: (value: number) => string\n): string {\n const i = positions.indexOf(value)\n return i >= 0 ? captions[i] : fallback(value)\n}\n\n/** Scales already resolved, by data array, then by field and pattern. */\nconst scales = new WeakMap<DataRecord[], Map<string, XScale>>()\n\n/**\n * How `field` positions the rows: the number it holds, the instant it names, or\n * failing both, the row's own place in the file.\n *\n * Kept per `data` array: a chart's bound props ask a dozen times a frame, and\n * parsing every row's instant each time was most of a frame.\n */\nexport function resolveXScale(\n data: DataRecord[],\n field: string,\n pattern?: string\n): XScale {\n let byKey = scales.get(data)\n if (!byKey) scales.set(data, (byKey = new Map()))\n const key = JSON.stringify([field, pattern ?? null])\n let scale = byKey.get(key)\n if (!scale) byKey.set(key, (scale = xScaleOf(data, field, pattern)))\n return scale\n}\n\nfunction xScaleOf(\n data: DataRecord[],\n field: string,\n /**\n * The number-format pattern for a *numeric* column. Ignored by the other two\n * kinds, and correctly so: a tick on those is captioned with the cell the\n * author wrote, and `Jan` has no decimal places.\n */\n pattern?: string\n): XScale {\n if (field !== \"\" && isNumericColumn(data, field)) {\n const positions = data.map((row) => num(row, field))\n return {\n kind: \"numeric\",\n at: (i) => positions[i] ?? 0,\n extent: extentOf(positions),\n label: (value) =>\n pattern === undefined ? formatMarker(value) : formatNumber(value, pattern),\n // Round numbers, evenly spaced — a continuous axis is read by its scale\n // rather than by which samples happen to be on it.\n ticks: markerValues,\n }\n }\n\n const captions = data.map((row) => text(row, field))\n const instants = field === \"\" ? null : instantColumn(data, field)\n\n if (instants) {\n const extent = extentOf(instants)\n const span = extent[1] - extent[0]\n return {\n kind: \"time\",\n at: (i) => instants[i] ?? 0,\n extent,\n label: (value) =>\n labelBySample(instants, captions, value, (v) =>\n formatInstant(v, span)\n ),\n ticks: (range, divisions) => sampledTicks(instants, range, divisions),\n }\n }\n\n const positions = data.map((_, i) => i)\n return {\n kind: \"ordinal\",\n at: (i) => i,\n // `[0, n-1]` rather than the tidy `[0, n]`: the first and last rows sit on\n // the plot's edges, which is where a reader looks for them.\n extent: positions.length > 0 ? [0, positions.length - 1] : [0, 1],\n label: (value) => captions[Math.round(value)] ?? \"\",\n ticks: (range, divisions) => sampledTicks(positions, range, divisions),\n }\n}\n"]}
1
+ {"version":3,"file":"x-scale.js","sourceRoot":"","sources":["../../src/line-chart/x-scale.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a line chart's x column means: a number (plotted as is), an instant\n * (`Jan`, `2026-W12`; plotted by time, so a gap in the data reads as a gap), or\n * anything else (the row's position, evenly spaced).\n *\n * A tick on a time or ordinal scale is a sample, so its caption is the cell the\n * author wrote; only a tick that is not (an author-set range, a `zoomTo`\n * mid-flight) is formatted, by {@link formatInstant}.\n */\n\nimport type { DataRecord } from \"@motionscript/sdk\"\n\nimport {\n formatMarker,\n markerValues,\n num,\n text,\n type Range,\n} from \"../kit/shared\"\nimport { formatNumber } from \"@motionscript/sdk/component\"\n\n/** How an x column's cells become positions along the axis. */\nexport type XScaleKind = \"numeric\" | \"time\" | \"ordinal\"\n\n/** An x column resolved into positions, captions and ticks: three closures built once per (data, column), not a tag callers switch on. */\nexport interface XScale {\n kind: XScaleKind\n /** Position of row `index` along the axis, in this scale's own units. */\n at(index: number): number\n /** `[min, max]` across the rows: the scale when no range is set. */\n extent: Range\n label(value: number): string\n /** `divisions + 1` tick values spanning `range`, ends included. */\n ticks(range: Range, divisions: number): number[]\n}\n\nconst DAY = 86_400_000\n\nconst MONTHS = [\n \"january\",\n \"february\",\n \"march\",\n \"april\",\n \"may\",\n \"june\",\n \"july\",\n \"august\",\n \"september\",\n \"october\",\n \"november\",\n \"december\",\n]\n\n/** The year a cell naming none is dated into; a leap year, so February spaces the same as in a column that carries years. */\nconst NOMINAL_YEAR = 2024\n\n/** Month index for `jan`, `sept` or `january`, else -1: three letters minimum, then a prefix match, so `sept` passes and `janitor` does not. */\nfunction monthIndex(word: string): number {\n if (word.length < 3) return -1\n return MONTHS.findIndex((month) => month.startsWith(word))\n}\n\nfunction isoWeekStart(year: number, week: number): number {\n // ISO week 1 is the week containing 4 January, and weeks start on Monday.\n const jan4 = Date.UTC(year, 0, 4)\n const weekday = (new Date(jan4).getUTCDay() + 6) % 7 // Monday = 0\n return jan4 - weekday * DAY + (week - 1) * 7 * DAY\n}\n\n/**\n * One cell read as an instant, or `null`. Not `Date.parse`, which accepts `\"5\"`\n * and `\"Q3\"`, differs between engines off ISO, and reads `2026-03-14` as UTC but\n * `2026/03/14` as local. Every form is matched explicitly, and an ambiguous\n * all-numeric date (`03/04/2026`) is not one of them.\n */\nexport function parseInstant(raw: unknown): number | null {\n if (typeof raw !== \"string\") return null\n const s = raw.trim().toLowerCase()\n if (s === \"\") return null\n\n // 2026-03-14 · 2026/03/14 · 2026-03 · 2026/03\n const ymd = /^(\\d{4})[-/](\\d{1,2})(?:[-/](\\d{1,2}))?$/.exec(s)\n if (ymd) {\n const month = Number(ymd[2])\n const day = ymd[3] === undefined ? 1 : Number(ymd[3])\n if (month < 1 || month > 12 || day < 1 || day > 31) return null\n return Date.UTC(Number(ymd[1]), month - 1, day)\n }\n\n // 2026-W12 · 2026W12 · w12 · week 12\n const week = /^(?:(\\d{4})[-\\s]?)?w(?:ee)?k?[-\\s]?(\\d{1,2})$/.exec(s)\n if (week) {\n const n = Number(week[2])\n if (n < 1 || n > 53) return null\n return isoWeekStart(\n week[1] === undefined ? NOMINAL_YEAR : Number(week[1]),\n n\n )\n }\n\n // jan · january · jan 2026 · january-2026 · 2026 jan\n const named = /^(?:(\\d{4})[-\\s]+)?([a-z]+)(?:[-\\s]+(\\d{4}))?$/.exec(s)\n if (named) {\n const month = monthIndex(named[2])\n if (month >= 0) {\n const year = named[1] ?? named[3]\n const y = year === undefined ? NOMINAL_YEAR : Number(year)\n return Date.UTC(y, month, 1)\n }\n }\n\n // 14 mar 2026 · mar 14 2026 · mar 14, 2026\n const dayNamed =\n /^(?:(\\d{1,2})[-\\s]+([a-z]+)|([a-z]+)[-\\s]+(\\d{1,2}),?)[-\\s]+(\\d{4})$/.exec(\n s\n )\n if (dayNamed) {\n const month = monthIndex(dayNamed[2] ?? dayNamed[3])\n const day = Number(dayNamed[1] ?? dayNamed[4])\n if (month >= 0 && day >= 1 && day <= 31) {\n return Date.UTC(Number(dayNamed[5]), month, day)\n }\n }\n\n return null\n}\n\n/** An instant captioned at the coarsest granularity the span tells apart: a decade reads as years, a year as months. */\nexport function formatInstant(value: number, span: number): string {\n const date = new Date(value)\n const year = date.getUTCFullYear()\n const month = MONTHS[date.getUTCMonth()].slice(0, 3)\n const label = month.charAt(0).toUpperCase() + month.slice(1)\n if (span > 4 * 365 * DAY) return String(year)\n if (span > 60 * DAY) return `${label} ${year}`\n return `${label} ${date.getUTCDate()}`\n}\n\n/** `numericFields`' rule for one column: a single text cell takes it off the numeric scale rather than plot that row at 0. */\nfunction isNumericColumn(data: DataRecord[], field: string): boolean {\n let sawValue = false\n for (const row of data) {\n const v = row[field]\n if (v === undefined || v === null || v === \"\") continue\n if (typeof v !== \"number\") return false\n sawValue = true\n }\n return sawValue\n}\n\n/**\n * Every non-empty cell as an instant, or `null` if any is not one. A blank cell\n * keeps its slot and takes the nearest known instant: stacking it on the epoch\n * would drag the whole scale back fifty years.\n */\nfunction instantColumn(data: DataRecord[], field: string): number[] | null {\n const parsed: (number | null)[] = []\n let sawValue = false\n for (const row of data) {\n const raw = row[field]\n if (raw === undefined || raw === null || raw === \"\") {\n parsed.push(null)\n continue\n }\n const t = parseInstant(raw)\n if (t === null) return null\n parsed.push(t)\n sawValue = true\n }\n if (!sawValue) return null\n\n const first = parsed.find((t): t is number => t !== null) as number\n let last = first\n return parsed.map((t) => (t === null ? last : (last = t)))\n}\n\n/** `[min, max]` of `values`, or `[0, 1]` when there is nothing to span. */\nfunction extentOf(values: number[]): Range {\n let lo = Infinity\n let hi = -Infinity\n for (const v of values) {\n if (v < lo) lo = v\n if (v > hi) hi = v\n }\n return lo <= hi ? [lo, hi] : [0, 1]\n}\n\n/** Up to this many samples in view, every one is a tick. */\nexport const EVERY_SAMPLE = 12\n\n/**\n * Ticks spanning `range`, each a sample, nearest to where an evenly spaced tick\n * would go. They must be samples (a caption between two has nothing to say) and\n * spread (every nth sample would print five dates on top of each other across a\n * burst of rows), so this walks the evenly spaced targets, not the samples.\n * Targets that pick the same sample collapse to one tick, so a gappy column\n * draws fewer honest ticks rather than overlapping ones.\n */\nfunction sampledTicks(\n positions: number[],\n [min, max]: Range,\n divisions: number\n): number[] {\n const inside = positions.filter((v) => v >= min && v <= max)\n const unique = [...new Set(inside)].sort((a, b) => a - b)\n if (unique.length === 0) return markerValues([min, max], divisions)\n // Few and even enough to caption every one: a month left out reads as missing data.\n if (unique.length <= EVERY_SAMPLE && evenlySpaced(unique)) return unique\n\n const nearest = (target: number) =>\n unique.reduce((best, v) =>\n Math.abs(v - target) < Math.abs(best - target) ? v : best\n )\n\n const out: number[] = []\n for (const target of markerValues([min, max], divisions)) {\n const tick = nearest(target)\n if (out[out.length - 1] !== tick) out.push(tick)\n }\n return out\n}\n\n/** Whether no two neighbouring samples sit much closer than the average gap: months do, a burst of days between weeks of nothing does not. */\nfunction evenlySpaced(sorted: readonly number[]): boolean {\n if (sorted.length < 3) return true\n const mean = (sorted[sorted.length - 1] - sorted[0]) / (sorted.length - 1)\n for (let i = 1; i < sorted.length; i++) if (sorted[i] - sorted[i - 1] < mean * 0.75) return false\n return true\n}\n\n/** The cell of the row a tick came from, or `fallback` when it is not a sample. */\nfunction labelBySample(\n positions: number[],\n captions: string[],\n value: number,\n fallback: (value: number) => string\n): string {\n const i = positions.indexOf(value)\n return i >= 0 ? captions[i] : fallback(value)\n}\n\n/** Resolved scales by data array, then by field and pattern. */\nconst scales = new WeakMap<DataRecord[], Map<string, XScale>>()\n\n/** Kept per `data` array: bound props ask a dozen times a frame, and parsing every row's instant each time was most of a frame. */\nexport function resolveXScale(\n data: DataRecord[],\n field: string,\n pattern?: string\n): XScale {\n let byKey = scales.get(data)\n if (!byKey) scales.set(data, (byKey = new Map()))\n const key = JSON.stringify([field, pattern ?? null])\n let scale = byKey.get(key)\n if (!scale) byKey.set(key, (scale = xScaleOf(data, field, pattern)))\n return scale\n}\n\nfunction xScaleOf(\n data: DataRecord[],\n field: string,\n /** Number-format pattern for a numeric column only; ticks on the other kinds are the author's cells. */\n pattern?: string\n): XScale {\n if (field !== \"\" && isNumericColumn(data, field)) {\n const positions = data.map((row) => num(row, field))\n return {\n kind: \"numeric\",\n at: (i) => positions[i] ?? 0,\n extent: extentOf(positions),\n label: (value) =>\n pattern === undefined ? formatMarker(value) : formatNumber(value, pattern),\n // A continuous axis is read by its scale, not by which samples are on it.\n ticks: markerValues,\n }\n }\n\n const captions = data.map((row) => text(row, field))\n const instants = field === \"\" ? null : instantColumn(data, field)\n\n if (instants) {\n const extent = extentOf(instants)\n const span = extent[1] - extent[0]\n return {\n kind: \"time\",\n at: (i) => instants[i] ?? 0,\n extent,\n label: (value) =>\n labelBySample(instants, captions, value, (v) =>\n formatInstant(v, span)\n ),\n ticks: (range, divisions) => sampledTicks(instants, range, divisions),\n }\n }\n\n const positions = data.map((_, i) => i)\n return {\n kind: \"ordinal\",\n at: (i) => i,\n // `[0, n-1]`, not `[0, n]`: the first and last rows sit on the plot's edges.\n extent: positions.length > 0 ? [0, positions.length - 1] : [0, 1],\n label: (value) => captions[Math.round(value)] ?? \"\",\n ticks: (range, divisions) => sampledTicks(positions, range, divisions),\n }\n}\n"]}
package/dist/nodes.d.ts CHANGED
@@ -1,17 +1,7 @@
1
1
  import { LineChart } from "./line-chart/index.js";
2
2
  import { BarChart } from "./bar-chart/index.js";
3
3
  import { PieChart } from "./pie-chart/index.js";
4
- /**
5
- * Every node type this package publishes.
6
- *
7
- * A host registers these by handing the array to an engine —
8
- * `new Engine(platform, { nodes: NODES })` — which reads each component's key.
9
- * Defining a component only *declares* that key: nothing is registered by
10
- * importing a module, so a registry holds exactly what its owner asked for.
11
- *
12
- * Listing the component **values** is also what survives bundling: this package
13
- * declares `sideEffects: false`, and a document names a node type by string, so
14
- * an import kept only for its side effect is exactly what gets shaken out.
15
- */
16
- export declare const NODES: (typeof LineChart | typeof BarChart | typeof PieChart)[];
4
+ import { RadarChart } from "./radar-chart/index.js";
5
+ import { RadialChart } from "./radial-chart/index.js";
6
+ export declare const NODES: (typeof LineChart | typeof BarChart | typeof PieChart | typeof RadarChart | typeof RadialChart)[];
17
7
  //# sourceMappingURL=nodes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,KAAK,EAAE,CAAC,OAAO,SAAS,GAAG,OAAO,QAAQ,GAAG,OAAO,QAAQ,CAAC,EAAoC,CAAC"}
1
+ {"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,eAAO,MAAM,KAAK,EAAE,CAAC,OAAO,SAAS,GAAG,OAAO,QAAQ,GAAG,OAAO,QAAQ,GAAG,OAAO,UAAU,GAAG,OAAO,WAAW,CAAC,EAMlH,CAAC"}
package/dist/nodes.js CHANGED
@@ -1,18 +1,14 @@
1
1
  import { LineChart } from "./line-chart/index.js";
2
2
  import { BarChart } from "./bar-chart/index.js";
3
3
  import { PieChart } from "./pie-chart/index.js";
4
- /**
5
- * Every node type this package publishes.
6
- *
7
- * A host registers these by handing the array to an engine —
8
- * `new Engine(platform, { nodes: NODES })` — which reads each component's key.
9
- * Defining a component only *declares* that key: nothing is registered by
10
- * importing a module, so a registry holds exactly what its owner asked for.
11
- *
12
- * Listing the component **values** is also what survives bundling: this package
13
- * declares `sideEffects: false`, and a document names a node type by string, so
14
- * an import kept only for its side effect is exactly what gets shaken out.
15
- */
4
+ import { RadarChart } from "./radar-chart/index.js";
5
+ import { RadialChart } from "./radial-chart/index.js";
16
6
  // Typed by name, so the declaration names each component rather than spelling out its type through core.
17
- export const NODES = [LineChart, BarChart, PieChart];
7
+ export const NODES = [
8
+ LineChart,
9
+ BarChart,
10
+ PieChart,
11
+ RadarChart,
12
+ RadialChart,
13
+ ];
18
14
  //# sourceMappingURL=nodes.js.map
package/dist/nodes.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"nodes.js","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;;;;;;;GAWG;AACH,yGAAyG;AACzG,MAAM,CAAC,MAAM,KAAK,GAA6D,CAAC,SAAS,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["import { LineChart } from \"./line-chart\";\nimport { BarChart } from \"./bar-chart\";\nimport { PieChart } from \"./pie-chart\";\n\n/**\n * Every node type this package publishes.\n *\n * A host registers these by handing the array to an engine —\n * `new Engine(platform, { nodes: NODES })` — which reads each component's key.\n * Defining a component only *declares* that key: nothing is registered by\n * importing a module, so a registry holds exactly what its owner asked for.\n *\n * Listing the component **values** is also what survives bundling: this package\n * declares `sideEffects: false`, and a document names a node type by string, so\n * an import kept only for its side effect is exactly what gets shaken out.\n */\n// Typed by name, so the declaration names each component rather than spelling out its type through core.\nexport const NODES: (typeof LineChart | typeof BarChart | typeof PieChart)[] = [LineChart, BarChart, PieChart];\n"]}
1
+ {"version":3,"file":"nodes.js","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,yGAAyG;AACzG,MAAM,CAAC,MAAM,KAAK,GAAsG;IACtH,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,WAAW;CACZ,CAAC","sourcesContent":["import { LineChart } from \"./line-chart\";\nimport { BarChart } from \"./bar-chart\";\nimport { PieChart } from \"./pie-chart\";\nimport { RadarChart } from \"./radar-chart\";\nimport { RadialChart } from \"./radial-chart\";\n\n// Typed by name, so the declaration names each component rather than spelling out its type through core.\nexport const NODES: (typeof LineChart | typeof BarChart | typeof PieChart | typeof RadarChart | typeof RadialChart)[] = [\n LineChart,\n BarChart,\n PieChart,\n RadarChart,\n RadialChart,\n];\n"]}
@@ -1,18 +1,6 @@
1
- /**
2
- * The pie chart: the one in the family that draws a mark per **row** rather
3
- * than per column.
4
- *
5
- * So it shares the legend and the theme machinery with its siblings and none of
6
- * the cartesian geometry. Set the theme's `ring.holeRadius` to 0 for a solid
7
- * pie; it defaults to a donut, because the hole is the only place a chart has
8
- * to put its conclusion.
9
- */
10
1
  export { PieChart, isSliceSelected } from "./pie-chart.js";
11
- export type { PieChartProps, PieChartValues } from "./pie-chart.js";
2
+ export type { PieChartValues } from "./pie-chart.js";
12
3
  export { PieRing } from "./pie-ring.js";
13
- export type { PieRingProps } from "./pie-ring.js";
14
- export { PieLegend } from "./pie-legend.js";
15
- export type { PieLegendProps } from "./pie-legend.js";
16
- export { DEFAULT_PIE_CHART_THEME, lerpPieChartTheme, pieArcs, relaxLabels, resolvePieChartTheme, resolvePieSlices, ringPoint, sliceTotal, sliceValue, } from "./shared.js";
17
- export type { CenterConfig, LabelConfig, PieArc, PieChartConfig, PieChartTheme, PieLabelMode, PieLabelPlacement, PieSlice, RingConfig, } from "./shared.js";
4
+ export { PIE_STYLE_GROUPS, pieArcs, relaxLabels, resolvePieSlices, sliceTotal, sliceValue } from "./shared.js";
5
+ export type { LabelConfig, PieArc, PieLabelMode, PieLabelPlacement, PieStyle, RingConfig } from "./shared.js";
18
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pie-chart/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACvD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAChE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACpC,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,OAAO,EACP,WAAW,EACX,oBAAoB,EACpB,gBAAgB,EAChB,SAAS,EACT,UAAU,EACV,UAAU,GACX,MAAM,UAAU,CAAA;AACjB,YAAY,EACV,YAAY,EACZ,WAAW,EACX,MAAM,EACN,cAAc,EACd,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,QAAQ,EACR,UAAU,GACX,MAAM,UAAU,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pie-chart/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACvD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACpC,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAC3G,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,iBAAiB,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA"}
@@ -1,14 +1,4 @@
1
- /**
2
- * The pie chart: the one in the family that draws a mark per **row** rather
3
- * than per column.
4
- *
5
- * So it shares the legend and the theme machinery with its siblings and none of
6
- * the cartesian geometry. Set the theme's `ring.holeRadius` to 0 for a solid
7
- * pie; it defaults to a donut, because the hole is the only place a chart has
8
- * to put its conclusion.
9
- */
10
1
  export { PieChart, isSliceSelected } from "./pie-chart.js";
11
2
  export { PieRing } from "./pie-ring.js";
12
- export { PieLegend } from "./pie-legend.js";
13
- export { DEFAULT_PIE_CHART_THEME, lerpPieChartTheme, pieArcs, relaxLabels, resolvePieChartTheme, resolvePieSlices, ringPoint, sliceTotal, sliceValue, } from "./shared.js";
3
+ export { PIE_STYLE_GROUPS, pieArcs, relaxLabels, resolvePieSlices, sliceTotal, sliceValue } from "./shared.js";
14
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pie-chart/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEvD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEpC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAExC,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,OAAO,EACP,WAAW,EACX,oBAAoB,EACpB,gBAAgB,EAChB,SAAS,EACT,UAAU,EACV,UAAU,GACX,MAAM,UAAU,CAAA","sourcesContent":["/**\n * The pie chart: the one in the family that draws a mark per **row** rather\n * than per column.\n *\n * So it shares the legend and the theme machinery with its siblings and none of\n * the cartesian geometry. Set the theme's `ring.holeRadius` to 0 for a solid\n * pie; it defaults to a donut, because the hole is the only place a chart has\n * to put its conclusion.\n */\nexport { PieChart, isSliceSelected } from \"./pie-chart\"\nexport type { PieChartProps, PieChartValues } from \"./pie-chart\"\nexport { PieRing } from \"./pie-ring\"\nexport type { PieRingProps } from \"./pie-ring\"\nexport { PieLegend } from \"./pie-legend\"\nexport type { PieLegendProps } from \"./pie-legend\"\nexport {\n DEFAULT_PIE_CHART_THEME,\n lerpPieChartTheme,\n pieArcs,\n relaxLabels,\n resolvePieChartTheme,\n resolvePieSlices,\n ringPoint,\n sliceTotal,\n sliceValue,\n} from \"./shared\"\nexport type {\n CenterConfig,\n LabelConfig,\n PieArc,\n PieChartConfig,\n PieChartTheme,\n PieLabelMode,\n PieLabelPlacement,\n PieSlice,\n RingConfig,\n} from \"./shared\"\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pie-chart/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEvD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACpC,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA","sourcesContent":["export { PieChart, isSliceSelected } from \"./pie-chart\"\nexport type { PieChartValues } from \"./pie-chart\"\nexport { PieRing } from \"./pie-ring\"\nexport { PIE_STYLE_GROUPS, pieArcs, relaxLabels, resolvePieSlices, sliceTotal, sliceValue } from \"./shared\"\nexport type { LabelConfig, PieArc, PieLabelMode, PieLabelPlacement, PieStyle, RingConfig } from \"./shared\"\n"]}