@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,115 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, SERIES } from './chart-geometry.js'
|
|
4
|
+
import { shade } from './chart-3d.js'
|
|
5
|
+
/**
|
|
6
|
+
* 3D pie / donut — extruded slices. Drag horizontally to spin in place (tilt stays fixed:
|
|
7
|
+
* free orbit only distorts an extruded pie). stairs extrudes each slice to its own height
|
|
8
|
+
* by value — the stepped presentation pie. Hover lifts a slice and pins a tooltip.
|
|
9
|
+
*/
|
|
10
|
+
const props = defineProps({ segments:{type:Array,default:()=>[]}, size:{type:Number,default:300}, thickness:{type:Number,default:26}, innerRatio:{type:Number,default:0}, tilt:{type:Number,default:0.45}, explode:{type:Number,default:6}, stairs:Boolean, spin:{type:Number,default:0}, valueFormat:{type:Function,default:fmtNum}, legend:{type:Boolean,default:true}, interactive:{type:Boolean,default:true}, style:Object })
|
|
11
|
+
const hover = ref(null)
|
|
12
|
+
const spinNow = ref(props.spin)
|
|
13
|
+
const drag = ref(null)
|
|
14
|
+
function down(e){ drag.value = { x:e.clientX, s:spinNow.value }; e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId) }
|
|
15
|
+
function move(e){ if (drag.value) spinNow.value = drag.value.s + (e.clientX-drag.value.x)*0.5 }
|
|
16
|
+
function up(){ drag.value = null }
|
|
17
|
+
const g = computed(() => {
|
|
18
|
+
const total = props.segments.reduce((s2,x)=>s2+x.value,0) || 1
|
|
19
|
+
const vmax = Math.max(...props.segments.map(s2=>s2.value), 1)
|
|
20
|
+
const maxTh = props.stairs ? Math.round(props.thickness*2.1) : props.thickness
|
|
21
|
+
const cx = props.size/2, cy = props.size/2 - maxTh/2 + 6
|
|
22
|
+
const rO = props.size/2-10, rI = rO*props.innerRatio
|
|
23
|
+
const pt = (r,deg,dz=0) => [cx + r*Math.cos(((deg-90)*Math.PI)/180), cy + r*Math.sin(((deg-90)*Math.PI)/180)*props.tilt + dz]
|
|
24
|
+
const arcPts = (r,a0,a1,dz) => { const steps = Math.max(2, Math.ceil(Math.abs(a1-a0)/6)); return [...Array(steps+1)].map((_,k)=>pt(r, a0+((a1-a0)*k)/steps, dz)) }
|
|
25
|
+
const path = pts => 'M' + pts.map(p=>p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' L') + ' Z'
|
|
26
|
+
let a = 0
|
|
27
|
+
const slices = props.segments.map((s2,i)=>{
|
|
28
|
+
const a0 = a + spinNow.value, a1 = a + (s2.value/total)*360 + spinNow.value; a += (s2.value/total)*360
|
|
29
|
+
const mid = ((((a0+a1)/2)%360)+360)%360
|
|
30
|
+
// Stairs: bottoms share one plane, each top rises by value.
|
|
31
|
+
const h = props.stairs ? maxTh*(0.35+0.65*(s2.value/vmax)) : props.thickness
|
|
32
|
+
return { s:s2, i, a0, a1, mid, h, color: s2.color || SERIES[i%SERIES.length] }
|
|
33
|
+
})
|
|
34
|
+
const off = sl => (hover.value===sl.i && props.interactive)
|
|
35
|
+
? [Math.cos(((sl.mid-90)*Math.PI)/180)*props.explode, Math.sin(((sl.mid-90)*Math.PI)/180)*props.explode*props.tilt]
|
|
36
|
+
: [0,0]
|
|
37
|
+
const nearness = sl => -Math.cos((sl.mid*Math.PI)/180)
|
|
38
|
+
const sorted = [...slices].sort((p,q)=>nearness(p)-nearness(q))
|
|
39
|
+
// Bottoms first: any sliver the walls miss reads as dark interior, never a hole.
|
|
40
|
+
const bottoms = slices.map(sl=>{
|
|
41
|
+
const [dx,dy] = off(sl)
|
|
42
|
+
const d = rI>0
|
|
43
|
+
? path([...arcPts(rO,sl.a0,sl.a1,maxTh), ...arcPts(rI,sl.a1,sl.a0,maxTh)].map(([x,y])=>[x+dx,y+dy]))
|
|
44
|
+
: path([[cx+dx, cy+dy+maxTh], ...arcPts(rO,sl.a0,sl.a1,maxTh).map(([x,y])=>[x+dx,y+dy])])
|
|
45
|
+
return { key:'b'+sl.i, d, fill: shade(sl.color,-0.45), i:sl.i }
|
|
46
|
+
})
|
|
47
|
+
const innerWalls = rI ? sorted.map(sl=>{
|
|
48
|
+
const [dx,dy] = off(sl)
|
|
49
|
+
const top = arcPts(rI,sl.a0,sl.a1,maxTh-sl.h).map(([x,y])=>[x+dx,y+dy])
|
|
50
|
+
const bot = arcPts(rI,sl.a1,sl.a0,maxTh).map(([x,y])=>[x+dx,y+dy])
|
|
51
|
+
return { key:'iw'+sl.i, d: path([...top,...bot]), fill: shade(sl.color,-0.38), i:sl.i }
|
|
52
|
+
}) : []
|
|
53
|
+
/* Each slice is a closed solid: two radial faces plus its full outer wall, painted
|
|
54
|
+
far-to-near, so the side of the pie is never open at any spin angle. */
|
|
55
|
+
const solids = sorted.map(sl=>{
|
|
56
|
+
const [dx,dy] = off(sl)
|
|
57
|
+
const zTop = maxTh - sl.h
|
|
58
|
+
const radial = deg => path([pt(rI||0.01,deg,zTop), pt(rO,deg,zTop), pt(rO,deg,maxTh), pt(rI||0.01,deg,maxTh)].map(([x,y])=>[x+dx,y+dy]))
|
|
59
|
+
const top = arcPts(rO,sl.a0,sl.a1,zTop).map(([x,y])=>[x+dx,y+dy])
|
|
60
|
+
const bot = arcPts(rO,sl.a0,sl.a1,maxTh).map(([x,y])=>[x+dx,y+dy])
|
|
61
|
+
return { key:'sd'+sl.i, i:sl.i, r0: radial(sl.a0), r1: radial(sl.a1),
|
|
62
|
+
wall: path([...top,...bot.reverse()]), side: shade(sl.color,-0.18), wallFill: shade(sl.color,-0.28) }
|
|
63
|
+
})
|
|
64
|
+
const tops = sorted.map(sl=>{
|
|
65
|
+
const [dx,dy] = off(sl)
|
|
66
|
+
const zTop = maxTh - sl.h
|
|
67
|
+
const d = rI>0
|
|
68
|
+
? path([...arcPts(rO,sl.a0,sl.a1,zTop), ...arcPts(rI,sl.a1,sl.a0,zTop)].map(([x,y])=>[x+dx,y+dy]))
|
|
69
|
+
: path([[cx+dx, cy+dy+zTop], ...arcPts(rO,sl.a0,sl.a1,zTop).map(([x,y])=>[x+dx,y+dy])])
|
|
70
|
+
return { key:'t'+sl.i, d, fill: sl.color, i:sl.i }
|
|
71
|
+
})
|
|
72
|
+
const h = hover.value!=null ? slices.find(sl=>sl.i===hover.value) : null
|
|
73
|
+
return { total, maxTh, slices, bottoms, innerWalls, solids, tops,
|
|
74
|
+
tip: h ? { p: pt(rO*0.8, (h.a0+h.a1)/2, (maxTh-h.h)/2), s:h.s, color:h.color } : null }
|
|
75
|
+
})
|
|
76
|
+
function dim(i){ return hover.value==null || hover.value===i ? 1 : 0.55 }
|
|
77
|
+
function enter(i){ if (props.interactive && !drag.value) hover.value = i }
|
|
78
|
+
</script>
|
|
79
|
+
<template>
|
|
80
|
+
<div :style="{display:'inline-block',lineHeight:0,...style}">
|
|
81
|
+
<div style="display:flex;align-items:center;gap:16px">
|
|
82
|
+
<div style="position:relative">
|
|
83
|
+
<svg :width="size" :height="size*0.72+g.maxTh" @pointerdown="down" @pointermove="move" @pointerup="up" @pointerleave="up"
|
|
84
|
+
role="img" :style="{cursor:interactive?'grab':'default',touchAction:'none'}">
|
|
85
|
+
<path v-for="b in g.bottoms" :key="b.key" :d="b.d" :fill="b.fill" :opacity="dim(b.i)"
|
|
86
|
+
@mouseenter="enter(b.i)" @mouseleave="interactive && (hover=null)"/>
|
|
87
|
+
<path v-for="w in g.innerWalls" :key="w.key" :d="w.d" :fill="w.fill" :opacity="dim(w.i)"
|
|
88
|
+
@mouseenter="enter(w.i)" @mouseleave="interactive && (hover=null)"/>
|
|
89
|
+
<g v-for="sd in g.solids" :key="sd.key" :opacity="dim(sd.i)"
|
|
90
|
+
@mouseenter="enter(sd.i)" @mouseleave="interactive && (hover=null)"
|
|
91
|
+
style="transition:opacity var(--dur-fast) var(--ease-out)">
|
|
92
|
+
<path :d="sd.r0" :fill="sd.side"/>
|
|
93
|
+
<path :d="sd.r1" :fill="sd.side"/>
|
|
94
|
+
<path :d="sd.wall" :fill="sd.wallFill"/>
|
|
95
|
+
</g>
|
|
96
|
+
<path v-for="t in g.tops" :key="t.key" :d="t.d" :fill="t.fill" stroke="var(--bg-surface)" stroke-width="1" :opacity="dim(t.i)"
|
|
97
|
+
@mouseenter="enter(t.i)" @mouseleave="interactive && (hover=null)"/>
|
|
98
|
+
</svg>
|
|
99
|
+
<div v-if="g.tip && interactive" :style="{position:'absolute',left:g.tip.p[0]+'px',top:g.tip.p[1]+'px',transform:g.tip.p[0]>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'}">
|
|
100
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px">{{ g.tip.s.label }}</div>
|
|
101
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">{{ valueFormat(g.tip.s.value) }} · {{ ((g.tip.s.value/g.total)*100).toFixed(1) }}%</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div v-if="legend" style="display:flex;flex-direction:column;gap:6px;line-height:normal">
|
|
105
|
+
<span v-for="sl in g.slices" :key="sl.s.label" @mouseenter="interactive && (hover=sl.i)" @mouseleave="interactive && (hover=null)"
|
|
106
|
+
:style="{display:'inline-flex',alignItems:'center',gap:'7px',opacity:dim(sl.i)}">
|
|
107
|
+
<span :style="{width:'8px',height:'8px',borderRadius:'2px',background:sl.color,flexShrink:0}"/>
|
|
108
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)">{{ sl.s.label }}</span>
|
|
109
|
+
<span style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);margin-left:6px">
|
|
110
|
+
{{ hover===sl.i ? valueFormat(sl.s.value) : ((sl.s.value/g.total)*100).toFixed(0)+'%' }}</span>
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface RadarAxis { label: string; /** Axis ceiling; auto-scaled when omitted */ max?: number; }
|
|
3
|
+
export interface RadarSeries { name: string; values: number[]; color?: string; }
|
|
4
|
+
/** Spider chart — a few entities across the same 3–10 measures. Hovering a spoke reports every series. */
|
|
5
|
+
export interface RadarChartProps {
|
|
6
|
+
axes?: RadarAxis[];
|
|
7
|
+
series?: RadarSeries[];
|
|
8
|
+
size?: number;
|
|
9
|
+
/** Concentric grid rings */ levels?: number;
|
|
10
|
+
legend?: boolean; interactive?: boolean;
|
|
11
|
+
valueFormat?: (v: number) => string;
|
|
12
|
+
style?: React.CSSProperties;
|
|
13
|
+
}
|
|
14
|
+
export declare function RadarChart(props: RadarChartProps): JSX.Element;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, niceTicks, polar, seriesColor } from "./chart-geometry.js";
|
|
3
|
+
import { ChartLegend, ChartTooltip, wrapStyle } from "./ChartKit.jsx";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Radar (spider) chart — compare a few entities across the same 3–10 measures.
|
|
7
|
+
* axes: [{label, max?}]; series: [{name, values, color?}] aligned to axes.
|
|
8
|
+
* Hovering an axis spoke highlights it and reports every series at that measure.
|
|
9
|
+
*/
|
|
10
|
+
export function RadarChart({ axes = [], series = [], size = 260, levels = 4, legend = true, interactive = true, valueFormat = fmtNum, style }) {
|
|
11
|
+
const [hover, setHover] = React.useState(null);
|
|
12
|
+
const [hidden, setHidden] = React.useState([]);
|
|
13
|
+
const shown = series.filter((s) => !hidden.includes(s.name));
|
|
14
|
+
/* Axis labels sit outside the outer ring, so the plot radius has to leave room for the
|
|
15
|
+
longest one — otherwise "Accuracy" gets clipped at the SVG edge. */
|
|
16
|
+
const cx = size / 2, cy = size / 2, r = size / 2 - 42;
|
|
17
|
+
const n = axes.length || 1;
|
|
18
|
+
const step = 360 / n;
|
|
19
|
+
const max = axes.map((a, i) => a.max != null ? a.max : niceTicks(0, Math.max(...series.map((s) => s.values[i] || 0)), 3).hi);
|
|
20
|
+
const at = (si, i) => {
|
|
21
|
+
const v = (shown[si].values[i] || 0) / (max[i] || 1);
|
|
22
|
+
return polar(cx, cy, Math.max(0, Math.min(1, v)) * r, i * step);
|
|
23
|
+
};
|
|
24
|
+
const colorOf = (s) => s.color || seriesColor(series.indexOf(s));
|
|
25
|
+
const move = (e) => {
|
|
26
|
+
if (!interactive) return;
|
|
27
|
+
const rc = e.currentTarget.getBoundingClientRect();
|
|
28
|
+
const dx = e.clientX - rc.left - cx, dy = e.clientY - rc.top - cy;
|
|
29
|
+
if (Math.hypot(dx, dy) > r + 22) return setHover(null);
|
|
30
|
+
let deg = (Math.atan2(dy, dx) * 180) / Math.PI + 90;
|
|
31
|
+
if (deg < 0) deg += 360;
|
|
32
|
+
setHover(Math.round(deg / step) % n);
|
|
33
|
+
};
|
|
34
|
+
const tip = hover != null ? polar(cx, cy, r * 0.72, hover * step) : null;
|
|
35
|
+
return (
|
|
36
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
37
|
+
<svg width={size} height={size} onMouseMove={move} onMouseLeave={() => setHover(null)} role="img" style={{ overflow: "visible" }}>
|
|
38
|
+
{[...Array(levels)].map((_, li) => {
|
|
39
|
+
const rr = (r * (li + 1)) / levels;
|
|
40
|
+
const pts = [...Array(n)].map((__, i) => polar(cx, cy, rr, i * step).map((v) => v.toFixed(1)).join(" ")).join(" L");
|
|
41
|
+
return <path key={li} d={"M" + pts + " Z"} fill="none" stroke="var(--chart-grid)" strokeWidth="1" />;
|
|
42
|
+
})}
|
|
43
|
+
{axes.map((a, i) => {
|
|
44
|
+
const [x, y] = polar(cx, cy, r, i * step);
|
|
45
|
+
const [lx, ly] = polar(cx, cy, r + 14, i * step);
|
|
46
|
+
/* An anchor="start" label grows rightward, so near-vertical spokes must center
|
|
47
|
+
and the right column must not sit so close to the edge that it clips. */
|
|
48
|
+
const anchor = Math.abs(lx - cx) < 12 ? "middle" : lx > cx ? "start" : "end";
|
|
49
|
+
const on = hover === i;
|
|
50
|
+
return (
|
|
51
|
+
<g key={a.label}>
|
|
52
|
+
<line x1={cx} y1={cy} x2={x} y2={y} stroke={on ? "var(--chart-guide)" : "var(--chart-axis)"} strokeWidth="1" />
|
|
53
|
+
<text x={lx} y={ly + 3} textAnchor={anchor}
|
|
54
|
+
style={{ fontFamily: "var(--font-sans)", fontSize: 10, fill: on ? "var(--text-heading)" : "var(--chart-label)" }}>{a.label}</text>
|
|
55
|
+
</g>
|
|
56
|
+
);
|
|
57
|
+
})}
|
|
58
|
+
{shown.map((s, si) => {
|
|
59
|
+
const pts = axes.map((_, i) => at(si, i));
|
|
60
|
+
const d = "M" + pts.map((p) => p[0].toFixed(1) + " " + p[1].toFixed(1)).join(" L") + " Z";
|
|
61
|
+
return (
|
|
62
|
+
<g key={s.name}>
|
|
63
|
+
<path d={d} fill={colorOf(s)} fillOpacity="0.14" stroke={colorOf(s)} strokeWidth="1.5" strokeLinejoin="round" />
|
|
64
|
+
{pts.map((p, i) => <circle key={i} cx={p[0]} cy={p[1]} r={hover === i ? 3.4 : 2.2} fill={colorOf(s)} stroke="var(--bg-surface)" strokeWidth={hover === i ? 1.5 : 0} />)}
|
|
65
|
+
</g>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
68
|
+
</svg>
|
|
69
|
+
{hover != null && interactive ? (
|
|
70
|
+
<ChartTooltip x={tip[0]} y={tip[1]} flip={tip[0] > size * 0.58} title={axes[hover].label}
|
|
71
|
+
rows={shown.map((s) => ({ label: s.name, value: valueFormat(s.values[hover]), color: colorOf(s) }))} />
|
|
72
|
+
) : null}
|
|
73
|
+
{legend && series.length > 1 ? (
|
|
74
|
+
<ChartLegend style={{ paddingTop: 6 }} items={series.map((s) => ({ label: s.name, color: colorOf(s) }))} hidden={hidden}
|
|
75
|
+
onToggle={(l) => setHidden(hidden.includes(l) ? hidden.filter((x) => x !== l) : hidden.length + 1 < series.length ? [...hidden, l] : hidden)} />
|
|
76
|
+
) : null}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, niceTicks, polar, SERIES } from './chart-geometry.js'
|
|
4
|
+
// Spider chart — a few entities across the same 3–10 measures. Hovering a spoke reports all series.
|
|
5
|
+
const props = defineProps({ axes:{type:Array,default:()=>[]}, series:{type:Array,default:()=>[]}, size:{type:Number,default:260}, levels:{type:Number,default:4}, legend:{type:Boolean,default:true}, interactive:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, style:Object })
|
|
6
|
+
const hover = ref(null); const hidden = ref([])
|
|
7
|
+
const g = computed(() => {
|
|
8
|
+
const cx = props.size/2, cy = props.size/2, r = props.size/2-42
|
|
9
|
+
const n = props.axes.length||1, step = 360/n
|
|
10
|
+
const shown = props.series.filter(s=>!hidden.value.includes(s.name))
|
|
11
|
+
const max = props.axes.map((a,i)=> a.max!=null ? a.max : niceTicks(0, Math.max(...props.series.map(s=>s.values[i]||0)), 3).hi)
|
|
12
|
+
const rings = [...Array(props.levels)].map((_,li)=>{
|
|
13
|
+
const rr = (r*(li+1))/props.levels
|
|
14
|
+
return 'M' + [...Array(n)].map((__,i)=>polar(cx,cy,rr,i*step).map(v=>v.toFixed(1)).join(' ')).join(' L') + ' Z'
|
|
15
|
+
})
|
|
16
|
+
const spokes = props.axes.map((a,i)=>{
|
|
17
|
+
const p = polar(cx,cy,r,i*step), l = polar(cx,cy,r+14,i*step)
|
|
18
|
+
// An anchor="start" label grows rightward, so near-vertical spokes must center.
|
|
19
|
+
return { label:a.label, x:p[0], y:p[1], lx:l[0], ly:l[1],
|
|
20
|
+
anchor: Math.abs(l[0]-cx)<12 ? 'middle' : (l[0]>cx ? 'start' : 'end') }
|
|
21
|
+
})
|
|
22
|
+
const shapes = shown.map(s=>{
|
|
23
|
+
const pts = props.axes.map((_,i)=>polar(cx,cy,Math.max(0,Math.min(1,(s.values[i]||0)/(max[i]||1)))*r, i*step))
|
|
24
|
+
return { s, color: s.color||SERIES[props.series.indexOf(s)%SERIES.length], pts,
|
|
25
|
+
d:'M'+pts.map(p=>p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' L')+' Z' }
|
|
26
|
+
})
|
|
27
|
+
return { cx, cy, r, n, step, rings, spokes, shapes, shown }
|
|
28
|
+
})
|
|
29
|
+
function move(e){
|
|
30
|
+
if(!props.interactive) return
|
|
31
|
+
const rc = e.currentTarget.getBoundingClientRect()
|
|
32
|
+
const dx = e.clientX-rc.left-g.value.cx, dy = e.clientY-rc.top-g.value.cy
|
|
33
|
+
if (Math.hypot(dx,dy) > g.value.r+22) { hover.value=null; return }
|
|
34
|
+
let deg = (Math.atan2(dy,dx)*180)/Math.PI + 90
|
|
35
|
+
if (deg<0) deg += 360
|
|
36
|
+
hover.value = Math.round(deg/g.value.step) % g.value.n
|
|
37
|
+
}
|
|
38
|
+
const tip = computed(()=> hover.value==null ? null : polar(g.value.cx, g.value.cy, g.value.r*0.72, hover.value*g.value.step))
|
|
39
|
+
function toggle(name){
|
|
40
|
+
if (hidden.value.includes(name)) hidden.value = hidden.value.filter(x=>x!==name)
|
|
41
|
+
else if (hidden.value.length+1 < props.series.length) hidden.value = [...hidden.value, name]
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
<template>
|
|
45
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
46
|
+
<svg :width="size" :height="size" @mousemove="move" @mouseleave="hover=null" role="img" style="overflow:visible">
|
|
47
|
+
<path v-for="(d,i) in g.rings" :key="'r'+i" :d="d" fill="none" stroke="var(--chart-grid)" stroke-width="1"/>
|
|
48
|
+
<g v-for="(sp,i) in g.spokes" :key="sp.label">
|
|
49
|
+
<line :x1="g.cx" :y1="g.cy" :x2="sp.x" :y2="sp.y" :stroke="hover===i?'var(--chart-guide)':'var(--chart-axis)'" stroke-width="1"/>
|
|
50
|
+
<text :x="sp.lx" :y="sp.ly+3" :text-anchor="sp.anchor" :style="{fontFamily:'var(--font-sans)',fontSize:'10px',fill:hover===i?'var(--text-heading)':'var(--chart-label)'}">{{ sp.label }}</text>
|
|
51
|
+
</g>
|
|
52
|
+
<g v-for="sh in g.shapes" :key="sh.s.name">
|
|
53
|
+
<path :d="sh.d" :fill="sh.color" fill-opacity="0.14" :stroke="sh.color" stroke-width="1.5" stroke-linejoin="round"/>
|
|
54
|
+
<circle v-for="(p,i) in sh.pts" :key="i" :cx="p[0]" :cy="p[1]" :r="hover===i?3.4:2.2" :fill="sh.color" stroke="var(--bg-surface)" :stroke-width="hover===i?1.5:0"/>
|
|
55
|
+
</g>
|
|
56
|
+
</svg>
|
|
57
|
+
<div v-if="hover!=null && interactive" :style="{position:'absolute',left:tip[0]+'px',top:tip[1]+'px',transform:tip[0]>size*0.58?'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'}">
|
|
58
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px">{{ axes[hover].label }}</div>
|
|
59
|
+
<div v-for="sh in g.shapes" :key="sh.s.name" style="display:flex;align-items:center;gap:6px">
|
|
60
|
+
<span :style="{width:'7px',height:'7px',borderRadius:'2px',background:sh.color}"/>
|
|
61
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)">{{ sh.s.name }}</span>
|
|
62
|
+
<span style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);margin-left:auto;padding-left:10px">{{ valueFormat(sh.s.values[hover]) }}</span>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<div v-if="legend && series.length>1" style="display:flex;flex-wrap:wrap;gap:4px 14px;padding-top:6px;line-height:normal">
|
|
66
|
+
<span v-for="(s,i) in series" :key="s.name" role="button" tabindex="0" @click="toggle(s.name)"
|
|
67
|
+
:style="{display:'inline-flex',alignItems:'center',gap:'6px',cursor:'pointer',opacity:hidden.includes(s.name)?0.4:1}">
|
|
68
|
+
<span :style="{width:'8px',height:'8px',borderRadius:'2px',background:s.color||SERIES[i%SERIES.length]}"/>
|
|
69
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)">{{ s.name }}</span>
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface Ribbon3DSeries { name: string; values: number[]; color?: string; }
|
|
3
|
+
/** Each series as an extruded band at its own depth row (the classic 3D area); drag to orbit. */
|
|
4
|
+
export interface Ribbon3DProps {
|
|
5
|
+
series?: Ribbon3DSeries[];
|
|
6
|
+
xLabels?: string[];
|
|
7
|
+
width?: number; height?: number;
|
|
8
|
+
yaw?: number; pitch?: number;
|
|
9
|
+
/** Band thickness as a fraction of its row, 0–1 */ ribbon?: number;
|
|
10
|
+
interactive?: boolean;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
export declare function Ribbon3D(props: Ribbon3DProps): JSX.Element;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, seriesColor, niceTicks } from "./chart-geometry.js";
|
|
3
|
+
import { makeProj, facePath, faceDepth, shade, dragRotate } from "./chart-3d.js";
|
|
4
|
+
import { ChartTooltip, wrapStyle } from "./ChartKit.jsx";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 3D ribbon series — each series is an extruded band at its own depth row (the classic
|
|
8
|
+
* "3D area" presentation). Drag to orbit.
|
|
9
|
+
*/
|
|
10
|
+
export function Ribbon3D({ series = [], xLabels = [], width = 460, height = 300, yaw: yaw0 = 34, pitch: pitch0 = 30, ribbon = 0.55, interactive = true, valueFormat = fmtNum, style }) {
|
|
11
|
+
const [rot, setRot] = React.useState({ yaw: yaw0, pitch: pitch0 });
|
|
12
|
+
const [hover, setHover] = React.useState(null);
|
|
13
|
+
const n = Math.max(...series.map((s) => s.values.length), 2);
|
|
14
|
+
const hi = Math.max(...series.flatMap((s) => s.values)) || 1;
|
|
15
|
+
const m = series.length;
|
|
16
|
+
const scale = Math.min(width, height) * 0.42;
|
|
17
|
+
const P = makeProj(width / 2, height / 2 + height * 0.05, scale, rot.yaw, rot.pitch);
|
|
18
|
+
const base = [P(-0.5, -0.45, -0.5), P(0.5, -0.45, -0.5), P(0.5, -0.45, 0.5), P(-0.5, -0.45, 0.5)];
|
|
19
|
+
/* Vertical value axis with real ticks at the front-left corner. */
|
|
20
|
+
const vt = niceTicks(0, hi, 3);
|
|
21
|
+
const vTicks = vt.ticks.filter((t) => t <= hi * 1.001).map((t) => ({ p: P(-0.5, (t / hi - 0.5) * 0.9, 0.5), v: fmtNum(t) }));
|
|
22
|
+
const vAxis = [P(-0.5, -0.45, 0.5), P(-0.5, 0.45, 0.5)];
|
|
23
|
+
const rows = series.map((s, si) => {
|
|
24
|
+
const z0 = si / m - 0.5 + (1 / m) * (0.5 - ribbon / 2), z1 = z0 + (ribbon / m);
|
|
25
|
+
const y = (i) => ((s.values[i] || 0) / hi - 0.5) * 0.9;
|
|
26
|
+
const quads = [];
|
|
27
|
+
for (let i = 0; i < n - 1; i++) {
|
|
28
|
+
const x0 = i / (n - 1) - 0.5, x1 = (i + 1) / (n - 1) - 0.5;
|
|
29
|
+
const top = [P(x0, y(i), z0), P(x1, y(i + 1), z0), P(x1, y(i + 1), z1), P(x0, y(i), z1)];
|
|
30
|
+
const front = [P(x0, -0.45, z1), P(x1, -0.45, z1), P(x1, y(i + 1), z1), P(x0, y(i), z1)];
|
|
31
|
+
const back = [P(x0, -0.45, z0), P(x1, -0.45, z0), P(x1, y(i + 1), z0), P(x0, y(i), z0)];
|
|
32
|
+
quads.push({ pts: back, kind: "back" }, { pts: top, kind: "top" }, { pts: front, kind: "front" });
|
|
33
|
+
}
|
|
34
|
+
const end = [P(0.5, -0.45, z0), P(0.5, -0.45, z1), P(0.5, y(n - 1), z1), P(0.5, y(n - 1), z0)];
|
|
35
|
+
const start = [P(-0.5, -0.45, z0), P(-0.5, -0.45, z1), P(-0.5, y(0), z1), P(-0.5, y(0), z0)];
|
|
36
|
+
quads.push({ pts: end, kind: "end" }, { pts: start, kind: "end" });
|
|
37
|
+
quads.forEach((q) => { q.depth = faceDepth(q.pts); });
|
|
38
|
+
const rowDepth = quads.reduce((t, q) => t + q.depth, 0) / quads.length;
|
|
39
|
+
return { s, si, quads, rowDepth, color: s.color || seriesColor(si) };
|
|
40
|
+
}).sort((a, b) => b.rowDepth - a.rowDepth);
|
|
41
|
+
const handlers = interactive ? dragRotate(() => rot, setRot) : {};
|
|
42
|
+
return (
|
|
43
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
44
|
+
<svg width={width} height={height} {...handlers} role="img" style={{ cursor: interactive ? "grab" : "default", touchAction: "none" }}>
|
|
45
|
+
<path d={facePath(base)} fill="var(--bg-inset)" stroke="var(--chart-axis)" strokeWidth="1" />
|
|
46
|
+
<line x1={vAxis[0].x} y1={vAxis[0].y} x2={vAxis[1].x} y2={vAxis[1].y} stroke="var(--chart-guide)" strokeWidth="1" />
|
|
47
|
+
{vTicks.map((t, i) => (
|
|
48
|
+
<g key={"vt" + i}>
|
|
49
|
+
<line x1={t.p.x - 4} y1={t.p.y} x2={t.p.x} y2={t.p.y} stroke="var(--chart-guide)" strokeWidth="1" />
|
|
50
|
+
<text x={t.p.x - 7} y={t.p.y + 3} textAnchor="end" style={{ fontFamily: "var(--font-mono)", fontSize: 9, fill: "var(--chart-label)" }}>{t.v}</text>
|
|
51
|
+
</g>
|
|
52
|
+
))}
|
|
53
|
+
{xLabels.map((lb, i) => {
|
|
54
|
+
if (lb == null || (xLabels.length > 8 && i % Math.ceil(xLabels.length / 8) && i !== xLabels.length - 1)) return null;
|
|
55
|
+
/* Follow whichever base edge faces the viewer; outside it, painted before the bands. */
|
|
56
|
+
const zE = P(0, -0.45, 0.5).y >= P(0, -0.45, -0.5).y ? 0.62 : -0.62;
|
|
57
|
+
const p0 = P(i / (n - 1) - 0.5, -0.45, zE);
|
|
58
|
+
const ctr = P(0, -0.45, 0);
|
|
59
|
+
const dx = p0.x - ctr.x, dy = p0.y - ctr.y, len = Math.hypot(dx, dy) || 1;
|
|
60
|
+
return <text key={"x" + i} x={p0.x + (dx / len) * 14} y={p0.y + (dy / len) * 14 + 3} textAnchor="middle" style={{ fontFamily: "var(--font-sans)", fontSize: 10, fill: "var(--chart-label)" }}>{lb}</text>;
|
|
61
|
+
})}
|
|
62
|
+
{rows.map((r) => (
|
|
63
|
+
<g key={r.s.name} opacity={hover == null || hover === r.si ? 1 : 0.4}
|
|
64
|
+
onMouseEnter={interactive ? () => setHover(r.si) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
65
|
+
style={{ transition: "opacity var(--dur-fast) var(--ease-out)" }}>
|
|
66
|
+
{r.quads.sort((a, b) => b.depth - a.depth).map((q, qi) => (
|
|
67
|
+
<path key={qi} d={facePath(q.pts)} fill={shade(r.color, q.kind === "top" ? 0.14 : q.kind === "front" ? -0.12 : q.kind === "back" ? -0.2 : -0.28)} stroke="var(--bg-surface)" strokeWidth="0.4" />
|
|
68
|
+
))}
|
|
69
|
+
</g>
|
|
70
|
+
))}
|
|
71
|
+
</svg>
|
|
72
|
+
{hover != null && interactive ? (() => {
|
|
73
|
+
const r = rows.find((x) => x.si === hover);
|
|
74
|
+
const peak = Math.max(...r.s.values);
|
|
75
|
+
return <ChartTooltip x={width / 2} y={height * 0.28} flip={false} title={r.s.name}
|
|
76
|
+
rows={[{ label: "peak", value: valueFormat(peak), color: r.color }, { label: "latest", value: valueFormat(r.s.values[r.s.values.length - 1]) }]} />;
|
|
77
|
+
})() : null}
|
|
78
|
+
<div style={{ display: "flex", gap: 14, paddingTop: 6, lineHeight: "normal" }}>
|
|
79
|
+
{series.map((s, i) => (
|
|
80
|
+
<span key={s.name} onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
81
|
+
style={{ display: "inline-flex", alignItems: "center", gap: 6, opacity: hover == null || hover === i ? 1 : 0.45, transition: "var(--t-hover)" }}>
|
|
82
|
+
<span style={{ width: 8, height: 8, borderRadius: 2, background: s.color || seriesColor(i) }} />
|
|
83
|
+
<span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", color: "var(--text-secondary)" }}>{s.name}</span>
|
|
84
|
+
</span>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, SERIES, niceTicks } from './chart-geometry.js'
|
|
4
|
+
import { makeProj, facePath, faceDepth, shade } from './chart-3d.js'
|
|
5
|
+
// Each series as an extruded band at its own depth row (the classic 3D area); drag to orbit.
|
|
6
|
+
const props = defineProps({ series:{type:Array,default:()=>[]}, xLabels:{type:Array,default:()=>[]}, width:{type:Number,default:460}, height:{type:Number,default:300}, yaw:{type:Number,default:34}, pitch:{type:Number,default:30}, ribbon:{type:Number,default:0.55}, interactive:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, style:Object })
|
|
7
|
+
const rot = ref({ yaw: props.yaw, pitch: props.pitch })
|
|
8
|
+
const hover = ref(null)
|
|
9
|
+
function down(e){ e.currentTarget.__kbDrag = { x:e.clientX, y:e.clientY, ...rot.value }; e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId) }
|
|
10
|
+
function move(e){ const s = e.currentTarget.__kbDrag; if(!s) return; rot.value = { yaw: s.yaw+(e.clientX-s.x)*0.45, pitch: Math.max(8, Math.min(80, s.pitch+(e.clientY-s.y)*0.35)) } }
|
|
11
|
+
function up(e){ e.currentTarget.__kbDrag = null }
|
|
12
|
+
const SHADE = { top:0.14, front:-0.12, back:-0.2, end:-0.28 }
|
|
13
|
+
const g = computed(() => {
|
|
14
|
+
const n = Math.max(...props.series.map(s=>s.values.length), 2)
|
|
15
|
+
const hi = Math.max(...props.series.flatMap(s=>s.values)) || 1
|
|
16
|
+
const m = props.series.length
|
|
17
|
+
const scale = Math.min(props.width, props.height)*0.42
|
|
18
|
+
const P = makeProj(props.width/2, props.height/2 + props.height*0.05, scale, rot.value.yaw, rot.value.pitch)
|
|
19
|
+
const base = [P(-0.5,-0.45,-0.5), P(0.5,-0.45,-0.5), P(0.5,-0.45,0.5), P(-0.5,-0.45,0.5)]
|
|
20
|
+
const rows = props.series.map((s,si)=>{
|
|
21
|
+
const z0 = si/m-0.5+(1/m)*(0.5-props.ribbon/2), z1 = z0+(props.ribbon/m)
|
|
22
|
+
const y = i => ((s.values[i]||0)/hi-0.5)*0.9
|
|
23
|
+
const quads = []
|
|
24
|
+
for (let i=0;i<n-1;i++) {
|
|
25
|
+
const x0 = i/(n-1)-0.5, x1 = (i+1)/(n-1)-0.5
|
|
26
|
+
quads.push({ pts:[P(x0,-0.45,z0),P(x1,-0.45,z0),P(x1,y(i+1),z0),P(x0,y(i),z0)], kind:'back' })
|
|
27
|
+
quads.push({ pts:[P(x0,y(i),z0),P(x1,y(i+1),z0),P(x1,y(i+1),z1),P(x0,y(i),z1)], kind:'top' })
|
|
28
|
+
quads.push({ pts:[P(x0,-0.45,z1),P(x1,-0.45,z1),P(x1,y(i+1),z1),P(x0,y(i),z1)], kind:'front' })
|
|
29
|
+
}
|
|
30
|
+
quads.push({ pts:[P(0.5,-0.45,z0),P(0.5,-0.45,z1),P(0.5,y(n-1),z1),P(0.5,y(n-1),z0)], kind:'end' })
|
|
31
|
+
quads.push({ pts:[P(-0.5,-0.45,z0),P(-0.5,-0.45,z1),P(-0.5,y(0),z1),P(-0.5,y(0),z0)], kind:'end' })
|
|
32
|
+
quads.forEach(q=>{ q.depth = faceDepth(q.pts); q.d = facePath(q.pts) })
|
|
33
|
+
quads.sort((a,b)=>b.depth-a.depth)
|
|
34
|
+
return { s, si, quads, rowDepth: quads.reduce((t,q)=>t+q.depth,0)/quads.length, color: s.color || SERIES[si%SERIES.length] }
|
|
35
|
+
}).sort((a,b)=>b.rowDepth-a.rowDepth)
|
|
36
|
+
const vt = niceTicks(0, hi, 3)
|
|
37
|
+
const vTicks = vt.ticks.filter(t=>t<=hi*1.001).map(t=>({ p:P(-0.5,(t/hi-0.5)*0.9,0.5), v:fmtNum(t) }))
|
|
38
|
+
const zE = P(0,-0.45,0.5).y >= P(0,-0.45,-0.5).y ? 0.62 : -0.62
|
|
39
|
+
const ctr = P(0,-0.45,0)
|
|
40
|
+
const every = props.xLabels.length > 8 ? Math.ceil(props.xLabels.length/8) : 1
|
|
41
|
+
const labels = props.xLabels.map((lb,i)=>{
|
|
42
|
+
if (lb==null || (every>1 && i%every && i!==props.xLabels.length-1)) return null
|
|
43
|
+
const p0 = P(i/(n-1)-0.5, -0.45, zE)
|
|
44
|
+
const dx = p0.x-ctr.x, dy = p0.y-ctr.y, len = Math.hypot(dx,dy)||1
|
|
45
|
+
return { lb, x: p0.x+(dx/len)*14, y: p0.y+(dy/len)*14+3 }
|
|
46
|
+
}).filter(Boolean)
|
|
47
|
+
return { rows, basePath: facePath(base), vTicks, vAxis:[P(-0.5,-0.45,0.5), P(-0.5,0.45,0.5)], labels, SHADE }
|
|
48
|
+
})
|
|
49
|
+
</script>
|
|
50
|
+
<template>
|
|
51
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
52
|
+
<svg :width="width" :height="height" @pointerdown="down" @pointermove="move" @pointerup="up" @pointerleave="up"
|
|
53
|
+
role="img" :style="{cursor:interactive?'grab':'default',touchAction:'none'}">
|
|
54
|
+
<path :d="g.basePath" fill="var(--bg-inset)" stroke="var(--chart-axis)" stroke-width="1"/>
|
|
55
|
+
<line :x1="g.vAxis[0].x" :y1="g.vAxis[0].y" :x2="g.vAxis[1].x" :y2="g.vAxis[1].y" stroke="var(--chart-guide)" stroke-width="1"/>
|
|
56
|
+
<g v-for="(t,i) in g.vTicks" :key="'vt'+i">
|
|
57
|
+
<line :x1="t.p.x-4" :y1="t.p.y" :x2="t.p.x" :y2="t.p.y" stroke="var(--chart-guide)" stroke-width="1"/>
|
|
58
|
+
<text :x="t.p.x-7" :y="t.p.y+3" text-anchor="end" style="font-family:var(--font-mono);font-size:9px;fill:var(--chart-label)">{{ t.v }}</text>
|
|
59
|
+
</g>
|
|
60
|
+
<text v-for="(l,i) in g.labels" :key="'x'+i" :x="l.x" :y="l.y" text-anchor="middle" style="font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)">{{ l.lb }}</text>
|
|
61
|
+
<g v-for="r in g.rows" :key="r.s.name" :opacity="hover==null||hover===r.si?1:0.4"
|
|
62
|
+
@mouseenter="interactive && (hover=r.si)" @mouseleave="interactive && (hover=null)"
|
|
63
|
+
style="transition:opacity var(--dur-fast) var(--ease-out)">
|
|
64
|
+
<path v-for="(q,qi) in r.quads" :key="qi" :d="q.d" :fill="shade(r.color, g.SHADE[q.kind])" stroke="var(--bg-surface)" stroke-width="0.4"/>
|
|
65
|
+
</g>
|
|
66
|
+
</svg>
|
|
67
|
+
<div v-if="hover!=null && interactive" :style="{position:'absolute',left:(width/2)+'px',top:(height*0.28)+'px',transform:'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'}">
|
|
68
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px">{{ series[hover].name }}</div>
|
|
69
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">peak {{ valueFormat(Math.max(...series[hover].values)) }} · latest {{ valueFormat(series[hover].values[series[hover].values.length-1]) }}</div>
|
|
70
|
+
</div>
|
|
71
|
+
<div style="display:flex;gap:14px;padding-top:6px;line-height:normal">
|
|
72
|
+
<span v-for="(s,i) in series" :key="s.name" @mouseenter="interactive && (hover=i)" @mouseleave="interactive && (hover=null)"
|
|
73
|
+
:style="{display:'inline-flex',alignItems:'center',gap:'6px',opacity:hover==null||hover===i?1:0.45,transition:'var(--t-hover)'}">
|
|
74
|
+
<span :style="{width:'8px',height:'8px',borderRadius:'2px',background:s.color||SERIES[i%SERIES.length]}"/>
|
|
75
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)">{{ s.name }}</span>
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface SankeyNode { id: string; label: string; color?: string; }
|
|
3
|
+
export interface SankeyLink { source: string; target: string; value: number; }
|
|
4
|
+
/** Flow diagram; node columns are derived from the longest path, never positioned by hand. */
|
|
5
|
+
export interface SankeyChartProps {
|
|
6
|
+
nodes?: SankeyNode[];
|
|
7
|
+
links?: SankeyLink[];
|
|
8
|
+
width?: number; height?: number;
|
|
9
|
+
nodeWidth?: number; nodePadding?: number;
|
|
10
|
+
/** Right-hand room reserved for labels */ labelWidth?: number;
|
|
11
|
+
valueFormat?: (v: number) => string;
|
|
12
|
+
interactive?: boolean;
|
|
13
|
+
style?: React.CSSProperties;
|
|
14
|
+
}
|
|
15
|
+
export declare function SankeyChart(props: SankeyChartProps): JSX.Element;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, sankey, seriesColor } from "./chart-geometry.js";
|
|
3
|
+
import { ChartTooltip, wrapStyle } from "./ChartKit.jsx";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sankey flow diagram — budget allocation, headcount movement, data lineage.
|
|
7
|
+
* nodes: [{id, label, color?}]; links: [{source, target, value}]. Depth (column) is
|
|
8
|
+
* derived from the longest path, so you never position nodes by hand. Hovering a
|
|
9
|
+
* ribbon or a node isolates that flow.
|
|
10
|
+
*/
|
|
11
|
+
export function SankeyChart({ nodes = [], links = [], width = 620, height = 320, nodeWidth = 13, nodePadding = 16, valueFormat = fmtNum, interactive = true, labelWidth = 96, style }) {
|
|
12
|
+
const [hover, setHover] = React.useState(null);
|
|
13
|
+
const pad = { l: 6, r: labelWidth, t: 8, b: 8 };
|
|
14
|
+
const inner = { w: width - pad.l - pad.r, h: height - pad.t - pad.b };
|
|
15
|
+
const { nodes: laid, ribbons } = React.useMemo(
|
|
16
|
+
() => sankey(nodes, links, inner.w, inner.h, nodeWidth, nodePadding),
|
|
17
|
+
[nodes, links, inner.w, inner.h, nodeWidth, nodePadding]
|
|
18
|
+
);
|
|
19
|
+
const byId = Object.fromEntries(laid.map((n) => [n.id, n]));
|
|
20
|
+
const colorOf = (n) => n.color || seriesColor(laid.indexOf(n));
|
|
21
|
+
const litRibbon = (r) => hover == null ? 0.42
|
|
22
|
+
: hover.kind === "ribbon" ? (hover.i === ribbons.indexOf(r) ? 0.78 : 0.12)
|
|
23
|
+
: (r.link.source === hover.id || r.link.target === hover.id ? 0.7 : 0.1);
|
|
24
|
+
const h = hover;
|
|
25
|
+
const tip = h ? (h.kind === "ribbon"
|
|
26
|
+
? { x: (ribbons[h.i].source.x + ribbons[h.i].target.x) / 2 + pad.l, y: 0, title: byId[ribbons[h.i].link.source].label + " → " + byId[ribbons[h.i].link.target].label,
|
|
27
|
+
rows: [{ label: "flow", value: valueFormat(ribbons[h.i].link.value), color: colorOf(byId[ribbons[h.i].link.source]) }] }
|
|
28
|
+
: { x: byId[h.id].x + pad.l + nodeWidth, y: byId[h.id].y + pad.t + byId[h.id].h / 2, title: byId[h.id].label,
|
|
29
|
+
rows: [{ label: "total", value: valueFormat(byId[h.id].value), color: colorOf(byId[h.id]) },
|
|
30
|
+
{ label: "links", value: String(byId[h.id].in.length + byId[h.id].out.length) }] }) : null;
|
|
31
|
+
return (
|
|
32
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
33
|
+
<svg width={width} height={height} role="img">
|
|
34
|
+
<g transform={"translate(" + pad.l + "," + pad.t + ")"}>
|
|
35
|
+
{ribbons.map((r, i) => (
|
|
36
|
+
<path key={i} d={r.d} fill={colorOf(byId[r.link.source])} opacity={litRibbon(r)}
|
|
37
|
+
onMouseEnter={interactive ? () => setHover({ kind: "ribbon", i }) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
38
|
+
style={{ transition: "opacity var(--dur-fast) var(--ease-out)", cursor: interactive ? "default" : undefined }} />
|
|
39
|
+
))}
|
|
40
|
+
{laid.map((n) => {
|
|
41
|
+
const c = colorOf(n);
|
|
42
|
+
const rightSide = n.x > inner.w * 0.55;
|
|
43
|
+
return (
|
|
44
|
+
<g key={n.id} onMouseEnter={interactive ? () => setHover({ kind: "node", id: n.id }) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
45
|
+
style={{ cursor: interactive ? "default" : undefined }}>
|
|
46
|
+
<rect x={n.x} y={n.y} width={n.w} height={n.h} rx="2" fill={c} opacity={hover == null || (hover.kind === "node" && hover.id === n.id) ? 1 : 0.55} />
|
|
47
|
+
<text x={rightSide ? n.x + n.w + 6 : n.x + n.w + 6} y={n.y + n.h / 2 + 3.5}
|
|
48
|
+
style={{ fontFamily: "var(--font-sans)", fontSize: 11, fill: "var(--text-heading)" }}>{n.label}</text>
|
|
49
|
+
<text x={n.x + n.w + 6} y={n.y + n.h / 2 + 15}
|
|
50
|
+
style={{ fontFamily: "var(--font-mono)", fontSize: 9.5, fill: "var(--chart-label)" }}>{valueFormat(n.value)}</text>
|
|
51
|
+
</g>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
</g>
|
|
55
|
+
</svg>
|
|
56
|
+
{tip && interactive ? <ChartTooltip x={tip.x} y={tip.y || height / 2} flip={tip.x > width * 0.6} title={tip.title} rows={tip.rows} /> : null}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, sankey, SERIES } from './chart-geometry.js'
|
|
4
|
+
// Flow diagram; node columns are derived from the longest path, never positioned by hand.
|
|
5
|
+
const props = defineProps({ nodes:{type:Array,default:()=>[]}, links:{type:Array,default:()=>[]}, width:{type:Number,default:620}, height:{type:Number,default:320}, nodeWidth:{type:Number,default:13}, nodePadding:{type:Number,default:16}, labelWidth:{type:Number,default:96}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })
|
|
6
|
+
const hover = ref(null)
|
|
7
|
+
const g = computed(() => {
|
|
8
|
+
const pad = { l:6, r:props.labelWidth, t:8, b:8 }
|
|
9
|
+
const inner = { w: props.width-pad.l-pad.r, h: props.height-pad.t-pad.b }
|
|
10
|
+
const laid = sankey(props.nodes, props.links, inner.w, inner.h, props.nodeWidth, props.nodePadding)
|
|
11
|
+
const byId = {}; laid.nodes.forEach(n=>{ byId[n.id]=n })
|
|
12
|
+
const color = n => n.color || SERIES[laid.nodes.indexOf(n)%SERIES.length]
|
|
13
|
+
return { pad, inner, laid, byId, color,
|
|
14
|
+
ribbons: laid.ribbons.map((r,i)=>Object.assign({}, r, { i, fill: color(byId[r.link.source]) })) }
|
|
15
|
+
})
|
|
16
|
+
function ribbonOpacity(r){
|
|
17
|
+
const h = hover.value
|
|
18
|
+
if (!h) return 0.42
|
|
19
|
+
if (h.kind==='ribbon') return h.i===r.i ? 0.78 : 0.12
|
|
20
|
+
return (r.link.source===h.id || r.link.target===h.id) ? 0.7 : 0.1
|
|
21
|
+
}
|
|
22
|
+
const tip = computed(()=>{
|
|
23
|
+
const h = hover.value
|
|
24
|
+
if (!h) return null
|
|
25
|
+
if (h.kind==='ribbon') {
|
|
26
|
+
const r = g.value.ribbons[h.i]
|
|
27
|
+
return { x:(r.source.x+r.target.x)/2+g.value.pad.l, y:props.height/2,
|
|
28
|
+
title: g.value.byId[r.link.source].label+' → '+g.value.byId[r.link.target].label, value: props.valueFormat(r.link.value) }
|
|
29
|
+
}
|
|
30
|
+
const n = g.value.byId[h.id]
|
|
31
|
+
return { x:n.x+g.value.pad.l+props.nodeWidth, y:n.y+g.value.pad.t+n.h/2, title:n.label, value: props.valueFormat(n.value) }
|
|
32
|
+
})
|
|
33
|
+
</script>
|
|
34
|
+
<template>
|
|
35
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
36
|
+
<svg :width="width" :height="height" role="img">
|
|
37
|
+
<g :transform="'translate('+g.pad.l+','+g.pad.t+')'">
|
|
38
|
+
<path v-for="r in g.ribbons" :key="r.i" :d="r.d" :fill="r.fill" :opacity="ribbonOpacity(r)"
|
|
39
|
+
@mouseenter="interactive && (hover={kind:'ribbon',i:r.i})" @mouseleave="interactive && (hover=null)"
|
|
40
|
+
style="transition:opacity var(--dur-fast) var(--ease-out)"/>
|
|
41
|
+
<g v-for="n in g.laid.nodes" :key="n.id" @mouseenter="interactive && (hover={kind:'node',id:n.id})" @mouseleave="interactive && (hover=null)">
|
|
42
|
+
<rect :x="n.x" :y="n.y" :width="n.w" :height="n.h" rx="2" :fill="g.color(n)" :opacity="hover==null||(hover.kind==='node'&&hover.id===n.id)?1:0.55"/>
|
|
43
|
+
<text :x="n.x+n.w+6" :y="n.y+n.h/2+3.5" style="font-family:var(--font-sans);font-size:11px;fill:var(--text-heading)">{{ n.label }}</text>
|
|
44
|
+
<text :x="n.x+n.w+6" :y="n.y+n.h/2+15" style="font-family:var(--font-mono);font-size:9.5px;fill:var(--chart-label)">{{ valueFormat(n.value) }}</text>
|
|
45
|
+
</g>
|
|
46
|
+
</g>
|
|
47
|
+
</svg>
|
|
48
|
+
<div v-if="tip && interactive" :style="{position:'absolute',left:tip.x+'px',top:tip.y+'px',transform:tip.x>width*0.6?'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'}">
|
|
49
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px">{{ tip.title }}</div>
|
|
50
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">{{ tip.value }}</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface Point3D { x: number; y: number; z: number; series?: string; label?: string; color?: string; }
|
|
3
|
+
/** 3D scatter in an orientation cube; drag to orbit. Depth attenuates radius and opacity. */
|
|
4
|
+
export interface Scatter3DProps {
|
|
5
|
+
points?: Point3D[];
|
|
6
|
+
width?: number; height?: number;
|
|
7
|
+
yaw?: number; pitch?: number;
|
|
8
|
+
/** Base dot radius */ r?: number;
|
|
9
|
+
legend?: boolean; interactive?: boolean;
|
|
10
|
+
style?: React.CSSProperties;
|
|
11
|
+
}
|
|
12
|
+
export declare function Scatter3D(props: Scatter3DProps): JSX.Element;
|