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.
- package/AGENTS.md +868 -0
- package/CHANGELOG.md +255 -0
- package/CLAUDE.md +178 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/cli/art.js +127 -0
- package/cli/commands/a11y.js +61 -0
- package/cli/commands/audit.js +225 -0
- package/cli/commands/build.js +38 -0
- package/cli/commands/dev.js +18 -0
- package/cli/commands/doctor.js +197 -0
- package/cli/commands/figma-sync.js +48 -0
- package/cli/commands/figma-tokens.js +55 -0
- package/cli/commands/generate.js +26 -0
- package/cli/commands/init.js +116 -0
- package/cli/commands/lint.js +209 -0
- package/cli/commands/mcp.js +530 -0
- package/cli/commands/migrate.js +175 -0
- package/cli/commands/test.js +38 -0
- package/cli/commands/validate.js +354 -0
- package/cli/index.js +113 -0
- package/package.json +95 -0
- package/reference/atoms.md +517 -0
- package/reference/behaviors.md +384 -0
- package/reference/build-tooling.md +275 -0
- package/reference/color-guidelines.md +965 -0
- package/reference/component-lifecycle.md +137 -0
- package/reference/compound-spacing.md +95 -0
- package/reference/decantation-process.md +499 -0
- package/reference/dev-server-routes.md +93 -0
- package/reference/form-system.md +253 -0
- package/reference/i18n.md +336 -0
- package/reference/icons.md +576 -0
- package/reference/llm-primer.md +953 -0
- package/reference/plugins.md +252 -0
- package/reference/registry-consumption.md +76 -0
- package/reference/router.md +217 -0
- package/reference/shells.md +116 -0
- package/reference/spatial-guidelines.md +541 -0
- package/reference/ssr.md +234 -0
- package/reference/state-data.md +215 -0
- package/reference/state-patterns.md +166 -0
- package/reference/state.md +194 -0
- package/reference/style-system.md +110 -0
- package/reference/tokens.md +460 -0
- package/src/app.js +19 -0
- package/src/chart/_animate.js +266 -0
- package/src/chart/_base.js +109 -0
- package/src/chart/_data.js +209 -0
- package/src/chart/_format.js +106 -0
- package/src/chart/_interact.js +364 -0
- package/src/chart/_palette.js +105 -0
- package/src/chart/_renderer.js +52 -0
- package/src/chart/_scene.js +262 -0
- package/src/chart/_shared.js +371 -0
- package/src/chart/index.js +637 -0
- package/src/chart/layouts/_layout-base.js +328 -0
- package/src/chart/layouts/cartesian.js +148 -0
- package/src/chart/layouts/hierarchy.js +562 -0
- package/src/chart/layouts/polar.js +101 -0
- package/src/chart/renderers/canvas.js +179 -0
- package/src/chart/renderers/svg.js +256 -0
- package/src/chart/renderers/webgpu.js +715 -0
- package/src/chart/types/_type-base.js +26 -0
- package/src/chart/types/area.js +134 -0
- package/src/chart/types/bar.js +173 -0
- package/src/chart/types/box-plot.js +125 -0
- package/src/chart/types/bubble.js +63 -0
- package/src/chart/types/candlestick.js +115 -0
- package/src/chart/types/chord.js +85 -0
- package/src/chart/types/combination.js +108 -0
- package/src/chart/types/funnel.js +68 -0
- package/src/chart/types/gauge.js +163 -0
- package/src/chart/types/heatmap.js +98 -0
- package/src/chart/types/histogram.js +71 -0
- package/src/chart/types/line.js +111 -0
- package/src/chart/types/org-chart.js +93 -0
- package/src/chart/types/pie.js +81 -0
- package/src/chart/types/radar.js +96 -0
- package/src/chart/types/radial.js +68 -0
- package/src/chart/types/range-area.js +55 -0
- package/src/chart/types/range-bar.js +61 -0
- package/src/chart/types/sankey.js +73 -0
- package/src/chart/types/scatter.js +66 -0
- package/src/chart/types/sparkline.js +81 -0
- package/src/chart/types/sunburst.js +69 -0
- package/src/chart/types/swimlane.js +88 -0
- package/src/chart/types/treemap.js +62 -0
- package/src/chart/types/waterfall.js +100 -0
- package/src/components/_base.js +1658 -0
- package/src/components/_behaviors.js +1140 -0
- package/src/components/_primitives.js +534 -0
- package/src/components/_qr-encoder.js +539 -0
- package/src/components/accordion.js +207 -0
- package/src/components/affix.js +62 -0
- package/src/components/alert-dialog.js +75 -0
- package/src/components/alert.js +47 -0
- package/src/components/aspect-ratio.js +24 -0
- package/src/components/avatar-group.js +55 -0
- package/src/components/avatar.js +38 -0
- package/src/components/back-top.js +75 -0
- package/src/components/badge.js +74 -0
- package/src/components/banner.js +68 -0
- package/src/components/breadcrumb.js +162 -0
- package/src/components/button.js +115 -0
- package/src/components/calendar.js +131 -0
- package/src/components/card.js +192 -0
- package/src/components/carousel.js +98 -0
- package/src/components/cascader.js +261 -0
- package/src/components/checkbox.js +80 -0
- package/src/components/chip.js +81 -0
- package/src/components/code-block.js +82 -0
- package/src/components/collapsible.js +50 -0
- package/src/components/color-palette.js +438 -0
- package/src/components/color-picker.js +314 -0
- package/src/components/combobox.js +181 -0
- package/src/components/command.js +174 -0
- package/src/components/comment.js +206 -0
- package/src/components/context-menu.js +76 -0
- package/src/components/data-table.js +724 -0
- package/src/components/date-picker.js +217 -0
- package/src/components/date-range-picker.js +244 -0
- package/src/components/datetime-picker.js +271 -0
- package/src/components/descriptions.js +68 -0
- package/src/components/drawer.js +179 -0
- package/src/components/dropdown.js +88 -0
- package/src/components/empty.js +41 -0
- package/src/components/float-button.js +90 -0
- package/src/components/form.js +106 -0
- package/src/components/hover-card.js +49 -0
- package/src/components/icon.js +87 -0
- package/src/components/image.js +97 -0
- package/src/components/index.js +117 -0
- package/src/components/input-group.js +75 -0
- package/src/components/input-number.js +155 -0
- package/src/components/input-otp.js +178 -0
- package/src/components/input.js +91 -0
- package/src/components/kbd.js +36 -0
- package/src/components/label.js +25 -0
- package/src/components/list.js +118 -0
- package/src/components/masked-input.js +236 -0
- package/src/components/mentions.js +165 -0
- package/src/components/menu.js +259 -0
- package/src/components/message.js +80 -0
- package/src/components/modal.js +147 -0
- package/src/components/navigation-menu.js +166 -0
- package/src/components/notification.js +84 -0
- package/src/components/pagination.js +104 -0
- package/src/components/placeholder.js +132 -0
- package/src/components/popconfirm.js +70 -0
- package/src/components/popover.js +58 -0
- package/src/components/progress.js +61 -0
- package/src/components/qrcode.js +251 -0
- package/src/components/radiogroup.js +120 -0
- package/src/components/range-slider.js +176 -0
- package/src/components/rate.js +186 -0
- package/src/components/resizable.js +83 -0
- package/src/components/result.js +57 -0
- package/src/components/scroll-area.js +43 -0
- package/src/components/segmented.js +97 -0
- package/src/components/select.js +165 -0
- package/src/components/separator.js +31 -0
- package/src/components/shell.js +407 -0
- package/src/components/skeleton.js +39 -0
- package/src/components/slider.js +141 -0
- package/src/components/sortable-list.js +176 -0
- package/src/components/space.js +42 -0
- package/src/components/spinner.js +112 -0
- package/src/components/splitter.js +147 -0
- package/src/components/statistic.js +136 -0
- package/src/components/steps.js +99 -0
- package/src/components/switch.js +95 -0
- package/src/components/table.js +44 -0
- package/src/components/tabs.js +216 -0
- package/src/components/tag.js +115 -0
- package/src/components/textarea.js +82 -0
- package/src/components/time-picker.js +153 -0
- package/src/components/time-range-picker.js +170 -0
- package/src/components/timeline.js +226 -0
- package/src/components/toast.js +71 -0
- package/src/components/toggle.js +213 -0
- package/src/components/tooltip.js +57 -0
- package/src/components/tour.js +159 -0
- package/src/components/transfer.js +163 -0
- package/src/components/tree-select.js +274 -0
- package/src/components/tree.js +141 -0
- package/src/components/typography.js +136 -0
- package/src/components/upload.js +118 -0
- package/src/components/visually-hidden.js +20 -0
- package/src/components/watermark.js +124 -0
- package/src/core/index.js +539 -0
- package/src/core/lifecycle.js +69 -0
- package/src/css/atoms.js +651 -0
- package/src/css/components.js +940 -0
- package/src/css/derive.js +1296 -0
- package/src/css/index.js +265 -0
- package/src/css/runtime.js +268 -0
- package/src/css/styles/addons/bioluminescent.js +93 -0
- package/src/css/styles/addons/clay.js +70 -0
- package/src/css/styles/addons/clean.js +57 -0
- package/src/css/styles/addons/command-center.js +143 -0
- package/src/css/styles/addons/dopamine.js +83 -0
- package/src/css/styles/addons/editorial.js +80 -0
- package/src/css/styles/addons/glassmorphism.js +99 -0
- package/src/css/styles/addons/liquid-glass.js +105 -0
- package/src/css/styles/addons/prismatic.js +100 -0
- package/src/css/styles/addons/retro.js +63 -0
- package/src/css/styles/auradecantism.js +96 -0
- package/src/css/theme-registry.js +444 -0
- package/src/data/entity.js +281 -0
- package/src/data/index.js +13 -0
- package/src/data/persist.js +225 -0
- package/src/data/query.js +839 -0
- package/src/data/realtime.js +299 -0
- package/src/data/url.js +177 -0
- package/src/data/worker.js +134 -0
- package/src/explorer/archetypes.js +243 -0
- package/src/explorer/atoms.js +228 -0
- package/src/explorer/charts.js +497 -0
- package/src/explorer/components.js +129 -0
- package/src/explorer/foundations.js +949 -0
- package/src/explorer/icons.js +178 -0
- package/src/explorer/patterns.js +247 -0
- package/src/explorer/recipes.js +194 -0
- package/src/explorer/shared/pattern-examples.js +1337 -0
- package/src/explorer/shared/showcase-renderer.js +958 -0
- package/src/explorer/shared/spec-table.js +41 -0
- package/src/explorer/shared/usage-links.js +87 -0
- package/src/explorer/shell-config.js +10 -0
- package/src/explorer/shells.js +551 -0
- package/src/explorer/styles.js +161 -0
- package/src/explorer/tokens.js +262 -0
- package/src/explorer/tools.js +525 -0
- package/src/form/index.js +804 -0
- package/src/i18n/index.js +251 -0
- package/src/icons/essential.js +479 -0
- package/src/icons/index.js +53 -0
- package/src/plugins/index.js +282 -0
- package/src/registry/archetypes/content-site.json +71 -0
- package/src/registry/archetypes/docs-explorer.json +23 -0
- package/src/registry/archetypes/ecommerce.json +104 -0
- package/src/registry/archetypes/financial-dashboard.json +77 -0
- package/src/registry/archetypes/index.json +41 -0
- package/src/registry/archetypes/portfolio.json +82 -0
- package/src/registry/archetypes/recipe-community.json +159 -0
- package/src/registry/archetypes/saas-dashboard.json +86 -0
- package/src/registry/architect/cross-cutting.json +45 -0
- package/src/registry/architect/domains/ecommerce.json +294 -0
- package/src/registry/architect/domains/financial-services.json +302 -0
- package/src/registry/architect/index.json +26 -0
- package/src/registry/architect/traits.json +379 -0
- package/src/registry/atoms.json +16 -0
- package/src/registry/chart-showcase.json +160 -0
- package/src/registry/chart.json +136 -0
- package/src/registry/components.json +8616 -0
- package/src/registry/core.json +216 -0
- package/src/registry/css.json +319 -0
- package/src/registry/data.json +135 -0
- package/src/registry/foundations.json +11 -0
- package/src/registry/icons.json +463 -0
- package/src/registry/index.json +101 -0
- package/src/registry/patterns/activity-feed.json +37 -0
- package/src/registry/patterns/article-content.json +27 -0
- package/src/registry/patterns/auth-form.json +37 -0
- package/src/registry/patterns/author-card.json +20 -0
- package/src/registry/patterns/card-grid.json +127 -0
- package/src/registry/patterns/category-nav.json +26 -0
- package/src/registry/patterns/chart-grid.json +36 -0
- package/src/registry/patterns/chat-interface.json +37 -0
- package/src/registry/patterns/checklist-card.json +55 -0
- package/src/registry/patterns/comparison-panel.json +27 -0
- package/src/registry/patterns/component-showcase.json +24 -0
- package/src/registry/patterns/contact-form.json +31 -0
- package/src/registry/patterns/cta-section.json +20 -0
- package/src/registry/patterns/data-table.json +37 -0
- package/src/registry/patterns/detail-header.json +83 -0
- package/src/registry/patterns/detail-panel.json +27 -0
- package/src/registry/patterns/explorer-shell.json +22 -0
- package/src/registry/patterns/filter-bar.json +33 -0
- package/src/registry/patterns/filter-sidebar.json +27 -0
- package/src/registry/patterns/form-sections.json +110 -0
- package/src/registry/patterns/goal-tracker.json +27 -0
- package/src/registry/patterns/hero.json +107 -0
- package/src/registry/patterns/index.json +47 -0
- package/src/registry/patterns/kpi-grid.json +36 -0
- package/src/registry/patterns/media-gallery.json +20 -0
- package/src/registry/patterns/order-history.json +20 -0
- package/src/registry/patterns/pagination.json +19 -0
- package/src/registry/patterns/photo-to-recipe.json +36 -0
- package/src/registry/patterns/pipeline-tracker.json +28 -0
- package/src/registry/patterns/post-list.json +27 -0
- package/src/registry/patterns/pricing-table.json +32 -0
- package/src/registry/patterns/scorecard.json +28 -0
- package/src/registry/patterns/search-bar.json +20 -0
- package/src/registry/patterns/specimen-grid.json +19 -0
- package/src/registry/patterns/stat-card.json +55 -0
- package/src/registry/patterns/stats-bar.json +55 -0
- package/src/registry/patterns/steps-card.json +55 -0
- package/src/registry/patterns/table-of-contents.json +19 -0
- package/src/registry/patterns/testimonials.json +21 -0
- package/src/registry/patterns/timeline.json +27 -0
- package/src/registry/patterns/token-inspector.json +21 -0
- package/src/registry/patterns/wizard.json +27 -0
- package/src/registry/recipe-auradecantism.json +69 -0
- package/src/registry/recipe-clean.json +65 -0
- package/src/registry/recipe-command-center.json +78 -0
- package/src/registry/router.json +73 -0
- package/src/registry/schema/README.md +197 -0
- package/src/registry/skeletons.json +259 -0
- package/src/registry/state.json +137 -0
- package/src/registry/tokens.json +40 -0
- package/src/router/hash.js +17 -0
- package/src/router/history.js +18 -0
- package/src/router/index.js +598 -0
- package/src/ssr/index.js +922 -0
- package/src/state/arrays.js +181 -0
- package/src/state/devtools.js +647 -0
- package/src/state/index.js +498 -0
- package/src/state/middleware.js +288 -0
- package/src/state/scheduler.js +206 -0
- package/src/state/store.js +300 -0
- package/src/tags/index.js +19 -0
- package/src/tannins/auth.js +396 -0
- package/src/test/dom.js +352 -0
- package/src/test/index.js +62 -0
- package/src/test/state.js +306 -0
- package/tools/a11y-audit.js +487 -0
- package/tools/analyzer.js +315 -0
- package/tools/audit.js +706 -0
- package/tools/builder.js +1422 -0
- package/tools/css-extract.js +188 -0
- package/tools/dev-server.js +316 -0
- package/tools/dts-gen.js +1260 -0
- package/tools/figma-components.js +329 -0
- package/tools/figma-patterns.js +516 -0
- package/tools/figma-plugin/code.js +453 -0
- package/tools/figma-plugin/manifest.json +14 -0
- package/tools/figma-plugin/ui.html +268 -0
- package/tools/figma-render.js +293 -0
- package/tools/figma-tokens.js +712 -0
- package/tools/figma-upload.js +318 -0
- package/tools/generate.js +738 -0
- package/tools/icons.js +133 -0
- package/tools/init-templates.js +265 -0
- package/tools/install-hooks.sh +5 -0
- package/tools/migrations/0.5.0.js +53 -0
- package/tools/migrations/0.6.0.js +95 -0
- package/tools/minify.js +170 -0
- package/tools/pre-commit +4 -0
- package/tools/registry.js +662 -0
- package/tools/reset-playground.js +61 -0
- package/tools/starter-templates/content-site/app.js +49 -0
- package/tools/starter-templates/content-site/essence.js +19 -0
- package/tools/starter-templates/content-site/pages.js +31 -0
- package/tools/starter-templates/ecommerce/app.js +50 -0
- package/tools/starter-templates/ecommerce/essence.js +19 -0
- package/tools/starter-templates/ecommerce/pages.js +31 -0
- package/tools/starter-templates/landing-page/app.js +38 -0
- package/tools/starter-templates/landing-page/essence.js +18 -0
- package/tools/starter-templates/landing-page/pages.js +21 -0
- package/tools/starter-templates/portfolio/app.js +45 -0
- package/tools/starter-templates/portfolio/essence.js +19 -0
- package/tools/starter-templates/portfolio/pages.js +33 -0
- package/tools/starter-templates/saas-dashboard/app.js +70 -0
- package/tools/starter-templates/saas-dashboard/essence.js +19 -0
- package/tools/starter-templates/saas-dashboard/pages.js +31 -0
- package/tools/verify-pack.js +203 -0
- package/types/chart.d.ts +77 -0
- package/types/components.d.ts +587 -0
- package/types/core.d.ts +89 -0
- package/types/css.d.ts +149 -0
- package/types/data.d.ts +238 -0
- package/types/form.d.ts +164 -0
- package/types/i18n.d.ts +51 -0
- package/types/icons.d.ts +27 -0
- package/types/index.d.ts +13 -0
- package/types/router.d.ts +116 -0
- package/types/ssr.d.ts +102 -0
- package/types/state.d.ts +83 -0
- package/types/tags.d.ts +62 -0
- package/types/tannins.d.ts +63 -0
- 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
|
+
}
|