@keyfunc/keybricks-ds 1.0.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/DEPLOY.md +204 -0
- package/DESIGN.md +269 -0
- package/INTEGRATION.md +417 -0
- package/SKILL.md +32 -0
- package/_adherence.oxlintrc.json +1469 -0
- package/_scripts/build-index.mjs +80 -0
- package/_scripts/build-tokens.mjs +135 -0
- package/assets/icons/icon-index.js +1 -0
- package/assets/icons/icon-index.json +1 -0
- package/assets/icons/unicons-index.js +1 -0
- package/assets/icons/unicons-index.json +1 -0
- package/assets/keybricks-logo.png +0 -0
- package/components/_cards/accordion.card.html +32 -0
- package/components/_cards/action-chip.card.html +22 -0
- package/components/_cards/activity.card.html +34 -0
- package/components/_cards/alert.card.html +32 -0
- package/components/_cards/app-chrome.card.html +36 -0
- package/components/_cards/banner.card.html +21 -0
- package/components/_cards/brand-visual.card.html +43 -0
- package/components/_cards/breadcrumb.card.html +23 -0
- package/components/_cards/button-1-base.card.html +39 -0
- package/components/_cards/button-2-states.card.html +56 -0
- package/components/_cards/button-3-sizes-icons.card.html +32 -0
- package/components/_cards/button-4-group.card.html +47 -0
- package/components/_cards/canvas-zoom-controls.card.html +19 -0
- package/components/_cards/cards-indicators.card.html +59 -0
- package/components/_cards/charts-1-sparkline.card.html +55 -0
- package/components/_cards/charts-10-gauge.card.html +31 -0
- package/components/_cards/charts-11-funnel.card.html +28 -0
- package/components/_cards/charts-12-treemap.card.html +23 -0
- package/components/_cards/charts-14-radar.card.html +31 -0
- package/components/_cards/charts-15-distribution.card.html +32 -0
- package/components/_cards/charts-16-pareto.card.html +25 -0
- package/components/_cards/charts-17-candlestick.card.html +34 -0
- package/components/_cards/charts-18-sankey.card.html +29 -0
- package/components/_cards/charts-19-chord.card.html +31 -0
- package/components/_cards/charts-2-line.card.html +46 -0
- package/components/_cards/charts-20-sunburst.card.html +38 -0
- package/components/_cards/charts-21-worldmap.card.html +33 -0
- package/components/_cards/charts-22-gantt.card.html +37 -0
- package/components/_cards/charts-23-bar3d.card.html +26 -0
- package/components/_cards/charts-24-pie3d.card.html +32 -0
- package/components/_cards/charts-25-surface3d.card.html +24 -0
- package/components/_cards/charts-26-scatter3d.card.html +24 -0
- package/components/_cards/charts-27-ribbon3d.card.html +23 -0
- package/components/_cards/charts-3-bar.card.html +71 -0
- package/components/_cards/charts-4-barlist.card.html +30 -0
- package/components/_cards/charts-5-donut.card.html +35 -0
- package/components/_cards/charts-6-scatter.card.html +26 -0
- package/components/_cards/charts-7-heatmap.card.html +28 -0
- package/components/_cards/charts-8-waterfall.card.html +25 -0
- package/components/_cards/charts-9-bullet.card.html +31 -0
- package/components/_cards/charts-99-fullpage.card.html +194 -0
- package/components/_cards/checkbox.card.html +34 -0
- package/components/_cards/codeblock.card.html +19 -0
- package/components/_cards/combobox.card.html +25 -0
- package/components/_cards/context-menu.card.html +21 -0
- package/components/_cards/datatable.card.html +48 -0
- package/components/_cards/date-picker.card.html +31 -0
- package/components/_cards/diffvalue.card.html +21 -0
- package/components/_cards/divider.card.html +24 -0
- package/components/_cards/drawer.card.html +30 -0
- package/components/_cards/dropdown-menu.card.html +38 -0
- package/components/_cards/dropzone.card.html +22 -0
- package/components/_cards/empty-state.card.html +29 -0
- package/components/_cards/filter-bar.card.html +34 -0
- package/components/_cards/icon-button.card.html +27 -0
- package/components/_cards/icons-1-core.card.html +49 -0
- package/components/_cards/icons-2-feather.card.html +39 -0
- package/components/_cards/icons-3-unicons.card.html +44 -0
- package/components/_cards/input.card.html +25 -0
- package/components/_cards/kbd.card.html +20 -0
- package/components/_cards/keyvalue.card.html +23 -0
- package/components/_cards/listrow.card.html +30 -0
- package/components/_cards/loading.card.html +33 -0
- package/components/_cards/log-viewer.card.html +23 -0
- package/components/_cards/modal.card.html +35 -0
- package/components/_cards/multi-select.card.html +26 -0
- package/components/_cards/number-stepper.card.html +24 -0
- package/components/_cards/pagination.card.html +25 -0
- package/components/_cards/people.card.html +46 -0
- package/components/_cards/pipeline-node.card.html +76 -0
- package/components/_cards/pipeline-palette.card.html +54 -0
- package/components/_cards/search.card.html +23 -0
- package/components/_cards/select.card.html +23 -0
- package/components/_cards/slider.card.html +25 -0
- package/components/_cards/smart-table.card.html +37 -0
- package/components/_cards/spreadsheet.card.html +31 -0
- package/components/_cards/stepper.card.html +25 -0
- package/components/_cards/tabs.card.html +25 -0
- package/components/_cards/tag.card.html +26 -0
- package/components/_cards/textarea.card.html +27 -0
- package/components/_cards/timeline.card.html +32 -0
- package/components/_cards/toast.card.html +33 -0
- package/components/_cards/toggles.card.html +68 -0
- package/components/_cards/tooltip.card.html +22 -0
- package/components/_cards/tree.card.html +150 -0
- package/components/_shared/frame-theme.js +16 -0
- package/components/_shared/src-brand.js +1 -0
- package/components/_shared/src-buttons.js +1 -0
- package/components/_shared/src-charts.js +1 -0
- package/components/_shared/src-data.js +1 -0
- package/components/_shared/src-display.js +1 -0
- package/components/_shared/src-feedback.js +1 -0
- package/components/_shared/src-forms.js +1 -0
- package/components/_shared/src-icons.js +1 -0
- package/components/_shared/src-navigation.js +1 -0
- package/components/_shared/src-pipeline.js +1 -0
- package/components/_shared/src-tabs.js +1 -0
- package/components/_shared/svg-download.js +19 -0
- package/components/_shared/view-source.js +185 -0
- package/components/_vue/index.mjs +114 -0
- package/components/brand/BrandVisual.d.ts +17 -0
- package/components/brand/BrandVisual.jsx +29 -0
- package/components/brand/BrandVisual.vue +34 -0
- package/components/brand/_dir.card.html +13 -0
- package/components/brand/brandMotifs.js +210 -0
- package/components/buttons/ActionChip.d.ts +10 -0
- package/components/buttons/ActionChip.jsx +20 -0
- package/components/buttons/ActionChip.vue +18 -0
- package/components/buttons/Button.d.ts +40 -0
- package/components/buttons/Button.jsx +101 -0
- package/components/buttons/Button.vue +87 -0
- package/components/buttons/IconButton.d.ts +26 -0
- package/components/buttons/IconButton.jsx +28 -0
- package/components/buttons/IconButton.vue +21 -0
- package/components/buttons/_dir.card.html +13 -0
- package/components/buttons/buttons.prompt.md +19 -0
- package/components/charts/Bar3D.d.ts +15 -0
- package/components/charts/Bar3D.jsx +93 -0
- package/components/charts/Bar3D.vue +79 -0
- package/components/charts/BarChart.d.ts +23 -0
- package/components/charts/BarChart.jsx +135 -0
- package/components/charts/BarChart.vue +139 -0
- package/components/charts/BarList.d.ts +13 -0
- package/components/charts/BarList.jsx +31 -0
- package/components/charts/BarList.vue +25 -0
- package/components/charts/BoxPlot.d.ts +18 -0
- package/components/charts/BoxPlot.jsx +62 -0
- package/components/charts/BoxPlot.vue +58 -0
- package/components/charts/BulletChart.d.ts +12 -0
- package/components/charts/BulletChart.jsx +41 -0
- package/components/charts/BulletChart.vue +33 -0
- package/components/charts/CandlestickChart.d.ts +15 -0
- package/components/charts/CandlestickChart.jsx +63 -0
- package/components/charts/CandlestickChart.vue +62 -0
- package/components/charts/ChartKit.d.ts +8 -0
- package/components/charts/ChartKit.jsx +76 -0
- package/components/charts/ChartKit.vue +47 -0
- package/components/charts/ChordChart.d.ts +13 -0
- package/components/charts/ChordChart.jsx +55 -0
- package/components/charts/ChordChart.vue +54 -0
- package/components/charts/DonutChart.d.ts +15 -0
- package/components/charts/DonutChart.jsx +73 -0
- package/components/charts/DonutChart.vue +61 -0
- package/components/charts/FunnelChart.d.ts +11 -0
- package/components/charts/FunnelChart.jsx +37 -0
- package/components/charts/FunnelChart.vue +29 -0
- package/components/charts/GanttChart.d.ts +20 -0
- package/components/charts/GanttChart.jsx +80 -0
- package/components/charts/GanttChart.vue +60 -0
- package/components/charts/GaugeChart.d.ts +13 -0
- package/components/charts/GaugeChart.jsx +33 -0
- package/components/charts/GaugeChart.vue +29 -0
- package/components/charts/HeatmapChart.d.ts +13 -0
- package/components/charts/HeatmapChart.jsx +62 -0
- package/components/charts/HeatmapChart.vue +38 -0
- package/components/charts/Histogram.d.ts +16 -0
- package/components/charts/Histogram.jsx +56 -0
- package/components/charts/Histogram.vue +53 -0
- package/components/charts/LineChart.d.ts +17 -0
- package/components/charts/LineChart.jsx +84 -0
- package/components/charts/LineChart.vue +86 -0
- package/components/charts/ParetoChart.d.ts +13 -0
- package/components/charts/ParetoChart.jsx +54 -0
- package/components/charts/ParetoChart.vue +55 -0
- package/components/charts/Pie3D.d.ts +17 -0
- package/components/charts/Pie3D.jsx +145 -0
- package/components/charts/Pie3D.vue +115 -0
- package/components/charts/RadarChart.d.ts +14 -0
- package/components/charts/RadarChart.jsx +79 -0
- package/components/charts/RadarChart.vue +73 -0
- package/components/charts/Ribbon3D.d.ts +13 -0
- package/components/charts/Ribbon3D.jsx +89 -0
- package/components/charts/Ribbon3D.vue +79 -0
- package/components/charts/SankeyChart.d.ts +15 -0
- package/components/charts/SankeyChart.jsx +59 -0
- package/components/charts/SankeyChart.vue +53 -0
- package/components/charts/Scatter3D.d.ts +12 -0
- package/components/charts/Scatter3D.jsx +69 -0
- package/components/charts/Scatter3D.vue +59 -0
- package/components/charts/ScatterChart.d.ts +12 -0
- package/components/charts/ScatterChart.jsx +53 -0
- package/components/charts/ScatterChart.vue +57 -0
- package/components/charts/Sparkline.d.ts +19 -0
- package/components/charts/Sparkline.jsx +63 -0
- package/components/charts/Sparkline.vue +54 -0
- package/components/charts/SunburstChart.d.ts +12 -0
- package/components/charts/SunburstChart.jsx +61 -0
- package/components/charts/SunburstChart.vue +60 -0
- package/components/charts/Surface3D.d.ts +13 -0
- package/components/charts/Surface3D.jsx +70 -0
- package/components/charts/Surface3D.vue +63 -0
- package/components/charts/TreemapChart.d.ts +11 -0
- package/components/charts/TreemapChart.jsx +35 -0
- package/components/charts/TreemapChart.vue +27 -0
- package/components/charts/WaterfallChart.d.ts +11 -0
- package/components/charts/WaterfallChart.jsx +59 -0
- package/components/charts/WaterfallChart.vue +46 -0
- package/components/charts/WorldMap.d.ts +21 -0
- package/components/charts/WorldMap.jsx +107 -0
- package/components/charts/WorldMap.vue +73 -0
- package/components/charts/_dir.card.html +7 -0
- package/components/charts/chart-3d.js +84 -0
- package/components/charts/chart-geometry.js +223 -0
- package/components/charts/charts.prompt.md +24 -0
- package/components/data/CodeBlock.d.ts +9 -0
- package/components/data/CodeBlock.jsx +19 -0
- package/components/data/CodeBlock.vue +22 -0
- package/components/data/DataTable.d.ts +28 -0
- package/components/data/DataTable.jsx +32 -0
- package/components/data/DataTable.vue +25 -0
- package/components/data/DiffValue.d.ts +11 -0
- package/components/data/DiffValue.jsx +15 -0
- package/components/data/DiffValue.vue +13 -0
- package/components/data/FilterBar.d.ts +16 -0
- package/components/data/FilterBar.jsx +22 -0
- package/components/data/FilterBar.vue +23 -0
- package/components/data/KeyValue.d.ts +16 -0
- package/components/data/KeyValue.jsx +25 -0
- package/components/data/KeyValue.vue +26 -0
- package/components/data/ListRow.d.ts +14 -0
- package/components/data/ListRow.jsx +25 -0
- package/components/data/ListRow.vue +25 -0
- package/components/data/Pagination.d.ts +12 -0
- package/components/data/Pagination.jsx +27 -0
- package/components/data/Pagination.vue +24 -0
- package/components/data/SmartTable.d.ts +30 -0
- package/components/data/SmartTable.jsx +73 -0
- package/components/data/SmartTable.vue +74 -0
- package/components/data/Spreadsheet.d.ts +33 -0
- package/components/data/Spreadsheet.jsx +110 -0
- package/components/data/Spreadsheet.vue +95 -0
- package/components/data/_dir.card.html +13 -0
- package/components/data/data.prompt.md +33 -0
- package/components/display/Avatar.d.ts +21 -0
- package/components/display/Avatar.jsx +31 -0
- package/components/display/Avatar.vue +34 -0
- package/components/display/Badge.d.ts +27 -0
- package/components/display/Badge.jsx +40 -0
- package/components/display/Badge.vue +47 -0
- package/components/display/Card.d.ts +27 -0
- package/components/display/Card.jsx +27 -0
- package/components/display/Card.vue +28 -0
- package/components/display/Divider.d.ts +8 -0
- package/components/display/Divider.jsx +14 -0
- package/components/display/Divider.vue +13 -0
- package/components/display/Kbd.d.ts +7 -0
- package/components/display/Kbd.jsx +12 -0
- package/components/display/Kbd.vue +9 -0
- package/components/display/ProgressBar.d.ts +42 -0
- package/components/display/ProgressBar.jsx +70 -0
- package/components/display/ProgressBar.vue +69 -0
- package/components/display/Tag.d.ts +9 -0
- package/components/display/Tag.jsx +15 -0
- package/components/display/Tag.vue +15 -0
- package/components/display/_dir.card.html +13 -0
- package/components/display/display.prompt.md +30 -0
- package/components/feedback/Accordion.d.ts +13 -0
- package/components/feedback/Accordion.jsx +21 -0
- package/components/feedback/Accordion.vue +22 -0
- package/components/feedback/ActivityItem.d.ts +25 -0
- package/components/feedback/ActivityItem.jsx +39 -0
- package/components/feedback/ActivityItem.vue +46 -0
- package/components/feedback/Alert.d.ts +36 -0
- package/components/feedback/Alert.jsx +48 -0
- package/components/feedback/Alert.vue +59 -0
- package/components/feedback/Banner.d.ts +15 -0
- package/components/feedback/Banner.jsx +25 -0
- package/components/feedback/Banner.vue +25 -0
- package/components/feedback/ContextMenu.d.ts +8 -0
- package/components/feedback/ContextMenu.jsx +18 -0
- package/components/feedback/ContextMenu.vue +19 -0
- package/components/feedback/Drawer.d.ts +18 -0
- package/components/feedback/Drawer.jsx +26 -0
- package/components/feedback/Drawer.vue +24 -0
- package/components/feedback/Menu.d.ts +10 -0
- package/components/feedback/Menu.jsx +24 -0
- package/components/feedback/Menu.vue +21 -0
- package/components/feedback/Modal.d.ts +38 -0
- package/components/feedback/Modal.jsx +69 -0
- package/components/feedback/Modal.vue +65 -0
- package/components/feedback/Stepper.d.ts +9 -0
- package/components/feedback/Stepper.jsx +24 -0
- package/components/feedback/Stepper.vue +18 -0
- package/components/feedback/Timeline.d.ts +15 -0
- package/components/feedback/Timeline.jsx +27 -0
- package/components/feedback/Timeline.vue +21 -0
- package/components/feedback/_dir.card.html +13 -0
- package/components/feedback/feedback.prompt.md +35 -0
- package/components/forms/Checkbox.d.ts +13 -0
- package/components/forms/Checkbox.jsx +17 -0
- package/components/forms/Checkbox.vue +18 -0
- package/components/forms/Combobox.d.ts +13 -0
- package/components/forms/Combobox.jsx +34 -0
- package/components/forms/Combobox.vue +35 -0
- package/components/forms/DatePicker.d.ts +12 -0
- package/components/forms/DatePicker.jsx +48 -0
- package/components/forms/DatePicker.vue +46 -0
- package/components/forms/Dropzone.d.ts +20 -0
- package/components/forms/Dropzone.jsx +36 -0
- package/components/forms/Dropzone.vue +28 -0
- package/components/forms/Field.d.ts +13 -0
- package/components/forms/Field.jsx +13 -0
- package/components/forms/Field.vue +12 -0
- package/components/forms/Input.d.ts +18 -0
- package/components/forms/Input.jsx +35 -0
- package/components/forms/Input.vue +38 -0
- package/components/forms/MultiSelect.d.ts +14 -0
- package/components/forms/MultiSelect.jsx +33 -0
- package/components/forms/MultiSelect.vue +36 -0
- package/components/forms/NumberStepper.d.ts +13 -0
- package/components/forms/NumberStepper.jsx +15 -0
- package/components/forms/NumberStepper.vue +17 -0
- package/components/forms/Radio.vue +16 -0
- package/components/forms/RadioGroup.vue +11 -0
- package/components/forms/SearchInput.d.ts +12 -0
- package/components/forms/SearchInput.jsx +17 -0
- package/components/forms/SearchInput.vue +14 -0
- package/components/forms/Select.d.ts +14 -0
- package/components/forms/Select.jsx +17 -0
- package/components/forms/Select.vue +16 -0
- package/components/forms/Slider.d.ts +15 -0
- package/components/forms/Slider.jsx +21 -0
- package/components/forms/Slider.vue +23 -0
- package/components/forms/Switch.d.ts +31 -0
- package/components/forms/Switch.jsx +40 -0
- package/components/forms/Switch.vue +16 -0
- package/components/forms/_dir.card.html +13 -0
- package/components/forms/forms.prompt.md +23 -0
- package/components/icons/Icon.d.ts +19 -0
- package/components/icons/Icon.jsx +93 -0
- package/components/icons/Icon.prompt.md +8 -0
- package/components/icons/Icon.vue +12 -0
- package/components/icons/Unicon.d.ts +12 -0
- package/components/icons/Unicon.jsx +16 -0
- package/components/icons/Unicon.vue +15 -0
- package/components/icons/_dir.card.html +13 -0
- package/components/icons/feather-paths.js +2 -0
- package/components/icons/icon-paths.js +73 -0
- package/components/index.d.ts +87 -0
- package/components/index.js +110 -0
- package/components/navigation/Breadcrumb.d.ts +24 -0
- package/components/navigation/Breadcrumb.jsx +120 -0
- package/components/navigation/Breadcrumb.vue +94 -0
- package/components/navigation/DocTree.d.ts +18 -0
- package/components/navigation/DocTree.jsx +37 -0
- package/components/navigation/DocTree.vue +25 -0
- package/components/navigation/Sidebar.d.ts +29 -0
- package/components/navigation/Sidebar.jsx +75 -0
- package/components/navigation/Sidebar.vue +54 -0
- package/components/navigation/TopBar.d.ts +27 -0
- package/components/navigation/TopBar.jsx +44 -0
- package/components/navigation/TopBar.vue +51 -0
- package/components/navigation/Tree.d.ts +83 -0
- package/components/navigation/Tree.jsx +347 -0
- package/components/navigation/Tree.vue +258 -0
- package/components/navigation/_dir.card.html +13 -0
- package/components/navigation/navigation.prompt.md +22 -0
- package/components/pipeline/ConfigPanel.d.ts +18 -0
- package/components/pipeline/ConfigPanel.jsx +30 -0
- package/components/pipeline/ConfigPanel.vue +32 -0
- package/components/pipeline/DataPreview.d.ts +14 -0
- package/components/pipeline/DataPreview.jsx +24 -0
- package/components/pipeline/DataPreview.vue +21 -0
- package/components/pipeline/NodeCard.d.ts +47 -0
- package/components/pipeline/NodeCard.jsx +97 -0
- package/components/pipeline/NodeCard.vue +75 -0
- package/components/pipeline/PaletteItem.vue +15 -0
- package/components/pipeline/PaletteListItem.vue +15 -0
- package/components/pipeline/PalettePanel.d.ts +20 -0
- package/components/pipeline/PalettePanel.jsx +74 -0
- package/components/pipeline/PalettePanel.vue +45 -0
- package/components/pipeline/PipelineCanvas.d.ts +25 -0
- package/components/pipeline/PipelineCanvas.jsx +94 -0
- package/components/pipeline/PipelineCanvas.vue +73 -0
- package/components/pipeline/ZoomControls.d.ts +9 -0
- package/components/pipeline/ZoomControls.jsx +15 -0
- package/components/pipeline/ZoomControls.vue +15 -0
- package/components/pipeline/_dir.card.html +13 -0
- package/components/pipeline/pipeline.prompt.md +22 -0
- package/components/tabs/SegmentedControl.d.ts +11 -0
- package/components/tabs/SegmentedControl.jsx +21 -0
- package/components/tabs/SegmentedControl.vue +15 -0
- package/components/tabs/Tabs.d.ts +13 -0
- package/components/tabs/Tabs.jsx +52 -0
- package/components/tabs/Tabs.vue +39 -0
- package/components/tabs/ViewToggle.d.ts +8 -0
- package/components/tabs/ViewToggle.jsx +12 -0
- package/components/tabs/ViewToggle.vue +12 -0
- package/components/tabs/_dir.card.html +13 -0
- package/components/tabs/tabs.prompt.md +14 -0
- package/package.json +76 -0
- package/readme.md +150 -0
- package/styles.css +10 -0
- package/tokens/alpha.css +27 -0
- package/tokens/charts.css +55 -0
- package/tokens/colors-extended.css +40 -0
- package/tokens/colors.css +39 -0
- package/tokens/dark.css +96 -0
- package/tokens/fonts.css +4 -0
- package/tokens/interaction.css +48 -0
- package/tokens/layout.css +30 -0
- package/tokens/motion.css +22 -0
- package/tokens/tokens.d.ts +680 -0
- package/tokens/tokens.js +1018 -0
- package/tokens/tokens.json +2928 -0
- package/tokens/typography.css +22 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, niceTicks, scaleLin, seriesColor } from "./chart-geometry.js";
|
|
3
|
+
import { ChartTooltip, PlotGrid, wrapStyle } from "./ChartKit.jsx";
|
|
4
|
+
|
|
5
|
+
/** Five-number summary from raw values (Tukey fences for outliers). */
|
|
6
|
+
export function summarize(values) {
|
|
7
|
+
const v = values.slice().sort((a, b) => a - b);
|
|
8
|
+
const q = (p) => { const i = (v.length - 1) * p, lo = Math.floor(i), hi = Math.ceil(i); return v[lo] + (v[hi] - v[lo]) * (i - lo); };
|
|
9
|
+
const q1 = q(0.25), med = q(0.5), q3 = q(0.75), iqr = q3 - q1;
|
|
10
|
+
const lo = Math.max(v[0], q1 - 1.5 * iqr), hi = Math.min(v[v.length - 1], q3 + 1.5 * iqr);
|
|
11
|
+
return { q1, med, q3, lo, hi, outliers: v.filter((x) => x < lo || x > hi) };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Box & whisker — spread and outliers per group. groups: [{label, values}] (summarized
|
|
16
|
+
* for you) or [{label, q1, med, q3, lo, hi, outliers}] when you have the stats already.
|
|
17
|
+
*/
|
|
18
|
+
export function BoxPlot({ groups = [], width = 560, height = 260, axes = true, valueFormat = fmtNum, interactive = true, style }) {
|
|
19
|
+
const [hover, setHover] = React.useState(null);
|
|
20
|
+
const boxes = groups.map((g) => (g.values ? { label: g.label, color: g.color, ...summarize(g.values) } : g));
|
|
21
|
+
const pad = { l: axes ? 44 : 4, r: 8, t: 12, b: axes ? 26 : 4 };
|
|
22
|
+
const all = boxes.flatMap((b) => [b.lo, b.hi, ...(b.outliers || [])]);
|
|
23
|
+
const { ticks, lo, hi } = niceTicks(Math.min(...all), Math.max(...all), 4);
|
|
24
|
+
const sy = scaleLin(lo, hi, height - pad.b, pad.t);
|
|
25
|
+
const slot = (width - pad.l - pad.r) / boxes.length;
|
|
26
|
+
const bw = Math.min(slot * 0.5, 38);
|
|
27
|
+
const move = (e) => {
|
|
28
|
+
if (!interactive) return;
|
|
29
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
30
|
+
const i = Math.floor((e.clientX - r.left - pad.l) / slot);
|
|
31
|
+
setHover(i >= 0 && i < boxes.length ? i : null);
|
|
32
|
+
};
|
|
33
|
+
const h = hover != null ? boxes[hover] : null;
|
|
34
|
+
return (
|
|
35
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
36
|
+
<svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role="img">
|
|
37
|
+
{axes ? <PlotGrid w={width} h={height} pad={pad} ticks={ticks} sy={sy} xLabels={[]} sxLabel={() => 0} yFormat={valueFormat} /> : null}
|
|
38
|
+
{boxes.map((b, i) => {
|
|
39
|
+
const x = pad.l + slot * i + slot / 2;
|
|
40
|
+
const c = b.color || seriesColor(i);
|
|
41
|
+
return (
|
|
42
|
+
<g key={b.label} opacity={hover == null || hover === i ? 1 : 0.45} style={{ transition: "opacity var(--dur-fast) var(--ease-out)" }}>
|
|
43
|
+
<line x1={x} x2={x} y1={sy(b.hi)} y2={sy(b.q3)} stroke="var(--chart-axis)" strokeWidth="1" />
|
|
44
|
+
<line x1={x} x2={x} y1={sy(b.q1)} y2={sy(b.lo)} stroke="var(--chart-axis)" strokeWidth="1" />
|
|
45
|
+
<line x1={x - bw / 3} x2={x + bw / 3} y1={sy(b.hi)} y2={sy(b.hi)} stroke="var(--chart-axis)" strokeWidth="1" />
|
|
46
|
+
<line x1={x - bw / 3} x2={x + bw / 3} y1={sy(b.lo)} y2={sy(b.lo)} stroke="var(--chart-axis)" strokeWidth="1" />
|
|
47
|
+
<rect x={x - bw / 2} y={sy(b.q3)} width={bw} height={Math.max(1, sy(b.q1) - sy(b.q3))} rx="2" fill={c} fillOpacity="0.28" stroke={c} strokeWidth="1.2" />
|
|
48
|
+
<line x1={x - bw / 2} x2={x + bw / 2} y1={sy(b.med)} y2={sy(b.med)} stroke={c} strokeWidth="2" />
|
|
49
|
+
{(b.outliers || []).map((o, oi) => <circle key={oi} cx={x} cy={sy(o)} r="2.2" fill="none" stroke={c} strokeWidth="1" />)}
|
|
50
|
+
<text x={x} y={height - pad.b + 16} textAnchor="middle" style={{ fontFamily: "var(--font-sans)", fontSize: 10, fill: "var(--chart-label)" }}>{b.label}</text>
|
|
51
|
+
</g>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
</svg>
|
|
55
|
+
{h && interactive ? (
|
|
56
|
+
<ChartTooltip x={pad.l + slot * hover + slot / 2} y={height / 2 - 10} flip={pad.l + slot * hover > width * 0.62} title={h.label}
|
|
57
|
+
rows={[{ label: "max", value: valueFormat(h.hi) }, { label: "Q3", value: valueFormat(h.q3) }, { label: "median", value: valueFormat(h.med), color: h.color || seriesColor(hover) }, { label: "Q1", value: valueFormat(h.q1) }, { label: "min", value: valueFormat(h.lo) }]
|
|
58
|
+
.concat((h.outliers || []).length ? [{ label: "outliers", value: String(h.outliers.length) }] : [])} />
|
|
59
|
+
) : null}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, niceTicks, scaleLin, SERIES } from './chart-geometry.js'
|
|
4
|
+
// Box & whisker with Tukey-fence outliers. Pass raw values or the five-number stats.
|
|
5
|
+
const props = defineProps({ groups:{type:Array,default:()=>[]}, width:{type:Number,default:560}, height:{type:Number,default:260}, axes:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })
|
|
6
|
+
const hover = ref(null)
|
|
7
|
+
function summarize(values){
|
|
8
|
+
const v = values.slice().sort((a,b)=>a-b)
|
|
9
|
+
const q = p => { const i=(v.length-1)*p, lo=Math.floor(i), hi=Math.ceil(i); return v[lo]+(v[hi]-v[lo])*(i-lo) }
|
|
10
|
+
const q1=q(0.25), med=q(0.5), q3=q(0.75), iqr=q3-q1
|
|
11
|
+
const lo=Math.max(v[0], q1-1.5*iqr), hi=Math.min(v[v.length-1], q3+1.5*iqr)
|
|
12
|
+
return { q1, med, q3, lo, hi, outliers: v.filter(x=>x<lo||x>hi) }
|
|
13
|
+
}
|
|
14
|
+
const g = computed(() => {
|
|
15
|
+
const boxes = props.groups.map((x,i)=> Object.assign({ label:x.label, color:x.color||SERIES[i%SERIES.length] }, x.values ? summarize(x.values) : x))
|
|
16
|
+
const pad = { l: props.axes?44:4, r:8, t:12, b: props.axes?26:4 }
|
|
17
|
+
const all = boxes.flatMap(b=>[b.lo, b.hi, ...(b.outliers||[])])
|
|
18
|
+
const { ticks, lo, hi } = niceTicks(Math.min(...all), Math.max(...all), 4)
|
|
19
|
+
const sy = scaleLin(lo, hi, props.height-pad.b, pad.t)
|
|
20
|
+
const slot = (props.width-pad.l-pad.r)/boxes.length
|
|
21
|
+
const bw = Math.min(slot*0.5, 38)
|
|
22
|
+
return { boxes: boxes.map((b,i)=>Object.assign({}, b, { x: pad.l+slot*i+slot/2, bw })), pad, ticks, sy, slot }
|
|
23
|
+
})
|
|
24
|
+
function move(e){
|
|
25
|
+
if(!props.interactive) return
|
|
26
|
+
const r = e.currentTarget.getBoundingClientRect()
|
|
27
|
+
const i = Math.floor((e.clientX-r.left-g.value.pad.l)/g.value.slot)
|
|
28
|
+
hover.value = i>=0 && i<g.value.boxes.length ? i : null
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
<template>
|
|
32
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
33
|
+
<svg :width="width" :height="height" @mousemove="move" @mouseleave="hover=null" role="img">
|
|
34
|
+
<template v-if="axes">
|
|
35
|
+
<g v-for="t in g.ticks" :key="t">
|
|
36
|
+
<line :x1="g.pad.l" :x2="width-g.pad.r" :y1="g.sy(t)" :y2="g.sy(t)" :stroke="t===0?'var(--chart-axis)':'var(--chart-grid)'" stroke-width="1"/>
|
|
37
|
+
<text :x="g.pad.l-8" :y="g.sy(t)+3" text-anchor="end" style="font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)">{{ valueFormat(t) }}</text>
|
|
38
|
+
</g>
|
|
39
|
+
</template>
|
|
40
|
+
<g v-for="(b,i) in g.boxes" :key="b.label" :opacity="hover==null||hover===i?1:0.45" style="transition:opacity var(--dur-fast) var(--ease-out)">
|
|
41
|
+
<line :x1="b.x" :x2="b.x" :y1="g.sy(b.hi)" :y2="g.sy(b.q3)" stroke="var(--chart-axis)" stroke-width="1"/>
|
|
42
|
+
<line :x1="b.x" :x2="b.x" :y1="g.sy(b.q1)" :y2="g.sy(b.lo)" stroke="var(--chart-axis)" stroke-width="1"/>
|
|
43
|
+
<line :x1="b.x-b.bw/3" :x2="b.x+b.bw/3" :y1="g.sy(b.hi)" :y2="g.sy(b.hi)" stroke="var(--chart-axis)" stroke-width="1"/>
|
|
44
|
+
<line :x1="b.x-b.bw/3" :x2="b.x+b.bw/3" :y1="g.sy(b.lo)" :y2="g.sy(b.lo)" stroke="var(--chart-axis)" stroke-width="1"/>
|
|
45
|
+
<rect :x="b.x-b.bw/2" :y="g.sy(b.q3)" :width="b.bw" :height="Math.max(1,g.sy(b.q1)-g.sy(b.q3))" rx="2" :fill="b.color" fill-opacity="0.28" :stroke="b.color" stroke-width="1.2"/>
|
|
46
|
+
<line :x1="b.x-b.bw/2" :x2="b.x+b.bw/2" :y1="g.sy(b.med)" :y2="g.sy(b.med)" :stroke="b.color" stroke-width="2"/>
|
|
47
|
+
<circle v-for="(o,oi) in (b.outliers||[])" :key="oi" :cx="b.x" :cy="g.sy(o)" r="2.2" fill="none" :stroke="b.color" stroke-width="1"/>
|
|
48
|
+
<text :x="b.x" :y="height-g.pad.b+16" text-anchor="middle" style="font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)">{{ b.label }}</text>
|
|
49
|
+
</g>
|
|
50
|
+
</svg>
|
|
51
|
+
<div v-if="hover!=null && interactive" :style="{position:'absolute',left:g.boxes[hover].x+'px',top:(height/2-10)+'px',transform:g.boxes[hover].x>width*0.62?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}">
|
|
52
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px">{{ g.boxes[hover].label }}</div>
|
|
53
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">
|
|
54
|
+
{{ valueFormat(g.boxes[hover].lo) }} · Q1 {{ valueFormat(g.boxes[hover].q1) }} · med {{ valueFormat(g.boxes[hover].med) }} · Q3 {{ valueFormat(g.boxes[hover].q3) }} · {{ valueFormat(g.boxes[hover].hi) }}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface BulletRow { label: string; value: number; target: number; max?: number; /** [poor, ok] track tints */ ranges?: [number, number]; }
|
|
3
|
+
/** Actual vs. target rows with a target tick. tone "auto" colors misses red. */
|
|
4
|
+
export interface BulletChartProps {
|
|
5
|
+
rows?: BulletRow[];
|
|
6
|
+
width?: number;
|
|
7
|
+
tone?: "default" | "auto";
|
|
8
|
+
valueFormat?: (v: number) => string;
|
|
9
|
+
interactive?: boolean;
|
|
10
|
+
style?: React.CSSProperties;
|
|
11
|
+
}
|
|
12
|
+
export declare function BulletChart(props: BulletChartProps): JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum } from "./chart-geometry.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Bullet rows — actual vs. target on a qualitative track. rows:
|
|
6
|
+
* [{label, value, target, max?, ranges?: [poor, ok] }] — ranges tint the track;
|
|
7
|
+
* the bar is --series-1 (or --chart-neg when value misses target with tone="auto").
|
|
8
|
+
*/
|
|
9
|
+
export function BulletChart({ rows = [], width = 360, valueFormat = fmtNum, tone = "default", interactive = true, style }) {
|
|
10
|
+
const [hover, setHover] = React.useState(null);
|
|
11
|
+
return (
|
|
12
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 14, width, ...style }}>
|
|
13
|
+
{rows.map((r) => {
|
|
14
|
+
const max = r.max != null ? r.max : Math.max(r.value, r.target) * 1.15;
|
|
15
|
+
const pc = (v) => Math.min(100, (v / max) * 100) + "%";
|
|
16
|
+
const missed = r.value < r.target;
|
|
17
|
+
const bar = tone === "auto" ? (missed ? "var(--chart-neg)" : "var(--chart-pos)") : "var(--series-1)";
|
|
18
|
+
const on = hover === r.label;
|
|
19
|
+
return (
|
|
20
|
+
<div key={r.label} onMouseEnter={interactive ? () => setHover(r.label) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
21
|
+
style={{ opacity: hover == null || on ? 1 : 0.55, transition: "var(--t-hover)" }}>
|
|
22
|
+
<div style={{ display: "flex", alignItems: "baseline", paddingBottom: 4 }}>
|
|
23
|
+
<span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", color: "var(--text-body)" }}>{r.label}</span>
|
|
24
|
+
<span style={{ marginLeft: "auto", fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--text-heading)" }}>{valueFormat(r.value)}</span>
|
|
25
|
+
<span style={{ fontFamily: "var(--font-mono)", fontSize: "var(--text-2xs)", color: "var(--text-muted)", paddingLeft: 6 }}>/ {valueFormat(r.target)}</span>
|
|
26
|
+
{on ? <span style={{ fontFamily: "var(--font-mono)", fontSize: "var(--text-2xs)", color: missed ? "var(--chart-neg)" : "var(--chart-pos)", paddingLeft: 8 }}>{(missed ? "" : "+") + (((r.value - r.target) / (r.target || 1)) * 100).toFixed(1)}%</span> : null}
|
|
27
|
+
</div>
|
|
28
|
+
<div style={{ position: "relative", height: 12, borderRadius: 3, background: "var(--chart-track)", overflow: "hidden" }}>
|
|
29
|
+
{r.ranges ? <>
|
|
30
|
+
<div style={{ position: "absolute", left: 0, top: 0, bottom: 0, width: pc(r.ranges[0]), background: "var(--alpha-heavy, rgba(0,0,0,.12))", opacity: 0.5 }} />
|
|
31
|
+
<div style={{ position: "absolute", left: 0, top: 0, bottom: 0, width: pc(r.ranges[1]), background: "var(--alpha-heavy, rgba(0,0,0,.12))", opacity: 0.25 }} />
|
|
32
|
+
</> : null}
|
|
33
|
+
<div style={{ position: "absolute", left: 0, top: 3, bottom: 3, width: pc(r.value), borderRadius: 2, background: bar, transition: "width var(--dur) var(--ease-out)" }} />
|
|
34
|
+
<div style={{ position: "absolute", left: pc(r.target), top: 0, bottom: 0, width: 2, background: "var(--chart-total)" }} />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
})}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { fmtNum } from './chart-geometry.js'
|
|
4
|
+
// Actual vs. target rows with a target tick. tone "auto" colors misses red.
|
|
5
|
+
const props = defineProps({ rows:{type:Array,default:()=>[]}, width:{type:Number,default:360}, tone:{type:String,default:'default'}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })
|
|
6
|
+
const hover = ref(null)
|
|
7
|
+
function geom(r){
|
|
8
|
+
const max = r.max!=null ? r.max : Math.max(r.value,r.target)*1.15
|
|
9
|
+
const pc = v => Math.min(100,(v/max)*100)+'%'
|
|
10
|
+
return { pc, bar: props.tone==='auto' ? (r.value<r.target?'var(--chart-neg)':'var(--chart-pos)') : 'var(--series-1)' }
|
|
11
|
+
}
|
|
12
|
+
</script>
|
|
13
|
+
<template>
|
|
14
|
+
<div :style="{display:'flex',flexDirection:'column',gap:'14px',width:width+'px',...style}">
|
|
15
|
+
<div v-for="r in rows" :key="r.label" @mouseenter="interactive && (hover=r.label)" @mouseleave="interactive && (hover=null)"
|
|
16
|
+
:style="{opacity:hover==null||hover===r.label?1:0.55,transition:'var(--t-hover)'}">
|
|
17
|
+
<div style="display:flex;align-items:baseline;padding-bottom:4px">
|
|
18
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-body)">{{ r.label }}</span>
|
|
19
|
+
<span style="margin-left:auto;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">{{ valueFormat(r.value) }}</span>
|
|
20
|
+
<span style="font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--text-muted);padding-left:6px">/ {{ valueFormat(r.target) }}</span>
|
|
21
|
+
<span v-if="hover===r.label" :style="{fontFamily:'var(--font-mono)',fontSize:'var(--text-2xs)',paddingLeft:'8px',color:r.value<r.target?'var(--chart-neg)':'var(--chart-pos)'}">{{ (r.value<r.target?'':'+')+(((r.value-r.target)/(r.target||1))*100).toFixed(1) }}%</span>
|
|
22
|
+
</div>
|
|
23
|
+
<div style="position:relative;height:12px;border-radius:3px;background:var(--chart-track);overflow:hidden">
|
|
24
|
+
<template v-if="r.ranges">
|
|
25
|
+
<div :style="{position:'absolute',left:0,top:0,bottom:0,width:geom(r).pc(r.ranges[0]),background:'rgba(0,0,0,.12)',opacity:.5}"/>
|
|
26
|
+
<div :style="{position:'absolute',left:0,top:0,bottom:0,width:geom(r).pc(r.ranges[1]),background:'rgba(0,0,0,.12)',opacity:.25}"/>
|
|
27
|
+
</template>
|
|
28
|
+
<div :style="{position:'absolute',left:0,top:'3px',bottom:'3px',width:geom(r).pc(r.value),borderRadius:'2px',background:geom(r).bar,transition:'width var(--dur) var(--ease-out)'}"/>
|
|
29
|
+
<div :style="{position:'absolute',left:geom(r).pc(r.target),top:0,bottom:0,width:'2px',background:'var(--chart-total)'}"/>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface Candle { /** Axis label (date) */ t: string; o: number; h: number; l: number; c: number; /** Volume */ v?: number; }
|
|
3
|
+
/** OHLC K-line with optional volume band and moving average. */
|
|
4
|
+
export interface CandlestickChartProps {
|
|
5
|
+
candles?: Candle[];
|
|
6
|
+
width?: number; height?: number;
|
|
7
|
+
axes?: boolean;
|
|
8
|
+
/** Volume histogram under the price plot */ volume?: boolean;
|
|
9
|
+
/** Moving-average window in periods; 0 = off */ ma?: number;
|
|
10
|
+
/** Hollow bodies for up candles (East-Asian convention) */ hollowUp?: boolean;
|
|
11
|
+
valueFormat?: (v: number) => string;
|
|
12
|
+
interactive?: boolean;
|
|
13
|
+
style?: React.CSSProperties;
|
|
14
|
+
}
|
|
15
|
+
export declare function CandlestickChart(props: CandlestickChartProps): JSX.Element;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, niceTicks, scaleLin } from "./chart-geometry.js";
|
|
3
|
+
import { ChartTooltip, PlotGrid, wrapStyle } from "./ChartKit.jsx";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Candlestick (K-line) — OHLC price action with an optional volume band and moving average.
|
|
7
|
+
* candles: [{t, o, h, l, c, v?}]. Up candles are --chart-pos, down --chart-neg; a hovered
|
|
8
|
+
* candle pins the full OHLC readout. Set hollowUp for the East-Asian hollow-body convention.
|
|
9
|
+
*/
|
|
10
|
+
export function CandlestickChart({ candles = [], width = 620, height = 300, axes = true, volume = true, ma = 0, hollowUp = false, valueFormat = fmtNum, interactive = true, style }) {
|
|
11
|
+
const [hover, setHover] = React.useState(null);
|
|
12
|
+
const volH = volume ? Math.round(height * 0.22) : 0;
|
|
13
|
+
const pad = { l: axes ? 46 : 4, r: 8, t: 10, b: (axes ? 22 : 4) + volH };
|
|
14
|
+
const { ticks, lo, hi } = niceTicks(Math.min(...candles.map((c) => c.l)), Math.max(...candles.map((c) => c.h)), 4);
|
|
15
|
+
const sy = scaleLin(lo, hi, height - pad.b, pad.t);
|
|
16
|
+
const slot = (width - pad.l - pad.r) / (candles.length || 1);
|
|
17
|
+
const bw = Math.max(2, Math.min(slot * 0.62, 14));
|
|
18
|
+
const cx = (i) => pad.l + slot * i + slot / 2;
|
|
19
|
+
const maxV = Math.max(1, ...candles.map((c) => c.v || 0));
|
|
20
|
+
const vy = (v) => height - (axes ? 22 : 4) - (v / maxV) * (volH - 6);
|
|
21
|
+
const avg = ma > 1 ? candles.map((_, i) => (i < ma - 1 ? null : candles.slice(i - ma + 1, i + 1).reduce((s, c) => s + c.c, 0) / ma)) : null;
|
|
22
|
+
const move = (e) => {
|
|
23
|
+
if (!interactive) return;
|
|
24
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
25
|
+
const i = Math.floor((e.clientX - r.left - pad.l) / slot);
|
|
26
|
+
setHover(i >= 0 && i < candles.length ? i : null);
|
|
27
|
+
};
|
|
28
|
+
const h = hover != null ? candles[hover] : null;
|
|
29
|
+
const up = (c) => c.c >= c.o;
|
|
30
|
+
return (
|
|
31
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
32
|
+
<svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role="img">
|
|
33
|
+
{axes ? <PlotGrid w={width} h={height} pad={pad} ticks={ticks} sy={sy} xLabels={[]} sxLabel={() => 0} yFormat={valueFormat} /> : null}
|
|
34
|
+
{hover != null ? <rect x={pad.l + slot * hover} y={pad.t} width={slot} height={height - pad.t - (axes ? 22 : 4)} fill="var(--bg-hover)" /> : null}
|
|
35
|
+
{avg ? <path d={"M" + avg.map((v, i) => (v == null ? null : cx(i).toFixed(1) + " " + sy(v).toFixed(1))).filter(Boolean).join(" L")} fill="none" stroke="var(--series-8)" strokeWidth="1.3" /> : null}
|
|
36
|
+
{candles.map((c, i) => {
|
|
37
|
+
const col = up(c) ? "var(--chart-pos)" : "var(--chart-neg)";
|
|
38
|
+
const yO = sy(c.o), yC = sy(c.c);
|
|
39
|
+
const top = Math.min(yO, yC), body = Math.max(1.5, Math.abs(yC - yO));
|
|
40
|
+
const solid = !hollowUp || !up(c);
|
|
41
|
+
return (
|
|
42
|
+
<g key={i} opacity={hover == null || hover === i ? 1 : 0.5} style={{ transition: "opacity var(--dur-fast) var(--ease-out)" }}>
|
|
43
|
+
<line x1={cx(i)} x2={cx(i)} y1={sy(c.h)} y2={sy(c.l)} stroke={col} strokeWidth="1" />
|
|
44
|
+
<rect x={cx(i) - bw / 2} y={top} width={bw} height={body} fill={solid ? col : "var(--bg-surface)"} stroke={col} strokeWidth="1" />
|
|
45
|
+
{volume && c.v != null ? <rect x={cx(i) - bw / 2} y={vy(c.v)} width={bw} height={Math.max(1, height - (axes ? 22 : 4) - vy(c.v))} fill={col} opacity="0.35" /> : null}
|
|
46
|
+
</g>
|
|
47
|
+
);
|
|
48
|
+
})}
|
|
49
|
+
{volume ? <line x1={pad.l} x2={width - pad.r} y1={height - (axes ? 22 : 4)} y2={height - (axes ? 22 : 4)} stroke="var(--chart-axis)" strokeWidth="1" /> : null}
|
|
50
|
+
{axes ? candles.map((c, i) => (i % Math.ceil(candles.length / 6) ? null : (
|
|
51
|
+
<text key={"t" + i} x={cx(i)} y={height - 6} textAnchor="middle" style={{ fontFamily: "var(--font-sans)", fontSize: 10, fill: "var(--chart-label)" }}>{c.t}</text>
|
|
52
|
+
))) : null}
|
|
53
|
+
</svg>
|
|
54
|
+
{h && interactive ? (
|
|
55
|
+
<ChartTooltip x={cx(hover)} y={height / 2 - 20} flip={cx(hover) > width * 0.62} title={h.t}
|
|
56
|
+
rows={[{ label: "open", value: valueFormat(h.o) }, { label: "high", value: valueFormat(h.h) }, { label: "low", value: valueFormat(h.l) },
|
|
57
|
+
{ label: "close", value: valueFormat(h.c), color: up(h) ? "var(--chart-pos)" : "var(--chart-neg)" },
|
|
58
|
+
{ label: "change", value: ((h.c - h.o) >= 0 ? "+" : "") + (((h.c - h.o) / (h.o || 1)) * 100).toFixed(2) + "%" }]
|
|
59
|
+
.concat(h.v != null ? [{ label: "volume", value: fmtNum(h.v) }] : [])} />
|
|
60
|
+
) : null}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, niceTicks, scaleLin } from './chart-geometry.js'
|
|
4
|
+
// OHLC K-line with optional volume band and moving average.
|
|
5
|
+
const props = defineProps({ candles:{type:Array,default:()=>[]}, width:{type:Number,default:620}, height:{type:Number,default:300}, axes:{type:Boolean,default:true}, volume:{type:Boolean,default:true}, ma:{type:Number,default:0}, hollowUp:Boolean, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })
|
|
6
|
+
const hover = ref(null)
|
|
7
|
+
const g = computed(() => {
|
|
8
|
+
const volH = props.volume ? Math.round(props.height*0.22) : 0
|
|
9
|
+
const axisB = props.axes ? 22 : 4
|
|
10
|
+
const pad = { l: props.axes?46:4, r:8, t:10, b: axisB+volH }
|
|
11
|
+
const { ticks, lo, hi } = niceTicks(Math.min(...props.candles.map(c=>c.l)), Math.max(...props.candles.map(c=>c.h)), 4)
|
|
12
|
+
const sy = scaleLin(lo, hi, props.height-pad.b, pad.t)
|
|
13
|
+
const slot = (props.width-pad.l-pad.r)/(props.candles.length||1)
|
|
14
|
+
const bw = Math.max(2, Math.min(slot*0.62, 14))
|
|
15
|
+
const cx = i => pad.l+slot*i+slot/2
|
|
16
|
+
const maxV = Math.max(1, ...props.candles.map(c=>c.v||0))
|
|
17
|
+
const vy = v => props.height-axisB-(v/maxV)*(volH-6)
|
|
18
|
+
const avg = props.ma>1 ? props.candles.map((_,i)=> i<props.ma-1 ? null : props.candles.slice(i-props.ma+1,i+1).reduce((s,c)=>s+c.c,0)/props.ma) : null
|
|
19
|
+
const bars = props.candles.map((c,i)=>{
|
|
20
|
+
const up = c.c>=c.o, col = up?'var(--chart-pos)':'var(--chart-neg)'
|
|
21
|
+
const yO=sy(c.o), yC=sy(c.c)
|
|
22
|
+
return { c, i, up, col, x:cx(i), bw, top:Math.min(yO,yC), body:Math.max(1.5,Math.abs(yC-yO)), yH:sy(c.h), yL:sy(c.l),
|
|
23
|
+
solid: !props.hollowUp || !up, vY: c.v!=null?vy(c.v):null, vH: c.v!=null?Math.max(1, props.height-axisB-vy(c.v)):0 }
|
|
24
|
+
})
|
|
25
|
+
const maPath = avg ? 'M'+avg.map((v,i)=> v==null?null:cx(i).toFixed(1)+' '+sy(v).toFixed(1)).filter(Boolean).join(' L') : null
|
|
26
|
+
return { pad, ticks, sy, slot, cx, bars, maPath, axisB, every: Math.ceil(props.candles.length/6) }
|
|
27
|
+
})
|
|
28
|
+
function move(e){
|
|
29
|
+
if(!props.interactive) return
|
|
30
|
+
const r = e.currentTarget.getBoundingClientRect()
|
|
31
|
+
const i = Math.floor((e.clientX-r.left-g.value.pad.l)/g.value.slot)
|
|
32
|
+
hover.value = i>=0 && i<props.candles.length ? i : null
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
<template>
|
|
36
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
37
|
+
<svg :width="width" :height="height" @mousemove="move" @mouseleave="hover=null" role="img">
|
|
38
|
+
<template v-if="axes">
|
|
39
|
+
<g v-for="t in g.ticks" :key="t">
|
|
40
|
+
<line :x1="g.pad.l" :x2="width-g.pad.r" :y1="g.sy(t)" :y2="g.sy(t)" :stroke="t===0?'var(--chart-axis)':'var(--chart-grid)'" stroke-width="1"/>
|
|
41
|
+
<text :x="g.pad.l-8" :y="g.sy(t)+3" text-anchor="end" style="font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)">{{ valueFormat(t) }}</text>
|
|
42
|
+
</g>
|
|
43
|
+
</template>
|
|
44
|
+
<rect v-if="hover!=null" :x="g.pad.l+g.slot*hover" :y="g.pad.t" :width="g.slot" :height="height-g.pad.t-g.axisB" fill="var(--bg-hover)"/>
|
|
45
|
+
<path v-if="g.maPath" :d="g.maPath" fill="none" stroke="var(--series-8)" stroke-width="1.3"/>
|
|
46
|
+
<g v-for="b in g.bars" :key="b.i" :opacity="hover==null||hover===b.i?1:0.5" style="transition:opacity var(--dur-fast) var(--ease-out)">
|
|
47
|
+
<line :x1="b.x" :x2="b.x" :y1="b.yH" :y2="b.yL" :stroke="b.col" stroke-width="1"/>
|
|
48
|
+
<rect :x="b.x-b.bw/2" :y="b.top" :width="b.bw" :height="b.body" :fill="b.solid?b.col:'var(--bg-surface)'" :stroke="b.col" stroke-width="1"/>
|
|
49
|
+
<rect v-if="volume && b.vY!=null" :x="b.x-b.bw/2" :y="b.vY" :width="b.bw" :height="b.vH" :fill="b.col" opacity="0.35"/>
|
|
50
|
+
</g>
|
|
51
|
+
<line v-if="volume" :x1="g.pad.l" :x2="width-g.pad.r" :y1="height-g.axisB" :y2="height-g.axisB" stroke="var(--chart-axis)" stroke-width="1"/>
|
|
52
|
+
<text v-if="axes" v-for="b in g.bars.filter(b=>b.i%g.every===0)" :key="'t'+b.i" :x="b.x" :y="height-6" text-anchor="middle" style="font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)">{{ b.c.t }}</text>
|
|
53
|
+
</svg>
|
|
54
|
+
<div v-if="hover!=null && interactive" :style="{position:'absolute',left:g.cx(hover)+'px',top:(height/2-20)+'px',transform:g.cx(hover)>width*0.62?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}">
|
|
55
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px">{{ candles[hover].t }}</div>
|
|
56
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">O {{ valueFormat(candles[hover].o) }} · H {{ valueFormat(candles[hover].h) }} · L {{ valueFormat(candles[hover].l) }} · C {{ valueFormat(candles[hover].c) }}</div>
|
|
57
|
+
<div :style="{fontFamily:'var(--font-mono)',fontSize:'var(--text-2xs)',color:candles[hover].c>=candles[hover].o?'var(--chart-pos)':'var(--chart-neg)'}">
|
|
58
|
+
{{ (candles[hover].c-candles[hover].o>=0?'+':'')+(((candles[hover].c-candles[hover].o)/(candles[hover].o||1))*100).toFixed(2) }}%<span v-if="candles[hover].v!=null" style="color:var(--text-muted)"> · vol {{ fmtNum(candles[hover].v) }}</span>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Shared chart furniture: legend, tooltip, grid. Used by every chart; rarely imported directly. */
|
|
3
|
+
export interface LegendItem { label: string; color?: string; }
|
|
4
|
+
export interface ChartLegendProps { items?: LegendItem[]; hidden?: string[]; onToggle?: (label: string) => void; align?: "left" | "right"; style?: React.CSSProperties; }
|
|
5
|
+
export declare function ChartLegend(props: ChartLegendProps): JSX.Element;
|
|
6
|
+
export interface TooltipRow { label: string; value: string; color?: string; }
|
|
7
|
+
export interface ChartTooltipProps { x: number; y: number; title?: React.ReactNode; rows?: TooltipRow[]; /** Render to the left of the anchor */ flip?: boolean; }
|
|
8
|
+
export declare function ChartTooltip(props: ChartTooltipProps): JSX.Element;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, seriesColor } from "./chart-geometry.js";
|
|
3
|
+
|
|
4
|
+
const sans = "var(--font-sans)", mono = "var(--font-mono)";
|
|
5
|
+
|
|
6
|
+
/** Legend row. items: [{label,color}] — with onToggle, clicking mutes a series. */
|
|
7
|
+
export function ChartLegend({ items = [], hidden = [], onToggle, align = "left", style }) {
|
|
8
|
+
return (
|
|
9
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: "4px 14px", justifyContent: align === "right" ? "flex-end" : "flex-start", lineHeight: "normal", ...style }}>
|
|
10
|
+
{items.map((it, i) => {
|
|
11
|
+
const off = hidden.includes(it.label);
|
|
12
|
+
return (
|
|
13
|
+
<span key={it.label} role={onToggle ? "button" : undefined} tabIndex={onToggle ? 0 : undefined}
|
|
14
|
+
onClick={onToggle ? () => onToggle(it.label) : undefined}
|
|
15
|
+
style={{ display: "inline-flex", alignItems: "center", gap: 6, cursor: onToggle ? "pointer" : "default", opacity: off ? 0.4 : 1, transition: "var(--t-hover)" }}>
|
|
16
|
+
<span style={{ width: 8, height: 8, borderRadius: 2, background: it.color || seriesColor(i), flexShrink: 0 }} />
|
|
17
|
+
<span style={{ fontFamily: sans, fontSize: "var(--text-xs)", color: "var(--text-secondary)", textDecoration: off ? "line-through" : "none" }}>{it.label}</span>
|
|
18
|
+
</span>
|
|
19
|
+
);
|
|
20
|
+
})}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Floating tooltip. rows: [{label,value,color}]; pins near (x,y) inside the chart wrapper. */
|
|
26
|
+
export function ChartTooltip({ x, y, title, rows = [], flip }) {
|
|
27
|
+
return (
|
|
28
|
+
<div style={{ position: "absolute", left: x, top: y, transform: `translate(${flip ? "calc(-100% - 10px)" : "10px"}, -50%)`, pointerEvents: "none", zIndex: 5, background: "var(--bg-surface)", border: "1px solid var(--border-card)", borderRadius: "var(--radius-md)", boxShadow: "var(--shadow-menu)", padding: "7px 10px", minWidth: 90, lineHeight: "normal", whiteSpace: "nowrap" }}>
|
|
29
|
+
{title != null ? <div style={{ fontFamily: sans, fontSize: "var(--text-2xs)", color: "var(--text-muted)", paddingBottom: rows.length ? 4 : 0, whiteSpace: "nowrap" }}>{title}</div> : null}
|
|
30
|
+
{rows.map((r, i) => (
|
|
31
|
+
<div key={i} style={{ display: "flex", alignItems: "center", gap: 6, padding: "1px 0" }}>
|
|
32
|
+
{r.color ? <span style={{ width: 7, height: 7, borderRadius: 2, background: r.color, flexShrink: 0 }} /> : null}
|
|
33
|
+
<span style={{ fontFamily: sans, fontSize: "var(--text-xs)", color: "var(--text-secondary)", whiteSpace: "nowrap" }}>{r.label}</span>
|
|
34
|
+
<span style={{ fontFamily: mono, fontSize: "var(--text-xs)", color: "var(--text-heading)", marginLeft: "auto", paddingLeft: 10 }}>{r.value}</span>
|
|
35
|
+
</div>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** y-axis tick labels + horizontal gridlines, x category labels. Shared plot furniture. */
|
|
42
|
+
export function PlotGrid({ w, h, pad, ticks, sy, xLabels = [], sxLabel, showAxisLine = false, yFormat = fmtNum }) {
|
|
43
|
+
return (
|
|
44
|
+
<g>
|
|
45
|
+
{ticks.map((t) => (
|
|
46
|
+
<g key={t}>
|
|
47
|
+
<line x1={pad.l} x2={w - pad.r} y1={sy(t)} y2={sy(t)} stroke={t === 0 ? "var(--chart-axis)" : "var(--chart-grid)"} strokeWidth="1" />
|
|
48
|
+
<text x={pad.l - 8} y={sy(t) + 3} textAnchor="end" style={{ fontFamily: mono, fontSize: 10, fill: "var(--chart-label)" }}>{yFormat(t)}</text>
|
|
49
|
+
</g>
|
|
50
|
+
))}
|
|
51
|
+
{showAxisLine ? <line x1={pad.l} x2={pad.l} y1={pad.t} y2={h - pad.b} stroke="var(--chart-axis)" strokeWidth="1" /> : null}
|
|
52
|
+
{xLabels.map((lb, i) => (lb == null ? null : (
|
|
53
|
+
<text key={i} x={sxLabel(i)} y={h - pad.b + 16} textAnchor="middle" style={{ fontFamily: sans, fontSize: 10, fill: "var(--chart-label)" }}>{lb}</text>
|
|
54
|
+
)))}
|
|
55
|
+
</g>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Thin x labels only (sm tier / no y axis). */
|
|
60
|
+
export function XLabels({ h, pad, xLabels = [], sxLabel }) {
|
|
61
|
+
return <g>{xLabels.map((lb, i) => (lb == null ? null : <text key={i} x={sxLabel(i)} y={h - pad.b + 16} textAnchor="middle" style={{ fontFamily: sans, fontSize: 10, fill: "var(--chart-label)" }}>{lb}</text>))}</g>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Skip x labels so they never collide: keeps ~every nth. */
|
|
65
|
+
export function thinLabels(labels, maxShown = 8) {
|
|
66
|
+
const n = labels.length;
|
|
67
|
+
if (n <= maxShown) return labels;
|
|
68
|
+
const step = Math.ceil(n / maxShown);
|
|
69
|
+
return labels.map((l, i) => (i % step === 0 || i === n - 1 ? l : null));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Wrapper div a chart mounts into: relative for tooltips, block-level svg. */
|
|
73
|
+
export const wrapStyle = { position: "relative", display: "inline-block", verticalAlign: "top", lineHeight: 0 };
|
|
74
|
+
|
|
75
|
+
/** Namespace handle so the kit can be imported as one symbol. */
|
|
76
|
+
export const ChartKit = { ChartLegend, ChartTooltip, PlotGrid, XLabels, thinLabels, wrapStyle };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { SERIES } from './chart-geometry.js'
|
|
4
|
+
/**
|
|
5
|
+
* Shared chart furniture for the Vue charts — one component, two shapes via `kind`
|
|
6
|
+
* ("legend" | "tooltip"), so a single import covers both. The React side exposes these
|
|
7
|
+
* as ChartLegend / ChartTooltip.
|
|
8
|
+
*/
|
|
9
|
+
const props = defineProps({
|
|
10
|
+
kind: { type: String, default: 'legend' },
|
|
11
|
+
items: { type: Array, default: () => [] },
|
|
12
|
+
hidden: { type: Array, default: () => [] },
|
|
13
|
+
align: { type: String, default: 'left' },
|
|
14
|
+
x: Number,
|
|
15
|
+
y: Number,
|
|
16
|
+
title: String,
|
|
17
|
+
rows: { type: Array, default: () => [] },
|
|
18
|
+
flip: Boolean,
|
|
19
|
+
style: Object,
|
|
20
|
+
})
|
|
21
|
+
const emit = defineEmits(['toggle'])
|
|
22
|
+
const tipStyle = computed(() => ({
|
|
23
|
+
position: 'absolute', left: props.x + 'px', top: props.y + 'px',
|
|
24
|
+
transform: props.flip ? 'translate(calc(-100% - 10px),-50%)' : 'translate(10px,-50%)',
|
|
25
|
+
pointerEvents: 'none', zIndex: 5, background: 'var(--bg-surface)',
|
|
26
|
+
border: '1px solid var(--border-card)', borderRadius: 'var(--radius-md)',
|
|
27
|
+
boxShadow: 'var(--shadow-menu)', padding: '7px 10px', minWidth: '90px',
|
|
28
|
+
lineHeight: 'normal', whiteSpace: 'nowrap',
|
|
29
|
+
}))
|
|
30
|
+
</script>
|
|
31
|
+
<template>
|
|
32
|
+
<div v-if="kind === 'tooltip'" :style="tipStyle">
|
|
33
|
+
<div v-if="title != null" style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:4px">{{ title }}</div>
|
|
34
|
+
<div v-for="(r,i) in rows" :key="i" style="display:flex;align-items:center;gap:6px;padding:1px 0">
|
|
35
|
+
<span v-if="r.color" :style="{width:'7px',height:'7px',borderRadius:'2px',background:r.color,flexShrink:0}"/>
|
|
36
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)">{{ r.label }}</span>
|
|
37
|
+
<span style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);margin-left:auto;padding-left:10px">{{ r.value }}</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div v-else :style="{display:'flex',flexWrap:'wrap',gap:'4px 14px',justifyContent:align==='right'?'flex-end':'flex-start',lineHeight:'normal',...style}">
|
|
41
|
+
<span v-for="(it,i) in items" :key="it.label" role="button" tabindex="0" @click="emit('toggle', it.label)"
|
|
42
|
+
:style="{display:'inline-flex',alignItems:'center',gap:'6px',cursor:'pointer',opacity:hidden.includes(it.label)?0.4:1,transition:'var(--t-hover)'}">
|
|
43
|
+
<span :style="{width:'8px',height:'8px',borderRadius:'2px',background:it.color||SERIES[i%SERIES.length],flexShrink:0}"/>
|
|
44
|
+
<span :style="{fontFamily:'var(--font-sans)',fontSize:'var(--text-xs)',color:'var(--text-secondary)',textDecoration:hidden.includes(it.label)?'line-through':'none'}">{{ it.label }}</span>
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Circular Sankey — many-to-many, cyclical flows. matrix[i][j] = flow from i to j. */
|
|
3
|
+
export interface ChordChartProps {
|
|
4
|
+
labels?: string[];
|
|
5
|
+
matrix?: number[][];
|
|
6
|
+
size?: number;
|
|
7
|
+
/** Arc ring thickness */ thickness?: number;
|
|
8
|
+
/** Degrees between arcs */ gap?: number;
|
|
9
|
+
valueFormat?: (v: number) => string;
|
|
10
|
+
interactive?: boolean;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
export declare function ChordChart(props: ChordChartProps): JSX.Element;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, chord, chordRibbon, arcPath, polar, seriesColor } from "./chart-geometry.js";
|
|
3
|
+
import { ChartTooltip, wrapStyle } from "./ChartKit.jsx";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Chord diagram — the circular Sankey. A square `matrix` of flows between `labels`
|
|
7
|
+
* (matrix[i][j] = i → j). Use it when flows are many-to-many and cyclical, where a
|
|
8
|
+
* left-to-right Sankey would cross itself. Hovering an arc isolates that node's flows.
|
|
9
|
+
*/
|
|
10
|
+
export function ChordChart({ labels = [], matrix = [], size = 380, thickness = 14, gap = 2, valueFormat = fmtNum, interactive = true, style }) {
|
|
11
|
+
const [hover, setHover] = React.useState(null);
|
|
12
|
+
const cx = size / 2, cy = size / 2;
|
|
13
|
+
const rO = size / 2 - 58, rI = rO - thickness;
|
|
14
|
+
const { arcs, ribbons, totals, grand } = React.useMemo(() => chord(matrix, gap), [matrix, gap]);
|
|
15
|
+
const colorOf = (i) => seriesColor(i);
|
|
16
|
+
const lit = (r) => hover == null ? 0.42 : hover.kind === "ribbon" ? (hover.k === r.k ? 0.8 : 0.08) : (r.i === hover.i || r.j === hover.i ? 0.72 : 0.07);
|
|
17
|
+
const keyed = ribbons.map((r, k) => ({ ...r, k }));
|
|
18
|
+
const h = hover;
|
|
19
|
+
const tipRows = () => {
|
|
20
|
+
if (h.kind === "ribbon") {
|
|
21
|
+
const r = keyed[h.k];
|
|
22
|
+
return { title: labels[r.i] + " → " + labels[r.j], rows: [{ label: "flow", value: valueFormat(r.value), color: colorOf(r.i) }, { label: "share", value: ((r.value / grand) * 100).toFixed(1) + "%" }] };
|
|
23
|
+
}
|
|
24
|
+
const out = matrix[h.i].reduce((s, v, j) => s + (j === h.i ? 0 : v), 0);
|
|
25
|
+
const inc = matrix.reduce((s, row, j) => s + (j === h.i ? 0 : row[h.i]), 0);
|
|
26
|
+
return { title: labels[h.i], rows: [{ label: "outgoing", value: valueFormat(out), color: colorOf(h.i) }, { label: "incoming", value: valueFormat(inc) }, { label: "total", value: valueFormat(totals[h.i]) }] };
|
|
27
|
+
};
|
|
28
|
+
const tipAt = h ? polar(cx, cy, rI * 0.55, h.kind === "ribbon" ? (keyed[h.k].a0 + keyed[h.k].b1) / 2 : (arcs[h.i].a0 + arcs[h.i].a1) / 2) : null;
|
|
29
|
+
return (
|
|
30
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
31
|
+
<svg width={size} height={size} role="img">
|
|
32
|
+
{keyed.map((r) => (
|
|
33
|
+
<path key={r.k} d={chordRibbon(cx, cy, rI - 1, r.a0, r.a1, r.b0, r.b1)} fill={colorOf(r.i)} opacity={lit(r)}
|
|
34
|
+
onMouseEnter={interactive ? () => setHover({ kind: "ribbon", k: r.k }) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
35
|
+
style={{ transition: "opacity var(--dur-fast) var(--ease-out)" }} />
|
|
36
|
+
))}
|
|
37
|
+
{arcs.map((a) => {
|
|
38
|
+
const mid = (a.a0 + a.a1) / 2;
|
|
39
|
+
const [lx, ly] = polar(cx, cy, rO + 12, mid);
|
|
40
|
+
const on = hover && hover.kind === "node" && hover.i === a.i;
|
|
41
|
+
const anchor = Math.abs(lx - cx) < 10 ? "middle" : lx > cx ? "start" : "end";
|
|
42
|
+
return (
|
|
43
|
+
<g key={a.i} onMouseEnter={interactive ? () => setHover({ kind: "node", i: a.i }) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
44
|
+
style={{ cursor: interactive ? "default" : undefined }}>
|
|
45
|
+
<path d={arcPath(cx, cy, rO, rI, a.a0, a.a1)} fill={colorOf(a.i)} opacity={hover == null || on ? 1 : 0.4} style={{ transition: "opacity var(--dur-fast) var(--ease-out)" }} />
|
|
46
|
+
<text x={lx} y={ly + 3} textAnchor={anchor} style={{ fontFamily: "var(--font-sans)", fontSize: 10.5, fill: on ? "var(--text-heading)" : "var(--text-secondary)" }}>{labels[a.i]}</text>
|
|
47
|
+
<text x={lx} y={ly + 14} textAnchor={anchor} style={{ fontFamily: "var(--font-mono)", fontSize: 9, fill: "var(--chart-label)" }}>{valueFormat(a.value)}</text>
|
|
48
|
+
</g>
|
|
49
|
+
);
|
|
50
|
+
})}
|
|
51
|
+
</svg>
|
|
52
|
+
{h && interactive ? <ChartTooltip x={tipAt[0]} y={tipAt[1]} flip={tipAt[0] > size * 0.55} {...tipRows()} /> : null}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, chord, chordRibbon, arcPath, polar, SERIES } from './chart-geometry.js'
|
|
4
|
+
// Circular Sankey — many-to-many, cyclical flows. matrix[i][j] = flow from i to j.
|
|
5
|
+
const props = defineProps({ labels:{type:Array,default:()=>[]}, matrix:{type:Array,default:()=>[]}, size:{type:Number,default:380}, thickness:{type:Number,default:14}, gap:{type:Number,default:2}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })
|
|
6
|
+
const hover = ref(null)
|
|
7
|
+
const g = computed(() => {
|
|
8
|
+
const cx = props.size/2, cy = props.size/2
|
|
9
|
+
const rO = props.size/2-58, rI = rO-props.thickness
|
|
10
|
+
const { arcs, ribbons, totals, grand } = chord(props.matrix, props.gap)
|
|
11
|
+
return { cx, cy, rO, rI, grand, totals,
|
|
12
|
+
arcs: arcs.map(a=>{
|
|
13
|
+
const mid=(a.a0+a.a1)/2, l=polar(cx,cy,rO+12,mid)
|
|
14
|
+
return Object.assign({}, a, { mid, d: arcPath(cx,cy,rO,rI,a.a0,a.a1), color: SERIES[a.i%SERIES.length],
|
|
15
|
+
lx:l[0], ly:l[1], anchor: Math.abs(l[0]-cx)<10 ? 'middle' : (l[0]>cx?'start':'end') })
|
|
16
|
+
}),
|
|
17
|
+
ribbons: ribbons.map((r,k)=>Object.assign({}, r, { k, d: chordRibbon(cx,cy,rI-1,r.a0,r.a1,r.b0,r.b1), color: SERIES[r.i%SERIES.length] })) }
|
|
18
|
+
})
|
|
19
|
+
function op(r){
|
|
20
|
+
const h = hover.value
|
|
21
|
+
if (!h) return 0.42
|
|
22
|
+
return h.kind==='ribbon' ? (h.k===r.k?0.8:0.08) : ((r.i===h.i||r.j===h.i)?0.72:0.07)
|
|
23
|
+
}
|
|
24
|
+
const tip = computed(()=>{
|
|
25
|
+
const h = hover.value
|
|
26
|
+
if (!h) return null
|
|
27
|
+
if (h.kind==='ribbon') {
|
|
28
|
+
const r = g.value.ribbons[h.k], p = polar(g.value.cx,g.value.cy,g.value.rI*0.55,(r.a0+r.b1)/2)
|
|
29
|
+
return { x:p[0], y:p[1], title: props.labels[r.i]+' → '+props.labels[r.j], value: props.valueFormat(r.value)+' · '+((r.value/g.value.grand)*100).toFixed(1)+'%' }
|
|
30
|
+
}
|
|
31
|
+
const a = g.value.arcs[h.i], p = polar(g.value.cx,g.value.cy,g.value.rI*0.55,a.mid)
|
|
32
|
+
const out = props.matrix[h.i].reduce((s,v,j)=>s+(j===h.i?0:v),0)
|
|
33
|
+
const inc = props.matrix.reduce((s,row,j)=>s+(j===h.i?0:row[h.i]),0)
|
|
34
|
+
return { x:p[0], y:p[1], title: props.labels[h.i], value: 'out '+props.valueFormat(out)+' · in '+props.valueFormat(inc) }
|
|
35
|
+
})
|
|
36
|
+
</script>
|
|
37
|
+
<template>
|
|
38
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
39
|
+
<svg :width="size" :height="size" role="img">
|
|
40
|
+
<path v-for="r in g.ribbons" :key="r.k" :d="r.d" :fill="r.color" :opacity="op(r)"
|
|
41
|
+
@mouseenter="interactive && (hover={kind:'ribbon',k:r.k})" @mouseleave="interactive && (hover=null)"
|
|
42
|
+
style="transition:opacity var(--dur-fast) var(--ease-out)"/>
|
|
43
|
+
<g v-for="a in g.arcs" :key="a.i" @mouseenter="interactive && (hover={kind:'node',i:a.i})" @mouseleave="interactive && (hover=null)">
|
|
44
|
+
<path :d="a.d" :fill="a.color" :opacity="hover==null||(hover.kind==='node'&&hover.i===a.i)?1:0.4" style="transition:opacity var(--dur-fast) var(--ease-out)"/>
|
|
45
|
+
<text :x="a.lx" :y="a.ly+3" :text-anchor="a.anchor" style="font-family:var(--font-sans);font-size:10.5px;fill:var(--text-secondary)">{{ labels[a.i] }}</text>
|
|
46
|
+
<text :x="a.lx" :y="a.ly+14" :text-anchor="a.anchor" style="font-family:var(--font-mono);font-size:9px;fill:var(--chart-label)">{{ valueFormat(a.value) }}</text>
|
|
47
|
+
</g>
|
|
48
|
+
</svg>
|
|
49
|
+
<div v-if="tip && interactive" :style="{position:'absolute',left:tip.x+'px',top:tip.y+'px',transform:tip.x>size*0.55?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}">
|
|
50
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px">{{ tip.title }}</div>
|
|
51
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">{{ tip.value }}</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|