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,266 @@
1
+ /**
2
+ * Animation Engine — enter/exit/update morphing, spring physics, stagger.
3
+ * Interpolates between two scene graphs for smooth transitions.
4
+ * @module _animate
5
+ */
6
+
7
+ import { getAnimations } from '../css/theme-registry.js';
8
+
9
+ // --- Easing functions ---
10
+
11
+ export const easings = {
12
+ linear: t => t,
13
+ standard: t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
14
+ decelerate: t => 1 - Math.pow(1 - t, 3),
15
+ accelerate: t => t * t * t,
16
+ bounce: t => {
17
+ const n1 = 7.5625, d1 = 2.75;
18
+ if (t < 1 / d1) return n1 * t * t;
19
+ if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
20
+ if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
21
+ return n1 * (t -= 2.625 / d1) * t + 0.984375;
22
+ },
23
+ overshoot: t => 1 + 2.70158 * Math.pow(t - 1, 3) + 1.70158 * Math.pow(t - 1, 2)
24
+ };
25
+
26
+ // --- Spring physics ---
27
+
28
+ /**
29
+ * Critically damped spring model.
30
+ * @param {number} current
31
+ * @param {number} target
32
+ * @param {Object} state — { velocity }
33
+ * @param {number} dt — time delta in seconds
34
+ * @param {Object} [opts] — { stiffness, damping, mass }
35
+ * @returns {number}
36
+ */
37
+ export function springStep(current, target, state, dt, opts = {}) {
38
+ const stiffness = opts.stiffness || 170;
39
+ const damping = opts.damping || 26;
40
+ const mass = opts.mass || 1;
41
+
42
+ const displacement = current - target;
43
+ const springForce = -stiffness * displacement;
44
+ const dampingForce = -damping * state.velocity;
45
+ const acceleration = (springForce + dampingForce) / mass;
46
+
47
+ state.velocity += acceleration * dt;
48
+ return current + state.velocity * dt;
49
+ }
50
+
51
+ // --- Scene interpolation ---
52
+
53
+ /**
54
+ * Interpolate between two scene graphs.
55
+ * Matches nodes by key, interpolates numeric attrs.
56
+ * @param {Object} from — source scene
57
+ * @param {Object} to — target scene
58
+ * @param {number} t — progress 0..1
59
+ * @returns {Object} interpolated scene
60
+ */
61
+ export function interpolateScene(from, to, t) {
62
+ if (!from) return to;
63
+ if (!to) return from;
64
+
65
+ return {
66
+ ...to,
67
+ children: interpolateChildren(from.children || [], to.children || [], t),
68
+ meta: to.meta
69
+ };
70
+ }
71
+
72
+ function interpolateChildren(fromChildren, toChildren, t) {
73
+ // Build key → node maps
74
+ const fromMap = buildKeyMap(fromChildren);
75
+ const toMap = buildKeyMap(toChildren);
76
+ const result = [];
77
+
78
+ // Process to-children (enter + update)
79
+ for (const toNode of toChildren) {
80
+ const key = nodeKey(toNode);
81
+ const fromNode = key ? fromMap.get(key) : null;
82
+
83
+ if (fromNode) {
84
+ // Update — interpolate
85
+ result.push(interpolateNode(fromNode, toNode, t));
86
+ } else {
87
+ // Enter — fade/scale in
88
+ result.push(applyEnter(toNode, t));
89
+ }
90
+ }
91
+
92
+ // Exit nodes (in from but not in to)
93
+ for (const fromNode of fromChildren) {
94
+ const key = nodeKey(fromNode);
95
+ if (key && !toMap.has(key)) {
96
+ result.push(applyExit(fromNode, t));
97
+ }
98
+ }
99
+
100
+ return result;
101
+ }
102
+
103
+ function interpolateNode(from, to, t) {
104
+ if (from.type !== to.type) return t < 0.5 ? from : to;
105
+
106
+ const result = { ...to };
107
+
108
+ // Interpolate numeric properties
109
+ const numericKeys = ['x', 'y', 'w', 'h', 'cx', 'cy', 'r', 'x1', 'y1', 'x2', 'y2',
110
+ 'innerR', 'outerR', 'startAngle', 'endAngle', 'opacity', 'rx', 'ry', 'strokeWidth'];
111
+
112
+ for (const k of numericKeys) {
113
+ if (typeof from[k] === 'number' && typeof to[k] === 'number') {
114
+ result[k] = from[k] + (to[k] - from[k]) * t;
115
+ }
116
+ }
117
+
118
+ // Interpolate path data (if both are simple M/L paths)
119
+ if (from.d && to.d && from.type === 'path') {
120
+ result.d = interpolatePath(from.d, to.d, t);
121
+ }
122
+
123
+ // Recurse into children
124
+ if (from.children && to.children) {
125
+ result.children = interpolateChildren(from.children, to.children, t);
126
+ }
127
+
128
+ return result;
129
+ }
130
+
131
+ function applyEnter(node, t) {
132
+ const result = { ...node };
133
+ if (result.opacity == null) result.opacity = t;
134
+ else result.opacity = result.opacity * t;
135
+
136
+ // Scale from center for rects
137
+ if (node.type === 'rect' && node.h != null) {
138
+ const fullH = node.h;
139
+ result.h = fullH * t;
140
+ result.y = (node.y || 0) + fullH * (1 - t);
141
+ }
142
+
143
+ if (node.children) {
144
+ result.children = node.children.map(c => applyEnter(c, t));
145
+ }
146
+
147
+ return result;
148
+ }
149
+
150
+ function applyExit(node, t) {
151
+ const result = { ...node };
152
+ result.opacity = 1 - t;
153
+ return result;
154
+ }
155
+
156
+ // --- Path interpolation ---
157
+
158
+ function interpolatePath(fromD, toD, t) {
159
+ const fromCmds = parsePath(fromD);
160
+ const toCmds = parsePath(toD);
161
+
162
+ if (fromCmds.length !== toCmds.length) return t < 0.5 ? fromD : toD;
163
+
164
+ let result = '';
165
+ for (let i = 0; i < toCmds.length; i++) {
166
+ const fc = fromCmds[i], tc = toCmds[i];
167
+ if (fc.cmd !== tc.cmd || fc.values.length !== tc.values.length) {
168
+ return t < 0.5 ? fromD : toD;
169
+ }
170
+ result += tc.cmd;
171
+ for (let j = 0; j < tc.values.length; j++) {
172
+ if (j > 0) result += ',';
173
+ result += (fc.values[j] + (tc.values[j] - fc.values[j]) * t).toFixed(2);
174
+ }
175
+ }
176
+ return result;
177
+ }
178
+
179
+ function parsePath(d) {
180
+ const cmds = [];
181
+ const re = /([MLHVCSQTAZ])([^MLHVCSQTAZ]*)/gi;
182
+ let match;
183
+ while ((match = re.exec(d))) {
184
+ const cmd = match[1];
185
+ const values = match[2].trim() ? match[2].trim().split(/[\s,]+/).map(Number) : [];
186
+ cmds.push({ cmd, values });
187
+ }
188
+ return cmds;
189
+ }
190
+
191
+ // --- Orchestrator ---
192
+
193
+ /**
194
+ * Animate between two scenes.
195
+ * @param {HTMLElement} container — DOM container
196
+ * @param {Object} fromScene — previous scene graph
197
+ * @param {Object} toScene — new scene graph
198
+ * @param {Function} renderFn — renderer function (scene → DOM element)
199
+ * @param {Object} [opts]
200
+ * @param {number} [opts.duration=300] — ms
201
+ * @param {string} [opts.easing='decelerate']
202
+ * @param {boolean} [opts.spring=false]
203
+ * @param {number} [opts.stagger=0] — ms delay between elements
204
+ * @returns {Promise<void>}
205
+ */
206
+ export function animate(container, fromScene, toScene, renderFn, opts = {}) {
207
+ // Check if animations are disabled
208
+ if (typeof getAnimations === 'function' && !getAnimations()) {
209
+ const el = renderFn(toScene);
210
+ container.textContent = '';
211
+ container.appendChild(el);
212
+ return Promise.resolve();
213
+ }
214
+
215
+ // Check prefers-reduced-motion
216
+ if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
217
+ const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
218
+ if (mq.matches) {
219
+ const el = renderFn(toScene);
220
+ container.textContent = '';
221
+ container.appendChild(el);
222
+ return Promise.resolve();
223
+ }
224
+ }
225
+
226
+ const duration = opts.duration || 300;
227
+ const easingFn = easings[opts.easing] || easings.decelerate;
228
+
229
+ return new Promise(resolve => {
230
+ const start = performance.now();
231
+
232
+ function frame(now) {
233
+ const elapsed = now - start;
234
+ const rawT = Math.min(1, elapsed / duration);
235
+ const t = easingFn(rawT);
236
+
237
+ const interpolated = interpolateScene(fromScene, toScene, t);
238
+ const el = renderFn(interpolated);
239
+ container.textContent = '';
240
+ container.appendChild(el);
241
+
242
+ if (rawT < 1) {
243
+ requestAnimationFrame(frame);
244
+ } else {
245
+ resolve();
246
+ }
247
+ }
248
+
249
+ requestAnimationFrame(frame);
250
+ });
251
+ }
252
+
253
+ // --- Helpers ---
254
+
255
+ function nodeKey(node) {
256
+ return node?.key || null;
257
+ }
258
+
259
+ function buildKeyMap(children) {
260
+ const map = new Map();
261
+ for (const child of children) {
262
+ const key = nodeKey(child);
263
+ if (key) map.set(key, child);
264
+ }
265
+ return map;
266
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Base structural CSS for chart components.
3
+ * Injected once on first chart render.
4
+ * Visual styling comes from the active theme (chart key in themes/*.js).
5
+ */
6
+
7
+ let injected = false;
8
+
9
+ const BASE_CSS = [
10
+ // Chart container
11
+ '.d-chart{position:relative;width:100%;min-width:0;overflow:visible}',
12
+ '.d-chart-inner{position:relative}',
13
+
14
+ // SVG container
15
+ '.d-chart-svg{display:block;width:100%;overflow:visible}',
16
+
17
+ // Title
18
+ '.d-chart-title{font-size:var(--d-text-lg);font-weight:var(--d-fw-title);line-height:var(--d-lh-snug);color:var(--d-fg);margin:0 0 var(--d-sp-3) 0}',
19
+
20
+ // Axes
21
+ '.d-chart-axis text,text.d-chart-axis{font-size:var(--d-text-xs);fill:var(--d-muted);font-family:var(--d-font)}',
22
+ '.d-chart-axis line,.d-chart-axis path,line.d-chart-axis,path.d-chart-axis{stroke:var(--d-border);fill:none;shape-rendering:crispEdges}',
23
+ '.d-chart-axis-label{font-size:var(--d-text-sm);fill:var(--d-muted);font-family:var(--d-font)}',
24
+
25
+ // Grid
26
+ 'line.d-chart-grid{stroke:var(--d-chart-grid,var(--d-border));shape-rendering:crispEdges}',
27
+
28
+ // Data elements
29
+ '.d-chart-line{fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}',
30
+ '.d-chart-area{}',
31
+ '.d-chart-bar{}',
32
+ '.d-chart-point{cursor:pointer;transition:r var(--d-duration-fast) ease}',
33
+ '.d-chart-point:hover{r:5}',
34
+ '.d-chart-point:focus{outline:2px solid var(--d-primary);outline-offset:2px}',
35
+ '.d-chart-slice{cursor:pointer;transition:opacity 0.15s}',
36
+ '.d-chart-slice:hover{opacity:0.85}',
37
+
38
+ // Legend
39
+ '.d-chart-legend{display:flex;flex-wrap:wrap;gap:var(--d-sp-3);padding:var(--d-sp-3) 0 0;font-size:var(--d-text-sm);color:var(--d-fg)}',
40
+ '.d-chart-legend-item{display:inline-flex;align-items:center;gap:var(--d-sp-1-5);cursor:pointer;user-select:none}',
41
+ '.d-chart-legend-swatch{width:var(--d-sp-3);height:var(--d-sp-3);border-radius:50%;flex-shrink:0}',
42
+ '.d-chart-legend-disabled{opacity:0.35}',
43
+
44
+ // Tooltip
45
+ '.d-chart-tooltip{position:absolute;z-index:1002;pointer-events:none;padding:var(--d-sp-2) var(--d-sp-3);font-size:var(--d-text-sm);line-height:var(--d-lh-normal);white-space:nowrap;border-radius:var(--d-radius);background:var(--d-chart-tooltip-bg,var(--d-surface-1));color:var(--d-fg);border:1px solid var(--d-border);box-shadow:0 2px 8px rgba(0,0,0,0.12);opacity:0;transition:opacity 0.12s}',
46
+ '.d-chart-tooltip-visible{opacity:1}',
47
+ '.d-chart-tooltip-label{font-weight:var(--d-fw-title);margin-bottom:var(--d-sp-1)}',
48
+ '.d-chart-tooltip-row{display:flex;align-items:center;gap:var(--d-sp-2)}',
49
+ '.d-chart-tooltip-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}',
50
+
51
+ // Data table fallback (accessibility)
52
+ '.d-chart-table{width:100%;border-collapse:collapse;font-size:var(--d-text-sm);margin-top:var(--d-sp-2)}',
53
+ '.d-chart-table th{text-align:left;font-weight:600;padding:var(--d-sp-2);border-bottom:2px solid var(--d-border)}',
54
+ '.d-chart-table td{padding:var(--d-sp-2);border-bottom:1px solid var(--d-border)}',
55
+
56
+ // Screen reader only
57
+ '.d-chart-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}',
58
+
59
+ // Sparkline
60
+ '.d-chart-spark{display:inline-block;vertical-align:middle}',
61
+ '.d-chart-spark svg{display:block}',
62
+
63
+ // Annotations
64
+ '.d-chart-annotation-line{stroke-dasharray:4,3}',
65
+ '.d-chart-annotation-label{font-size:var(--d-text-xs);fill:var(--d-muted);font-family:var(--d-font)}',
66
+ '.d-chart-annotation-band{opacity:0.08}',
67
+
68
+ // Crosshair
69
+ '.d-chart-crosshair{pointer-events:none}',
70
+
71
+ // Brush selection
72
+ '.d-chart-brush{fill:var(--d-chart-selection,var(--d-primary-subtle));stroke:var(--d-primary-border);pointer-events:none}',
73
+
74
+ // Grid — theme-aware (duplicate removed; consolidated above)
75
+
76
+ // Tick marks
77
+ '.d-chart-tick{stroke:var(--d-border);shape-rendering:crispEdges}',
78
+
79
+ // Scene graph text elements
80
+ '.d-chart text{font-family:var(--d-font)}',
81
+
82
+ // Live streaming — smooth CSS path morphing instead of full SVG rebuild
83
+ '.d-chart-live .d-chart-line{transition:d 0.8s ease-out}',
84
+ '.d-chart-live .d-chart-area{transition:d 0.8s ease-out}',
85
+
86
+ // Line draw entrance animation
87
+ '@keyframes d-chart-draw{from{stroke-dashoffset:var(--d-path-len)}to{stroke-dashoffset:0}}',
88
+ '.d-chart-line[data-animate]{stroke-dasharray:var(--d-path-len);animation:d-chart-draw 0.75s ease-out forwards}',
89
+
90
+ // Reduced motion
91
+ '@media(prefers-reduced-motion:reduce){.d-chart-line,.d-chart-area,.d-chart-bar,.d-chart-slice,.d-chart-point,.d-chart-tooltip{animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important}}'
92
+ ].join('');
93
+
94
+ export function injectChartBase() {
95
+ if (injected) return;
96
+ if (typeof document === 'undefined') return;
97
+ injected = true;
98
+ let el = document.querySelector('[data-decantr-chart]');
99
+ if (!el) {
100
+ el = document.createElement('style');
101
+ el.setAttribute('data-decantr-chart', '');
102
+ document.head.appendChild(el);
103
+ }
104
+ el.textContent = `@layer d.base{${BASE_CSS}}`;
105
+ }
106
+
107
+ export function resetChartBase() {
108
+ injected = false;
109
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Data Pipeline — transforms, aggregations, streaming, virtual windowing.
3
+ * All functions are composable and return new arrays.
4
+ * @module _data
5
+ */
6
+
7
+ // --- Transform functions ---
8
+
9
+ /**
10
+ * Filter data rows by predicate.
11
+ * @param {Object[]} data
12
+ * @param {Function} predicate — (row) => boolean
13
+ * @returns {Object[]}
14
+ */
15
+ export function filter(data, predicate) {
16
+ return data.filter(predicate);
17
+ }
18
+
19
+ /**
20
+ * Sort data by field.
21
+ * @param {Object[]} data
22
+ * @param {string} field
23
+ * @param {'asc'|'desc'} [order='asc']
24
+ * @returns {Object[]}
25
+ */
26
+ export function sortBy(data, field, order = 'asc') {
27
+ const sorted = [...data];
28
+ const dir = order === 'desc' ? -1 : 1;
29
+ sorted.sort((a, b) => {
30
+ const av = a[field], bv = b[field];
31
+ if (av < bv) return -dir;
32
+ if (av > bv) return dir;
33
+ return 0;
34
+ });
35
+ return sorted;
36
+ }
37
+
38
+ /**
39
+ * Aggregate data by group field.
40
+ * @param {Object[]} data
41
+ * @param {string} groupField — field to group by
42
+ * @param {string} aggField — field to aggregate
43
+ * @param {'sum'|'avg'|'min'|'max'|'count'} fn
44
+ * @returns {Object[]} — [{ [groupField], [aggField] }]
45
+ */
46
+ export function aggregate(data, groupField, aggField, fn) {
47
+ const groups = new Map();
48
+ for (const d of data) {
49
+ const key = d[groupField];
50
+ if (!groups.has(key)) groups.set(key, []);
51
+ groups.get(key).push(+d[aggField] || 0);
52
+ }
53
+
54
+ return [...groups.entries()].map(([key, values]) => {
55
+ let result;
56
+ switch (fn) {
57
+ case 'sum': result = values.reduce((s, v) => s + v, 0); break;
58
+ case 'avg': result = values.reduce((s, v) => s + v, 0) / values.length; break;
59
+ case 'min': result = Math.min(...values); break;
60
+ case 'max': result = Math.max(...values); break;
61
+ case 'count': result = values.length; break;
62
+ default: result = values.reduce((s, v) => s + v, 0);
63
+ }
64
+ return { [groupField]: key, [aggField]: result };
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Pivot table transformation.
70
+ * @param {Object[]} data
71
+ * @param {string} rowField
72
+ * @param {string} colField
73
+ * @param {string} valueField
74
+ * @returns {Object[]}
75
+ */
76
+ export function pivot(data, rowField, colField, valueField) {
77
+ const rows = new Map();
78
+ for (const d of data) {
79
+ const rowKey = d[rowField];
80
+ if (!rows.has(rowKey)) rows.set(rowKey, { [rowField]: rowKey });
81
+ rows.get(rowKey)[d[colField]] = +d[valueField] || 0;
82
+ }
83
+ return [...rows.values()];
84
+ }
85
+
86
+ /**
87
+ * Running cumulative sum.
88
+ * @param {Object[]} data
89
+ * @param {string} field
90
+ * @returns {Object[]}
91
+ */
92
+ export function cumulative(data, field) {
93
+ let sum = 0;
94
+ return data.map(d => {
95
+ sum += +d[field] || 0;
96
+ return { ...d, [field]: sum };
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Normalize values to 0-100% of total.
102
+ * @param {Object[]} data
103
+ * @param {string[]} fields
104
+ * @returns {Object[]}
105
+ */
106
+ export function normalize(data, fields) {
107
+ return data.map(d => {
108
+ let total = 0;
109
+ for (const f of fields) total += Math.abs(+d[f] || 0);
110
+ if (total === 0) total = 1;
111
+ const result = { ...d };
112
+ for (const f of fields) result[f] = ((+d[f] || 0) / total) * 100;
113
+ return result;
114
+ });
115
+ }
116
+
117
+ // --- Statistical functions ---
118
+
119
+ /**
120
+ * Histogram binning.
121
+ * @param {Object[]} data
122
+ * @param {string} field
123
+ * @param {number} [binCount] — auto if not provided
124
+ * @returns {{ lo: number, hi: number, count: number }[]}
125
+ */
126
+ export function binData(data, field, binCount) {
127
+ const values = data.map(d => +d[field]).filter(v => !isNaN(v)).sort((a, b) => a - b);
128
+ if (!values.length) return [];
129
+
130
+ const n = binCount || Math.max(5, Math.ceil(Math.sqrt(values.length)));
131
+ const min = values[0], max = values[values.length - 1];
132
+ const width = (max - min) / n || 1;
133
+
134
+ const bins = [];
135
+ for (let i = 0; i < n; i++) {
136
+ const lo = min + i * width;
137
+ const hi = lo + width;
138
+ const count = values.filter(v => v >= lo && (i === n - 1 ? v <= hi : v < hi)).length;
139
+ bins.push({ lo, hi, count });
140
+ }
141
+ return bins;
142
+ }
143
+
144
+ /**
145
+ * Box plot statistics.
146
+ * @param {Object[]} data
147
+ * @param {string} field
148
+ * @returns {{ q1, median, q3, whiskerLow, whiskerHigh, outliers, min, max, mean }}
149
+ */
150
+ export function boxStats(data, field) {
151
+ const values = data.map(d => +d[field]).filter(v => !isNaN(v)).sort((a, b) => a - b);
152
+ if (!values.length) return { q1: 0, median: 0, q3: 0, whiskerLow: 0, whiskerHigh: 0, outliers: [], min: 0, max: 0, mean: 0 };
153
+
154
+ const q = (p) => {
155
+ const idx = (values.length - 1) * p;
156
+ const lo = Math.floor(idx), hi = Math.ceil(idx);
157
+ return lo === hi ? values[lo] : values[lo] + (values[hi] - values[lo]) * (idx - lo);
158
+ };
159
+
160
+ const median = q(0.5), q1 = q(0.25), q3 = q(0.75);
161
+ const iqr = q3 - q1;
162
+ const lowerFence = q1 - 1.5 * iqr;
163
+ const upperFence = q3 + 1.5 * iqr;
164
+ const whiskerLow = values.find(v => v >= lowerFence) ?? values[0];
165
+ const whiskerHigh = [...values].reverse().find(v => v <= upperFence) ?? values[values.length - 1];
166
+ const outliers = values.filter(v => v < lowerFence || v > upperFence);
167
+ const mean = values.reduce((s, v) => s + v, 0) / values.length;
168
+
169
+ return { q1, median, q3, whiskerLow, whiskerHigh, outliers, min: values[0], max: values[values.length - 1], mean };
170
+ }
171
+
172
+ // --- Virtual windowing ---
173
+
174
+ /**
175
+ * Extract visible data window using binary search.
176
+ * O(log n) for datasets with sorted x values.
177
+ * @param {Object[]} data — sorted by xField
178
+ * @param {string} xField
179
+ * @param {number[]} viewport — [minX, maxX]
180
+ * @returns {Object[]}
181
+ */
182
+ export function virtualWindow(data, xField, viewport) {
183
+ if (!data.length) return [];
184
+ const [minX, maxX] = viewport;
185
+
186
+ // Binary search for start index
187
+ let lo = 0, hi = data.length - 1;
188
+ while (lo < hi) {
189
+ const mid = (lo + hi) >> 1;
190
+ if (+data[mid][xField] < minX) lo = mid + 1;
191
+ else hi = mid;
192
+ }
193
+ const startIdx = lo;
194
+
195
+ // Binary search for end index
196
+ lo = startIdx; hi = data.length - 1;
197
+ while (lo < hi) {
198
+ const mid = (lo + hi + 1) >> 1;
199
+ if (+data[mid][xField] > maxX) hi = mid - 1;
200
+ else lo = mid;
201
+ }
202
+ const endIdx = hi;
203
+
204
+ // Include one point before and after for line continuity
205
+ const start = Math.max(0, startIdx - 1);
206
+ const end = Math.min(data.length - 1, endIdx + 1);
207
+
208
+ return data.slice(start, end + 1);
209
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Number/date/duration formatting — extracted + extended.
3
+ * @module _format
4
+ */
5
+
6
+ /**
7
+ * Format a number for display.
8
+ * @param {number} v
9
+ * @returns {string}
10
+ */
11
+ export function formatNumber(v) {
12
+ if (v == null || isNaN(v)) return '';
13
+ if (Math.abs(v) >= 1e12) return (v / 1e12).toFixed(1) + 'T';
14
+ if (Math.abs(v) >= 1e9) return (v / 1e9).toFixed(1) + 'B';
15
+ if (Math.abs(v) >= 1e6) return (v / 1e6).toFixed(1) + 'M';
16
+ if (Math.abs(v) >= 1e3) return (v / 1e3).toFixed(1) + 'K';
17
+ if (Number.isInteger(v)) return String(v);
18
+ return v.toFixed(1);
19
+ }
20
+
21
+ /**
22
+ * Format a date for display.
23
+ * @param {Date|number|string} d
24
+ * @returns {string}
25
+ */
26
+ export function formatDate(d) {
27
+ if (!(d instanceof Date)) d = new Date(d);
28
+ if (isNaN(d.getTime())) return '';
29
+ return `${d.getMonth() + 1}/${d.getDate()}`;
30
+ }
31
+
32
+ /**
33
+ * Format a date with time.
34
+ * @param {Date|number|string} d
35
+ * @returns {string}
36
+ */
37
+ export function formatDateTime(d) {
38
+ if (!(d instanceof Date)) d = new Date(d);
39
+ if (isNaN(d.getTime())) return '';
40
+ const h = d.getHours().toString().padStart(2, '0');
41
+ const m = d.getMinutes().toString().padStart(2, '0');
42
+ return `${d.getMonth() + 1}/${d.getDate()} ${h}:${m}`;
43
+ }
44
+
45
+ /**
46
+ * Format a duration in milliseconds.
47
+ * @param {number} ms
48
+ * @returns {string}
49
+ */
50
+ export function formatDuration(ms) {
51
+ if (ms < 1000) return ms + 'ms';
52
+ if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
53
+ if (ms < 3600000) return (ms / 60000).toFixed(1) + 'm';
54
+ if (ms < 86400000) return (ms / 3600000).toFixed(1) + 'h';
55
+ return (ms / 86400000).toFixed(1) + 'd';
56
+ }
57
+
58
+ /**
59
+ * Format a percentage.
60
+ * @param {number} v — 0-1 or 0-100
61
+ * @param {number} [decimals=1]
62
+ * @returns {string}
63
+ */
64
+ export function formatPercent(v, decimals = 1) {
65
+ const pct = v > 1 ? v : v * 100;
66
+ return pct.toFixed(decimals) + '%';
67
+ }
68
+
69
+ /**
70
+ * Format bytes to human-readable.
71
+ * @param {number} bytes
72
+ * @returns {string}
73
+ */
74
+ export function formatBytes(bytes) {
75
+ if (bytes < 1024) return bytes + ' B';
76
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
77
+ if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
78
+ return (bytes / 1073741824).toFixed(1) + ' GB';
79
+ }
80
+
81
+ /**
82
+ * Format currency.
83
+ * @param {number} v
84
+ * @param {string} [symbol='$']
85
+ * @returns {string}
86
+ */
87
+ export function formatCurrency(v, symbol = '$') {
88
+ return symbol + formatNumber(v);
89
+ }
90
+
91
+ /**
92
+ * Create a custom number formatter.
93
+ * @param {Object} opts
94
+ * @param {string} [opts.prefix='']
95
+ * @param {string} [opts.suffix='']
96
+ * @param {number} [opts.decimals=1]
97
+ * @param {boolean} [opts.compact=true]
98
+ * @returns {(v: number) => string}
99
+ */
100
+ export function createFormatter(opts = {}) {
101
+ const { prefix = '', suffix = '', decimals = 1, compact = true } = opts;
102
+ return function(v) {
103
+ const formatted = compact ? formatNumber(v) : v.toFixed(decimals);
104
+ return prefix + formatted + suffix;
105
+ };
106
+ }