decantr 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/AGENTS.md +868 -0
  2. package/CHANGELOG.md +255 -0
  3. package/CLAUDE.md +178 -0
  4. package/LICENSE +21 -0
  5. package/README.md +229 -0
  6. package/cli/art.js +127 -0
  7. package/cli/commands/a11y.js +61 -0
  8. package/cli/commands/audit.js +225 -0
  9. package/cli/commands/build.js +38 -0
  10. package/cli/commands/dev.js +18 -0
  11. package/cli/commands/doctor.js +197 -0
  12. package/cli/commands/figma-sync.js +48 -0
  13. package/cli/commands/figma-tokens.js +55 -0
  14. package/cli/commands/generate.js +26 -0
  15. package/cli/commands/init.js +116 -0
  16. package/cli/commands/lint.js +209 -0
  17. package/cli/commands/mcp.js +530 -0
  18. package/cli/commands/migrate.js +175 -0
  19. package/cli/commands/test.js +38 -0
  20. package/cli/commands/validate.js +354 -0
  21. package/cli/index.js +113 -0
  22. package/package.json +95 -0
  23. package/reference/atoms.md +517 -0
  24. package/reference/behaviors.md +384 -0
  25. package/reference/build-tooling.md +275 -0
  26. package/reference/color-guidelines.md +965 -0
  27. package/reference/component-lifecycle.md +137 -0
  28. package/reference/compound-spacing.md +95 -0
  29. package/reference/decantation-process.md +499 -0
  30. package/reference/dev-server-routes.md +93 -0
  31. package/reference/form-system.md +253 -0
  32. package/reference/i18n.md +336 -0
  33. package/reference/icons.md +576 -0
  34. package/reference/llm-primer.md +953 -0
  35. package/reference/plugins.md +252 -0
  36. package/reference/registry-consumption.md +76 -0
  37. package/reference/router.md +217 -0
  38. package/reference/shells.md +116 -0
  39. package/reference/spatial-guidelines.md +541 -0
  40. package/reference/ssr.md +234 -0
  41. package/reference/state-data.md +215 -0
  42. package/reference/state-patterns.md +166 -0
  43. package/reference/state.md +194 -0
  44. package/reference/style-system.md +110 -0
  45. package/reference/tokens.md +460 -0
  46. package/src/app.js +19 -0
  47. package/src/chart/_animate.js +266 -0
  48. package/src/chart/_base.js +109 -0
  49. package/src/chart/_data.js +209 -0
  50. package/src/chart/_format.js +106 -0
  51. package/src/chart/_interact.js +364 -0
  52. package/src/chart/_palette.js +105 -0
  53. package/src/chart/_renderer.js +52 -0
  54. package/src/chart/_scene.js +262 -0
  55. package/src/chart/_shared.js +371 -0
  56. package/src/chart/index.js +637 -0
  57. package/src/chart/layouts/_layout-base.js +328 -0
  58. package/src/chart/layouts/cartesian.js +148 -0
  59. package/src/chart/layouts/hierarchy.js +562 -0
  60. package/src/chart/layouts/polar.js +101 -0
  61. package/src/chart/renderers/canvas.js +179 -0
  62. package/src/chart/renderers/svg.js +256 -0
  63. package/src/chart/renderers/webgpu.js +715 -0
  64. package/src/chart/types/_type-base.js +26 -0
  65. package/src/chart/types/area.js +134 -0
  66. package/src/chart/types/bar.js +173 -0
  67. package/src/chart/types/box-plot.js +125 -0
  68. package/src/chart/types/bubble.js +63 -0
  69. package/src/chart/types/candlestick.js +115 -0
  70. package/src/chart/types/chord.js +85 -0
  71. package/src/chart/types/combination.js +108 -0
  72. package/src/chart/types/funnel.js +68 -0
  73. package/src/chart/types/gauge.js +163 -0
  74. package/src/chart/types/heatmap.js +98 -0
  75. package/src/chart/types/histogram.js +71 -0
  76. package/src/chart/types/line.js +111 -0
  77. package/src/chart/types/org-chart.js +93 -0
  78. package/src/chart/types/pie.js +81 -0
  79. package/src/chart/types/radar.js +96 -0
  80. package/src/chart/types/radial.js +68 -0
  81. package/src/chart/types/range-area.js +55 -0
  82. package/src/chart/types/range-bar.js +61 -0
  83. package/src/chart/types/sankey.js +73 -0
  84. package/src/chart/types/scatter.js +66 -0
  85. package/src/chart/types/sparkline.js +81 -0
  86. package/src/chart/types/sunburst.js +69 -0
  87. package/src/chart/types/swimlane.js +88 -0
  88. package/src/chart/types/treemap.js +62 -0
  89. package/src/chart/types/waterfall.js +100 -0
  90. package/src/components/_base.js +1658 -0
  91. package/src/components/_behaviors.js +1140 -0
  92. package/src/components/_primitives.js +534 -0
  93. package/src/components/_qr-encoder.js +539 -0
  94. package/src/components/accordion.js +207 -0
  95. package/src/components/affix.js +62 -0
  96. package/src/components/alert-dialog.js +75 -0
  97. package/src/components/alert.js +47 -0
  98. package/src/components/aspect-ratio.js +24 -0
  99. package/src/components/avatar-group.js +55 -0
  100. package/src/components/avatar.js +38 -0
  101. package/src/components/back-top.js +75 -0
  102. package/src/components/badge.js +74 -0
  103. package/src/components/banner.js +68 -0
  104. package/src/components/breadcrumb.js +162 -0
  105. package/src/components/button.js +115 -0
  106. package/src/components/calendar.js +131 -0
  107. package/src/components/card.js +192 -0
  108. package/src/components/carousel.js +98 -0
  109. package/src/components/cascader.js +261 -0
  110. package/src/components/checkbox.js +80 -0
  111. package/src/components/chip.js +81 -0
  112. package/src/components/code-block.js +82 -0
  113. package/src/components/collapsible.js +50 -0
  114. package/src/components/color-palette.js +438 -0
  115. package/src/components/color-picker.js +314 -0
  116. package/src/components/combobox.js +181 -0
  117. package/src/components/command.js +174 -0
  118. package/src/components/comment.js +206 -0
  119. package/src/components/context-menu.js +76 -0
  120. package/src/components/data-table.js +724 -0
  121. package/src/components/date-picker.js +217 -0
  122. package/src/components/date-range-picker.js +244 -0
  123. package/src/components/datetime-picker.js +271 -0
  124. package/src/components/descriptions.js +68 -0
  125. package/src/components/drawer.js +179 -0
  126. package/src/components/dropdown.js +88 -0
  127. package/src/components/empty.js +41 -0
  128. package/src/components/float-button.js +90 -0
  129. package/src/components/form.js +106 -0
  130. package/src/components/hover-card.js +49 -0
  131. package/src/components/icon.js +87 -0
  132. package/src/components/image.js +97 -0
  133. package/src/components/index.js +117 -0
  134. package/src/components/input-group.js +75 -0
  135. package/src/components/input-number.js +155 -0
  136. package/src/components/input-otp.js +178 -0
  137. package/src/components/input.js +91 -0
  138. package/src/components/kbd.js +36 -0
  139. package/src/components/label.js +25 -0
  140. package/src/components/list.js +118 -0
  141. package/src/components/masked-input.js +236 -0
  142. package/src/components/mentions.js +165 -0
  143. package/src/components/menu.js +259 -0
  144. package/src/components/message.js +80 -0
  145. package/src/components/modal.js +147 -0
  146. package/src/components/navigation-menu.js +166 -0
  147. package/src/components/notification.js +84 -0
  148. package/src/components/pagination.js +104 -0
  149. package/src/components/placeholder.js +132 -0
  150. package/src/components/popconfirm.js +70 -0
  151. package/src/components/popover.js +58 -0
  152. package/src/components/progress.js +61 -0
  153. package/src/components/qrcode.js +251 -0
  154. package/src/components/radiogroup.js +120 -0
  155. package/src/components/range-slider.js +176 -0
  156. package/src/components/rate.js +186 -0
  157. package/src/components/resizable.js +83 -0
  158. package/src/components/result.js +57 -0
  159. package/src/components/scroll-area.js +43 -0
  160. package/src/components/segmented.js +97 -0
  161. package/src/components/select.js +165 -0
  162. package/src/components/separator.js +31 -0
  163. package/src/components/shell.js +407 -0
  164. package/src/components/skeleton.js +39 -0
  165. package/src/components/slider.js +141 -0
  166. package/src/components/sortable-list.js +176 -0
  167. package/src/components/space.js +42 -0
  168. package/src/components/spinner.js +112 -0
  169. package/src/components/splitter.js +147 -0
  170. package/src/components/statistic.js +136 -0
  171. package/src/components/steps.js +99 -0
  172. package/src/components/switch.js +95 -0
  173. package/src/components/table.js +44 -0
  174. package/src/components/tabs.js +216 -0
  175. package/src/components/tag.js +115 -0
  176. package/src/components/textarea.js +82 -0
  177. package/src/components/time-picker.js +153 -0
  178. package/src/components/time-range-picker.js +170 -0
  179. package/src/components/timeline.js +226 -0
  180. package/src/components/toast.js +71 -0
  181. package/src/components/toggle.js +213 -0
  182. package/src/components/tooltip.js +57 -0
  183. package/src/components/tour.js +159 -0
  184. package/src/components/transfer.js +163 -0
  185. package/src/components/tree-select.js +274 -0
  186. package/src/components/tree.js +141 -0
  187. package/src/components/typography.js +136 -0
  188. package/src/components/upload.js +118 -0
  189. package/src/components/visually-hidden.js +20 -0
  190. package/src/components/watermark.js +124 -0
  191. package/src/core/index.js +539 -0
  192. package/src/core/lifecycle.js +69 -0
  193. package/src/css/atoms.js +651 -0
  194. package/src/css/components.js +940 -0
  195. package/src/css/derive.js +1296 -0
  196. package/src/css/index.js +265 -0
  197. package/src/css/runtime.js +268 -0
  198. package/src/css/styles/addons/bioluminescent.js +93 -0
  199. package/src/css/styles/addons/clay.js +70 -0
  200. package/src/css/styles/addons/clean.js +57 -0
  201. package/src/css/styles/addons/command-center.js +143 -0
  202. package/src/css/styles/addons/dopamine.js +83 -0
  203. package/src/css/styles/addons/editorial.js +80 -0
  204. package/src/css/styles/addons/glassmorphism.js +99 -0
  205. package/src/css/styles/addons/liquid-glass.js +105 -0
  206. package/src/css/styles/addons/prismatic.js +100 -0
  207. package/src/css/styles/addons/retro.js +63 -0
  208. package/src/css/styles/auradecantism.js +96 -0
  209. package/src/css/theme-registry.js +444 -0
  210. package/src/data/entity.js +281 -0
  211. package/src/data/index.js +13 -0
  212. package/src/data/persist.js +225 -0
  213. package/src/data/query.js +839 -0
  214. package/src/data/realtime.js +299 -0
  215. package/src/data/url.js +177 -0
  216. package/src/data/worker.js +134 -0
  217. package/src/explorer/archetypes.js +243 -0
  218. package/src/explorer/atoms.js +228 -0
  219. package/src/explorer/charts.js +497 -0
  220. package/src/explorer/components.js +129 -0
  221. package/src/explorer/foundations.js +949 -0
  222. package/src/explorer/icons.js +178 -0
  223. package/src/explorer/patterns.js +247 -0
  224. package/src/explorer/recipes.js +194 -0
  225. package/src/explorer/shared/pattern-examples.js +1337 -0
  226. package/src/explorer/shared/showcase-renderer.js +958 -0
  227. package/src/explorer/shared/spec-table.js +41 -0
  228. package/src/explorer/shared/usage-links.js +87 -0
  229. package/src/explorer/shell-config.js +10 -0
  230. package/src/explorer/shells.js +551 -0
  231. package/src/explorer/styles.js +161 -0
  232. package/src/explorer/tokens.js +262 -0
  233. package/src/explorer/tools.js +525 -0
  234. package/src/form/index.js +804 -0
  235. package/src/i18n/index.js +251 -0
  236. package/src/icons/essential.js +479 -0
  237. package/src/icons/index.js +53 -0
  238. package/src/plugins/index.js +282 -0
  239. package/src/registry/archetypes/content-site.json +71 -0
  240. package/src/registry/archetypes/docs-explorer.json +23 -0
  241. package/src/registry/archetypes/ecommerce.json +104 -0
  242. package/src/registry/archetypes/financial-dashboard.json +77 -0
  243. package/src/registry/archetypes/index.json +41 -0
  244. package/src/registry/archetypes/portfolio.json +82 -0
  245. package/src/registry/archetypes/recipe-community.json +159 -0
  246. package/src/registry/archetypes/saas-dashboard.json +86 -0
  247. package/src/registry/architect/cross-cutting.json +45 -0
  248. package/src/registry/architect/domains/ecommerce.json +294 -0
  249. package/src/registry/architect/domains/financial-services.json +302 -0
  250. package/src/registry/architect/index.json +26 -0
  251. package/src/registry/architect/traits.json +379 -0
  252. package/src/registry/atoms.json +16 -0
  253. package/src/registry/chart-showcase.json +160 -0
  254. package/src/registry/chart.json +136 -0
  255. package/src/registry/components.json +8616 -0
  256. package/src/registry/core.json +216 -0
  257. package/src/registry/css.json +319 -0
  258. package/src/registry/data.json +135 -0
  259. package/src/registry/foundations.json +11 -0
  260. package/src/registry/icons.json +463 -0
  261. package/src/registry/index.json +101 -0
  262. package/src/registry/patterns/activity-feed.json +37 -0
  263. package/src/registry/patterns/article-content.json +27 -0
  264. package/src/registry/patterns/auth-form.json +37 -0
  265. package/src/registry/patterns/author-card.json +20 -0
  266. package/src/registry/patterns/card-grid.json +127 -0
  267. package/src/registry/patterns/category-nav.json +26 -0
  268. package/src/registry/patterns/chart-grid.json +36 -0
  269. package/src/registry/patterns/chat-interface.json +37 -0
  270. package/src/registry/patterns/checklist-card.json +55 -0
  271. package/src/registry/patterns/comparison-panel.json +27 -0
  272. package/src/registry/patterns/component-showcase.json +24 -0
  273. package/src/registry/patterns/contact-form.json +31 -0
  274. package/src/registry/patterns/cta-section.json +20 -0
  275. package/src/registry/patterns/data-table.json +37 -0
  276. package/src/registry/patterns/detail-header.json +83 -0
  277. package/src/registry/patterns/detail-panel.json +27 -0
  278. package/src/registry/patterns/explorer-shell.json +22 -0
  279. package/src/registry/patterns/filter-bar.json +33 -0
  280. package/src/registry/patterns/filter-sidebar.json +27 -0
  281. package/src/registry/patterns/form-sections.json +110 -0
  282. package/src/registry/patterns/goal-tracker.json +27 -0
  283. package/src/registry/patterns/hero.json +107 -0
  284. package/src/registry/patterns/index.json +47 -0
  285. package/src/registry/patterns/kpi-grid.json +36 -0
  286. package/src/registry/patterns/media-gallery.json +20 -0
  287. package/src/registry/patterns/order-history.json +20 -0
  288. package/src/registry/patterns/pagination.json +19 -0
  289. package/src/registry/patterns/photo-to-recipe.json +36 -0
  290. package/src/registry/patterns/pipeline-tracker.json +28 -0
  291. package/src/registry/patterns/post-list.json +27 -0
  292. package/src/registry/patterns/pricing-table.json +32 -0
  293. package/src/registry/patterns/scorecard.json +28 -0
  294. package/src/registry/patterns/search-bar.json +20 -0
  295. package/src/registry/patterns/specimen-grid.json +19 -0
  296. package/src/registry/patterns/stat-card.json +55 -0
  297. package/src/registry/patterns/stats-bar.json +55 -0
  298. package/src/registry/patterns/steps-card.json +55 -0
  299. package/src/registry/patterns/table-of-contents.json +19 -0
  300. package/src/registry/patterns/testimonials.json +21 -0
  301. package/src/registry/patterns/timeline.json +27 -0
  302. package/src/registry/patterns/token-inspector.json +21 -0
  303. package/src/registry/patterns/wizard.json +27 -0
  304. package/src/registry/recipe-auradecantism.json +69 -0
  305. package/src/registry/recipe-clean.json +65 -0
  306. package/src/registry/recipe-command-center.json +78 -0
  307. package/src/registry/router.json +73 -0
  308. package/src/registry/schema/README.md +197 -0
  309. package/src/registry/skeletons.json +259 -0
  310. package/src/registry/state.json +137 -0
  311. package/src/registry/tokens.json +40 -0
  312. package/src/router/hash.js +17 -0
  313. package/src/router/history.js +18 -0
  314. package/src/router/index.js +598 -0
  315. package/src/ssr/index.js +922 -0
  316. package/src/state/arrays.js +181 -0
  317. package/src/state/devtools.js +647 -0
  318. package/src/state/index.js +498 -0
  319. package/src/state/middleware.js +288 -0
  320. package/src/state/scheduler.js +206 -0
  321. package/src/state/store.js +300 -0
  322. package/src/tags/index.js +19 -0
  323. package/src/tannins/auth.js +396 -0
  324. package/src/test/dom.js +352 -0
  325. package/src/test/index.js +62 -0
  326. package/src/test/state.js +306 -0
  327. package/tools/a11y-audit.js +487 -0
  328. package/tools/analyzer.js +315 -0
  329. package/tools/audit.js +706 -0
  330. package/tools/builder.js +1422 -0
  331. package/tools/css-extract.js +188 -0
  332. package/tools/dev-server.js +316 -0
  333. package/tools/dts-gen.js +1260 -0
  334. package/tools/figma-components.js +329 -0
  335. package/tools/figma-patterns.js +516 -0
  336. package/tools/figma-plugin/code.js +453 -0
  337. package/tools/figma-plugin/manifest.json +14 -0
  338. package/tools/figma-plugin/ui.html +268 -0
  339. package/tools/figma-render.js +293 -0
  340. package/tools/figma-tokens.js +712 -0
  341. package/tools/figma-upload.js +318 -0
  342. package/tools/generate.js +738 -0
  343. package/tools/icons.js +133 -0
  344. package/tools/init-templates.js +265 -0
  345. package/tools/install-hooks.sh +5 -0
  346. package/tools/migrations/0.5.0.js +53 -0
  347. package/tools/migrations/0.6.0.js +95 -0
  348. package/tools/minify.js +170 -0
  349. package/tools/pre-commit +4 -0
  350. package/tools/registry.js +662 -0
  351. package/tools/reset-playground.js +61 -0
  352. package/tools/starter-templates/content-site/app.js +49 -0
  353. package/tools/starter-templates/content-site/essence.js +19 -0
  354. package/tools/starter-templates/content-site/pages.js +31 -0
  355. package/tools/starter-templates/ecommerce/app.js +50 -0
  356. package/tools/starter-templates/ecommerce/essence.js +19 -0
  357. package/tools/starter-templates/ecommerce/pages.js +31 -0
  358. package/tools/starter-templates/landing-page/app.js +38 -0
  359. package/tools/starter-templates/landing-page/essence.js +18 -0
  360. package/tools/starter-templates/landing-page/pages.js +21 -0
  361. package/tools/starter-templates/portfolio/app.js +45 -0
  362. package/tools/starter-templates/portfolio/essence.js +19 -0
  363. package/tools/starter-templates/portfolio/pages.js +33 -0
  364. package/tools/starter-templates/saas-dashboard/app.js +70 -0
  365. package/tools/starter-templates/saas-dashboard/essence.js +19 -0
  366. package/tools/starter-templates/saas-dashboard/pages.js +31 -0
  367. package/tools/verify-pack.js +203 -0
  368. package/types/chart.d.ts +77 -0
  369. package/types/components.d.ts +587 -0
  370. package/types/core.d.ts +89 -0
  371. package/types/css.d.ts +149 -0
  372. package/types/data.d.ts +238 -0
  373. package/types/form.d.ts +164 -0
  374. package/types/i18n.d.ts +51 -0
  375. package/types/icons.d.ts +27 -0
  376. package/types/index.d.ts +13 -0
  377. package/types/router.d.ts +116 -0
  378. package/types/ssr.d.ts +102 -0
  379. package/types/state.d.ts +83 -0
  380. package/types/tags.d.ts +62 -0
  381. package/types/tannins.d.ts +63 -0
  382. package/types/test.d.ts +48 -0
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Shared layout contract — base utilities for all layout modules.
3
+ * Axis/grid scene builders, margin computation, tick formatting.
4
+ * @module layouts/_layout-base
5
+ */
6
+
7
+ import { scaleLinear, scaleBand, scaleTime, extent, unique, padExtent, isDateLike, toDate } from '../_shared.js';
8
+ import { scene, group, line, text, rect, circle, gridLines, axisTicks } from '../_scene.js';
9
+
10
+ // --- Constants ---
11
+
12
+ /** Default chart margins (px) */
13
+ export const MARGINS = { top: 24, right: 16, bottom: 40, left: 48 };
14
+
15
+ /** No-axes margins (sparkline, pie) */
16
+ export const MARGINS_NONE = { top: 0, right: 0, bottom: 0, left: 0 };
17
+
18
+ /** Pie/donut margins (small padding) */
19
+ export const MARGINS_PIE = { top: 4, right: 4, bottom: 4, left: 4 };
20
+
21
+ // --- Dimensions ---
22
+
23
+ /**
24
+ * Compute inner dimensions from outer + margins.
25
+ * @param {number} width
26
+ * @param {number} height
27
+ * @param {Object} margins
28
+ * @returns {{ innerW: number, innerH: number, margins: Object }}
29
+ */
30
+ export function innerDimensions(width, height, margins) {
31
+ return {
32
+ innerW: Math.max(0, width - margins.left - margins.right),
33
+ innerH: Math.max(0, height - margins.top - margins.bottom),
34
+ margins
35
+ };
36
+ }
37
+
38
+ // --- Scale builders ---
39
+
40
+ /**
41
+ * Build an X scale from data + field + inner width.
42
+ * Auto-detects band (categorical) vs linear/time (numeric/date).
43
+ * @param {Object[]} data
44
+ * @param {string} xField
45
+ * @param {number} innerW
46
+ * @returns {{ scale: Function, type: 'band'|'linear'|'time' }}
47
+ */
48
+ export function buildXScale(data, xField, innerW) {
49
+ const values = data.map(d => d[xField]);
50
+
51
+ if (isDateLike(values)) {
52
+ const dates = values.map(toDate);
53
+ const [min, max] = [Math.min(...dates), Math.max(...dates)];
54
+ return { scale: scaleTime([new Date(min), new Date(max)], [0, innerW]), type: 'time' };
55
+ }
56
+
57
+ if (typeof values[0] === 'number') {
58
+ const ext = padExtent(extent(data, xField));
59
+ return { scale: scaleLinear(ext, [0, innerW]), type: 'linear' };
60
+ }
61
+
62
+ // Categorical
63
+ const cats = unique(data, xField);
64
+ return { scale: scaleBand(cats, [0, innerW]), type: 'band' };
65
+ }
66
+
67
+ /**
68
+ * Build a Y scale from data + field(s) + inner height.
69
+ * Always linear, domain goes bottom→top (range inverted).
70
+ * @param {Object[]} data
71
+ * @param {string|string[]} yFields
72
+ * @param {number} innerH
73
+ * @param {{ stacked?: boolean, padBottom?: boolean, min?: number, max?: number }} opts
74
+ * @returns {Function}
75
+ */
76
+ export function buildYScale(data, yFields, innerH, opts = {}) {
77
+ const fields = Array.isArray(yFields) ? yFields : [yFields];
78
+ let min = opts.min != null ? opts.min : Infinity;
79
+ let max = opts.max != null ? opts.max : -Infinity;
80
+
81
+ if (min === Infinity || max === -Infinity) {
82
+ if (opts.stacked) {
83
+ for (let i = 0; i < data.length; i++) {
84
+ let sum = 0;
85
+ for (const f of fields) sum += +data[i][f] || 0;
86
+ if (sum > max) max = sum;
87
+ }
88
+ if (min === Infinity) min = 0;
89
+ } else {
90
+ for (const f of fields) {
91
+ const [lo, hi] = extent(data, f);
92
+ if (lo < min) min = lo;
93
+ if (hi > max) max = hi;
94
+ }
95
+ }
96
+ }
97
+
98
+ if (opts.padBottom !== false && min > 0) min = 0;
99
+ const ext = padExtent([min, max], 0.05);
100
+ if (ext[0] > 0 && min === 0) ext[0] = 0;
101
+ return scaleLinear(ext, [innerH, 0]);
102
+ }
103
+
104
+ // --- Formatting ---
105
+
106
+ /**
107
+ * Format a number for axis labels.
108
+ * @param {number} v
109
+ * @returns {string}
110
+ */
111
+ export function formatNumber(v) {
112
+ if (Math.abs(v) >= 1e9) return (v / 1e9).toFixed(1) + 'B';
113
+ if (Math.abs(v) >= 1e6) return (v / 1e6).toFixed(1) + 'M';
114
+ if (Math.abs(v) >= 1e3) return (v / 1e3).toFixed(1) + 'K';
115
+ if (Number.isInteger(v)) return String(v);
116
+ return v.toFixed(1);
117
+ }
118
+
119
+ /**
120
+ * Format a date for axis labels.
121
+ * @param {Date} d
122
+ * @returns {string}
123
+ */
124
+ export function formatDate(d) {
125
+ if (!(d instanceof Date)) d = new Date(d);
126
+ return `${d.getMonth() + 1}/${d.getDate()}`;
127
+ }
128
+
129
+ /**
130
+ * Format duration (ms) to human-readable.
131
+ * @param {number} ms
132
+ * @returns {string}
133
+ */
134
+ export function formatDuration(ms) {
135
+ if (ms < 1000) return ms + 'ms';
136
+ if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
137
+ if (ms < 3600000) return (ms / 60000).toFixed(1) + 'm';
138
+ return (ms / 3600000).toFixed(1) + 'h';
139
+ }
140
+
141
+ /**
142
+ * Format percentage.
143
+ * @param {number} v — 0-100 or 0-1
144
+ * @returns {string}
145
+ */
146
+ export function formatPercent(v) {
147
+ const pct = v > 1 ? v : v * 100;
148
+ return pct.toFixed(1) + '%';
149
+ }
150
+
151
+ // --- Tick computation ---
152
+
153
+ /**
154
+ * Compute axis tick positions and labels.
155
+ * @param {Function} scale — scale with .ticks() method
156
+ * @param {string} type — 'linear'|'time'|'band'
157
+ * @param {number} [count=5]
158
+ * @param {Function} [format]
159
+ * @returns {{ value: any, position: number, label: string }[]}
160
+ */
161
+ export function computeTicks(scale, type, count = 5, format) {
162
+ if (type === 'band') return [];
163
+
164
+ const ticks = scale.ticks(count);
165
+ return ticks.map(t => ({
166
+ value: t,
167
+ position: scale(t),
168
+ label: format ? format(t) : (type === 'time' ? formatDate(t) : formatNumber(t))
169
+ }));
170
+ }
171
+
172
+ /**
173
+ * Get chart color for a series index.
174
+ * Uses CSS custom properties (--d-chart-0 through --d-chart-7).
175
+ * For 8-31, uses extended palette. For 32+, cycles.
176
+ * @param {number} index
177
+ * @returns {string}
178
+ */
179
+ export function chartColor(index) {
180
+ return `var(--d-chart-${index % 8})`;
181
+ }
182
+
183
+ /**
184
+ * Extended chart color — uses extended palette tokens for 8+ series.
185
+ * @param {number} index
186
+ * @returns {string}
187
+ */
188
+ export function chartColorExtended(index) {
189
+ if (index < 8) return `var(--d-chart-${index})`;
190
+ if (index < 32) return `var(--d-chart-${index % 8}-ext-${Math.floor((index - 8) / 8) + 1})`;
191
+ return `var(--d-chart-${index % 8})`;
192
+ }
193
+
194
+ /** Total base chart palette size */
195
+ export const PALETTE_SIZE = 8;
196
+
197
+ // --- Annotation builders ---
198
+
199
+ /**
200
+ * Build annotation scene nodes from spec.annotations array.
201
+ * Supports 3 types: 'line' (horizontal/vertical reference line),
202
+ * 'band' (shaded range), 'point' (labeled marker).
203
+ * @param {Object[]} annotations — [{type, axis, value, from, to, x, y, label, color, dash}]
204
+ * @param {Function} xScale
205
+ * @param {Function} yScale
206
+ * @param {string} xType — 'band'|'linear'|'time'
207
+ * @param {number} innerW
208
+ * @param {number} innerH
209
+ * @returns {Object[]} scene graph nodes
210
+ */
211
+ export function buildAnnotations(annotations, xScale, yScale, xType, innerW, innerH) {
212
+ if (!annotations || !annotations.length) return [];
213
+ const nodes = [];
214
+ for (const a of annotations) {
215
+ const color = a.color || 'var(--d-muted)';
216
+ if (a.type === 'line') {
217
+ if (a.axis === 'y') {
218
+ const y = yScale(a.value);
219
+ nodes.push(line({ x1: 0, y1: y, x2: innerW, y2: y, stroke: color, strokeDash: a.dash || '4,3', class: 'd-chart-annotation-line' }));
220
+ if (a.label) {
221
+ nodes.push(text({ x: innerW + 4, y: y + 4, content: a.label, anchor: 'start', class: 'd-chart-annotation-label', fill: color }));
222
+ }
223
+ } else {
224
+ const x = xType === 'band' ? xScale(a.value) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0) : xScale(a.value);
225
+ nodes.push(line({ x1: x, y1: 0, x2: x, y2: innerH, stroke: color, strokeDash: a.dash || '4,3', class: 'd-chart-annotation-line' }));
226
+ if (a.label) {
227
+ nodes.push(text({ x: x, y: -6, content: a.label, anchor: 'middle', class: 'd-chart-annotation-label', fill: color }));
228
+ }
229
+ }
230
+ } else if (a.type === 'band') {
231
+ if (a.axis === 'y') {
232
+ const y1 = yScale(a.to);
233
+ const y2 = yScale(a.from);
234
+ nodes.push(rect({ x: 0, y: y1, w: innerW, h: y2 - y1, fill: color, class: 'd-chart-annotation-band' }));
235
+ if (a.label) {
236
+ nodes.push(text({ x: innerW + 4, y: (y1 + y2) / 2 + 4, content: a.label, anchor: 'start', class: 'd-chart-annotation-label', fill: color }));
237
+ }
238
+ } else {
239
+ const x1 = xType === 'band' ? xScale(a.from) : xScale(a.from);
240
+ const x2 = xType === 'band' ? xScale(a.to) + (xScale.bandwidth ? xScale.bandwidth() : 0) : xScale(a.to);
241
+ nodes.push(rect({ x: x1, y: 0, w: x2 - x1, h: innerH, fill: color, class: 'd-chart-annotation-band' }));
242
+ if (a.label) {
243
+ nodes.push(text({ x: (x1 + x2) / 2, y: -6, content: a.label, anchor: 'middle', class: 'd-chart-annotation-label', fill: color }));
244
+ }
245
+ }
246
+ } else if (a.type === 'point') {
247
+ const px = xType === 'band' ? xScale(a.x) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0) : xScale(a.x);
248
+ const py = yScale(a.y);
249
+ nodes.push(circle({ cx: px, cy: py, r: 5, fill: 'none', stroke: color, strokeWidth: 2, class: 'd-chart-annotation-line' }));
250
+ if (a.label) {
251
+ nodes.push(text({ x: px, y: py - 10, content: a.label, anchor: 'middle', class: 'd-chart-annotation-label', fill: color }));
252
+ }
253
+ }
254
+ }
255
+ return nodes;
256
+ }
257
+
258
+ // --- Scene builders ---
259
+
260
+ /**
261
+ * Build X-axis scene nodes.
262
+ * @param {Function} xScale
263
+ * @param {string} xType
264
+ * @param {number} innerH
265
+ * @param {number} innerW
266
+ * @param {Object} spec
267
+ * @param {Object} [layout] — for band labels
268
+ * @returns {Object[]}
269
+ */
270
+ export function buildXAxisNodes(xScale, xType, innerH, innerW, spec, layout) {
271
+ const nodes = [];
272
+ if (spec.axisLine) {
273
+ nodes.push(line({ x1: 0, y1: innerH, x2: innerW, y2: innerH, stroke: 'var(--d-border)', class: 'd-chart-axis' }));
274
+ }
275
+
276
+ if (xType === 'band' && layout && layout.categories) {
277
+ const bandW = xScale.bandwidth();
278
+ for (const cat of layout.categories) {
279
+ const x = xScale(cat) + bandW / 2;
280
+ nodes.push(text({ x, y: innerH + 20, content: String(cat), anchor: 'middle', class: 'd-chart-axis' }));
281
+ }
282
+ } else if (xType !== 'band') {
283
+ const fmt = spec.xFormat || (xType === 'time' ? formatDate : formatNumber);
284
+ const ticks = computeTicks(xScale, xType, 6, fmt);
285
+ for (const t of ticks) {
286
+ nodes.push(text({ x: t.position, y: innerH + 20, content: t.label, anchor: 'middle', class: 'd-chart-axis' }));
287
+ }
288
+ }
289
+
290
+ return nodes;
291
+ }
292
+
293
+ /**
294
+ * Build Y-axis scene nodes.
295
+ * @param {Function} yScale
296
+ * @param {number} innerH
297
+ * @param {Object} spec
298
+ * @returns {Object[]}
299
+ */
300
+ export function buildYAxisNodes(yScale, innerH, spec) {
301
+ const fmt = spec.yFormat || formatNumber;
302
+ const ticks = yScale.ticks(5);
303
+ const nodes = [];
304
+ if (spec.axisLine) {
305
+ nodes.push(line({ x1: 0, y1: 0, x2: 0, y2: innerH, stroke: 'var(--d-border)', class: 'd-chart-axis' }));
306
+ }
307
+
308
+ for (const t of ticks) {
309
+ const y = yScale(t);
310
+ nodes.push(text({ x: -8, y: y + 4, content: fmt(t), anchor: 'end', class: 'd-chart-axis' }));
311
+ }
312
+
313
+ return nodes;
314
+ }
315
+
316
+ /**
317
+ * Build grid line scene nodes.
318
+ * @param {Function} yScale
319
+ * @param {number} innerW
320
+ * @returns {Object[]}
321
+ */
322
+ export function buildGridNodes(yScale, innerW) {
323
+ const ticks = yScale.ticks(5);
324
+ return ticks.map(t => line({
325
+ x1: 0, y1: yScale(t), x2: innerW, y2: yScale(t),
326
+ class: 'd-chart-grid'
327
+ }));
328
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Shared cartesian coordinate layout.
3
+ * Used by 13 chart types. Handles: margin calculation, x/y scale construction,
4
+ * axis tick generation, grid lines, horizontal mode, secondary y-axis, zoom viewport.
5
+ * @module layouts/cartesian
6
+ */
7
+
8
+ import { scene, group } from '../_scene.js';
9
+ import {
10
+ MARGINS, innerDimensions, buildXScale, buildYScale,
11
+ buildXAxisNodes, buildYAxisNodes, buildGridNodes, chartColor
12
+ } from './_layout-base.js';
13
+ import { scaleBand, scaleLinear, unique, extent, padExtent } from '../_shared.js';
14
+
15
+ /**
16
+ * Build a complete cartesian coordinate system.
17
+ * Returns dimensions, scales, and a scene group with axes + grid.
18
+ *
19
+ * @param {Object} spec — chart spec
20
+ * @param {number} width
21
+ * @param {number} height
22
+ * @param {Object} [opts]
23
+ * @param {boolean} [opts.bandX=false] — force band X scale
24
+ * @param {boolean} [opts.skipAxes=false] — skip axis/grid generation
25
+ * @param {Object} [opts.margins] — override margins
26
+ * @param {Object} [opts.yOpts] — pass to buildYScale
27
+ * @returns {{ innerW, innerH, margins, xScale, yScale, xType, y2Scale, categories, axisNodes, gridNodes, spec }}
28
+ */
29
+ export function cartesian(spec, width, height, opts = {}) {
30
+ const data = Array.isArray(spec.data) ? spec.data : [];
31
+ const horizontal = !!spec.horizontal;
32
+ const margins = opts.margins || { ...MARGINS };
33
+
34
+ // Increase left margin for horizontal charts (labels on Y axis)
35
+ if (horizontal) margins.left = Math.max(margins.left, 72);
36
+
37
+ // Increase right margin for secondary Y axis
38
+ if (spec.y2) margins.right = Math.max(margins.right, 48);
39
+
40
+ const { innerW, innerH } = innerDimensions(width, height, margins);
41
+
42
+ // Resolve field names
43
+ const xField = spec.x;
44
+ const yFields = Array.isArray(spec.y) ? spec.y : [spec.y];
45
+
46
+ let xScale, xType, yScale, y2Scale, categories;
47
+
48
+ if (horizontal) {
49
+ // Horizontal mode: Y becomes categorical, X becomes value
50
+ categories = unique(data, xField);
51
+ xScale = scaleBand(categories, [0, innerH], 0.2);
52
+ xType = 'band';
53
+ yScale = buildYScale(data, yFields, innerW, { ...opts.yOpts, padBottom: opts.yOpts?.padBottom });
54
+ // Swap: yScale maps to horizontal (innerW) but we need it to go left→right
55
+ const yMin = 0, yMax = extent(data, yFields[0])[1];
56
+ yScale = scaleLinear(padExtent([yMin, yMax], 0.05), [0, innerW]);
57
+ if (yScale.ticks) { /* keep ticks */ }
58
+ } else {
59
+ // Normal: X is categorical or continuous, Y is value
60
+ if (opts.bandX) {
61
+ categories = unique(data, xField);
62
+ xScale = scaleBand(categories, [0, innerW], 0.2);
63
+ xType = 'band';
64
+ } else {
65
+ const built = buildXScale(data, xField, innerW);
66
+ xScale = built.scale;
67
+ xType = built.type;
68
+ if (xType === 'band') categories = unique(data, xField);
69
+ }
70
+
71
+ yScale = buildYScale(data, yFields, innerH, opts.yOpts || { stacked: !!spec.stacked });
72
+
73
+ // Secondary Y axis
74
+ if (spec.y2) {
75
+ const y2Fields = Array.isArray(spec.y2) ? spec.y2 : [spec.y2];
76
+ y2Scale = buildYScale(data, y2Fields, innerH, { padBottom: true });
77
+ }
78
+ }
79
+
80
+ // Build axis + grid scene nodes
81
+ let axisNodes = [];
82
+ let gridNodes = [];
83
+
84
+ if (!opts.skipAxes) {
85
+ if (horizontal) {
86
+ // Horizontal: value axis on bottom, category axis on left
87
+ // Value axis (bottom)
88
+ axisNodes.push(...buildHorizontalValueAxis(yScale, innerW, innerH, spec));
89
+ // Category axis (left)
90
+ axisNodes.push(...buildHorizontalCategoryAxis(xScale, categories, innerH));
91
+ // Grid (vertical lines)
92
+ gridNodes = buildHorizontalGrid(yScale, innerH);
93
+ } else {
94
+ axisNodes.push(...buildXAxisNodes(xScale, xType, innerH, innerW, spec, { categories }));
95
+ axisNodes.push(...buildYAxisNodes(yScale, innerH, spec));
96
+ gridNodes = buildGridNodes(yScale, innerW);
97
+ }
98
+ }
99
+
100
+ return {
101
+ width, height, innerW, innerH, margins,
102
+ xScale, yScale, xType, y2Scale,
103
+ categories, axisNodes, gridNodes,
104
+ horizontal, data, spec
105
+ };
106
+ }
107
+
108
+ // --- Horizontal chart axis helpers ---
109
+
110
+ import { line, text } from '../_scene.js';
111
+
112
+ function buildHorizontalValueAxis(valueScale, innerW, innerH, spec) {
113
+ const nodes = [
114
+ line({ x1: 0, y1: innerH, x2: innerW, y2: innerH, stroke: 'var(--d-border)', class: 'd-chart-axis' })
115
+ ];
116
+ const ticks = valueScale.ticks(5);
117
+ const fmt = spec.yFormat || (v => {
118
+ if (Math.abs(v) >= 1e6) return (v / 1e6).toFixed(1) + 'M';
119
+ if (Math.abs(v) >= 1e3) return (v / 1e3).toFixed(1) + 'K';
120
+ if (Number.isInteger(v)) return String(v);
121
+ return v.toFixed(1);
122
+ });
123
+ for (const t of ticks) {
124
+ const x = valueScale(t);
125
+ nodes.push(text({ x, y: innerH + 20, content: fmt(t), anchor: 'middle', class: 'd-chart-axis' }));
126
+ }
127
+ return nodes;
128
+ }
129
+
130
+ function buildHorizontalCategoryAxis(bandScale, categories, innerH) {
131
+ const nodes = [
132
+ line({ x1: 0, y1: 0, x2: 0, y2: innerH, stroke: 'var(--d-border)', class: 'd-chart-axis' })
133
+ ];
134
+ const bandW = bandScale.bandwidth();
135
+ for (const cat of categories) {
136
+ const y = bandScale(cat) + bandW / 2;
137
+ nodes.push(text({ x: -8, y: y + 4, content: String(cat), anchor: 'end', class: 'd-chart-axis' }));
138
+ }
139
+ return nodes;
140
+ }
141
+
142
+ function buildHorizontalGrid(valueScale, innerH) {
143
+ const ticks = valueScale.ticks(5);
144
+ return ticks.map(t => line({
145
+ x1: valueScale(t), y1: 0, x2: valueScale(t), y2: innerH,
146
+ class: 'd-chart-grid'
147
+ }));
148
+ }