@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,63 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, niceTicks } from './chart-geometry.js'
|
|
4
|
+
import { makeProj, facePath, faceDepth, shade, lightOf, heatColor } from './chart-3d.js'
|
|
5
|
+
// 3D surface z = f(x,y); drag to orbit. Cells shade on the heat ramp by height.
|
|
6
|
+
const props = defineProps({ values:{type:Array,default:()=>[]}, width:{type:Number,default:420}, height:{type:Number,default:300}, yaw:{type:Number,default:38}, pitch:{type:Number,default:32}, zScale:{type:Number,default:0.55}, wire:{type:Boolean,default:true}, xLabel:{type:String,default:'x'}, yLabel:{type:String,default:'y'}, 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
|
+
// Drag state on the DOM node: a closure var is reset by re-render mid-drag.
|
|
10
|
+
function down(e){ e.currentTarget.__kbDrag = { x:e.clientX, y:e.clientY, ...rot.value }; e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId) }
|
|
11
|
+
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)) } }
|
|
12
|
+
function up(e){ e.currentTarget.__kbDrag = null }
|
|
13
|
+
const g = computed(() => {
|
|
14
|
+
const rows = props.values.length, cols = props.values[0] ? props.values[0].length : 0
|
|
15
|
+
let lo = Infinity, hi = -Infinity
|
|
16
|
+
props.values.forEach(r=>r.forEach(v=>{ if(v<lo) lo=v; if(v>hi) hi=v }))
|
|
17
|
+
const span = hi-lo || 1
|
|
18
|
+
const scale = Math.min(props.width, props.height)*0.42
|
|
19
|
+
const P = makeProj(props.width/2, props.height/2 + props.height*0.05, scale, rot.value.yaw, rot.value.pitch)
|
|
20
|
+
const pt = (r,c) => P((c/(cols-1))-0.5, (((props.values[r][c]-lo)/span)-0.5)*props.zScale*2, (r/(rows-1))-0.5)
|
|
21
|
+
const quads = []
|
|
22
|
+
for (let r=0;r<rows-1;r++) for (let c=0;c<cols-1;c++) {
|
|
23
|
+
const pts = [pt(r,c), pt(r,c+1), pt(r+1,c+1), pt(r+1,c)]
|
|
24
|
+
const avg = (props.values[r][c]+props.values[r][c+1]+props.values[r+1][c]+props.values[r+1][c+1])/4
|
|
25
|
+
quads.push({ d: facePath(pts), t:(avg-lo)/span, avg, r, c, depth: faceDepth(pts), light: lightOf(pts[0],pts[1],pts[3]),
|
|
26
|
+
cx: pts.reduce((s,p)=>s+p.x,0)/4, cy: pts.reduce((s,p)=>s+p.y,0)/4 })
|
|
27
|
+
}
|
|
28
|
+
quads.sort((a,b)=>b.depth-a.depth)
|
|
29
|
+
const yB = -props.zScale-0.06
|
|
30
|
+
const base = [P(-0.5,yB,-0.5), P(0.5,yB,-0.5), P(0.5,yB,0.5), P(-0.5,yB,0.5)]
|
|
31
|
+
const corner = P(-0.5,yB,0.5)
|
|
32
|
+
const axes3 = [
|
|
33
|
+
{ a:corner, b:P(0.5,yB,0.5), label:props.xLabel, lp:P(0,yB-0.16,0.5), anchor:'middle' },
|
|
34
|
+
{ a:corner, b:P(-0.5,yB,-0.5), label:props.yLabel, lp:P(-0.62,yB-0.12,0), anchor:'end' },
|
|
35
|
+
{ a:corner, b:P(-0.5,props.zScale,0.5) },
|
|
36
|
+
]
|
|
37
|
+
const zt = niceTicks(lo,hi,3)
|
|
38
|
+
const zTicks = zt.ticks.filter(t=>t>=lo&&t<=hi).map(t=>({ p:P(-0.5, ((t-lo)/span-0.5)*props.zScale*2, 0.5), v:props.valueFormat(t) }))
|
|
39
|
+
return { quads, basePath: facePath(base), axes3, zTicks }
|
|
40
|
+
})
|
|
41
|
+
function fill(q){ return shade(heatColor(q.t), Math.max(-0.35, Math.min(0.3, q.light*0.4)) + (hover.value===q ? 0.18 : 0)) }
|
|
42
|
+
</script>
|
|
43
|
+
<template>
|
|
44
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
45
|
+
<svg :width="width" :height="height" @pointerdown="down" @pointermove="move" @pointerup="up" @pointerleave="up"
|
|
46
|
+
role="img" :style="{cursor:interactive?'grab':'default',touchAction:'none'}">
|
|
47
|
+
<path :d="g.basePath" fill="var(--bg-inset)" stroke="var(--chart-axis)" stroke-width="1"/>
|
|
48
|
+
<line v-for="(ax,i) in g.axes3" :key="'ae'+i" :x1="ax.a.x" :y1="ax.a.y" :x2="ax.b.x" :y2="ax.b.y" stroke="var(--chart-guide)" stroke-width="1"/>
|
|
49
|
+
<text v-for="(ax,i) in g.axes3.filter(a=>a.label)" :key="'al'+i" :x="ax.lp.x" :y="ax.lp.y+3" :text-anchor="ax.anchor"
|
|
50
|
+
style="font-family:var(--font-sans);font-size:10px;fill:var(--text-secondary)">{{ ax.label }}</text>
|
|
51
|
+
<g v-for="(t,i) in g.zTicks" :key="'zt'+i">
|
|
52
|
+
<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"/>
|
|
53
|
+
<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>
|
|
54
|
+
</g>
|
|
55
|
+
<path v-for="(q,i) in g.quads" :key="i" :d="q.d" :fill="fill(q)" :stroke="wire?'var(--bg-surface)':'none'" stroke-width="0.5"
|
|
56
|
+
@mouseenter="interactive && (hover=q)" @mouseleave="interactive && (hover=null)"/>
|
|
57
|
+
</svg>
|
|
58
|
+
<div v-if="hover && interactive" :style="{position:'absolute',left:hover.cx+'px',top:hover.cy+'px',transform:hover.cx>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'}">
|
|
59
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px">row {{ hover.r+1 }} · col {{ hover.c+1 }}</div>
|
|
60
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">{{ valueFormat(hover.avg) }}</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface TreemapItem { label: string; value: number; color?: string; }
|
|
3
|
+
/** Squarified treemap; labels render only where they fit, hover dims the rest. */
|
|
4
|
+
export interface TreemapChartProps {
|
|
5
|
+
items?: TreemapItem[];
|
|
6
|
+
width?: number; height?: number;
|
|
7
|
+
valueFormat?: (v: number) => string;
|
|
8
|
+
interactive?: boolean;
|
|
9
|
+
style?: React.CSSProperties;
|
|
10
|
+
}
|
|
11
|
+
export declare function TreemapChart(props: TreemapChartProps): JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, treemap, seriesColor } from "./chart-geometry.js";
|
|
3
|
+
import { ChartTooltip, wrapStyle } from "./ChartKit.jsx";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Squarified treemap — composition at a glance (cost base, storage by dataset).
|
|
7
|
+
* items: [{label, value, color?}]. Labels render only where they fit.
|
|
8
|
+
*/
|
|
9
|
+
export function TreemapChart({ items = [], width = 560, height = 260, valueFormat = fmtNum, interactive = true, style }) {
|
|
10
|
+
const [hover, setHover] = React.useState(null);
|
|
11
|
+
const cells = treemap(items, width, height);
|
|
12
|
+
const total = items.reduce((s, i) => s + i.value, 0) || 1;
|
|
13
|
+
const colorOf = (it) => it.color || seriesColor(items.indexOf(it));
|
|
14
|
+
const h = hover != null ? cells[hover] : null;
|
|
15
|
+
return (
|
|
16
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
17
|
+
<svg width={width} height={height} role="img">
|
|
18
|
+
{cells.map((c, i) => (
|
|
19
|
+
<g key={i} onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined} style={{ cursor: interactive ? "default" : undefined }}>
|
|
20
|
+
<rect x={c.x + 1} y={c.y + 1} width={Math.max(1, c.w - 2)} height={Math.max(1, c.h - 2)} rx="3" fill={colorOf(c.item)}
|
|
21
|
+
opacity={hover == null ? 0.85 : hover === i ? 1 : 0.35} style={{ transition: "opacity var(--dur-fast) var(--ease-out)" }} />
|
|
22
|
+
{c.w > 74 && c.h > 34 ? <>
|
|
23
|
+
<text x={c.x + 9} y={c.y + 18} style={{ fontFamily: "var(--font-sans)", fontSize: 11, fill: "var(--on-accent)" }}>{c.item.label}</text>
|
|
24
|
+
<text x={c.x + 9} y={c.y + 32} style={{ fontFamily: "var(--font-mono)", fontSize: 10, fill: "var(--on-accent)", opacity: 0.85 }}>{valueFormat(c.item.value)}</text>
|
|
25
|
+
</> : null}
|
|
26
|
+
</g>
|
|
27
|
+
))}
|
|
28
|
+
</svg>
|
|
29
|
+
{h && interactive ? (
|
|
30
|
+
<ChartTooltip x={h.x + h.w / 2} y={h.y + h.h / 2} flip={h.x + h.w / 2 > width * 0.62} title={h.item.label}
|
|
31
|
+
rows={[{ label: "value", value: valueFormat(h.item.value) }, { label: "share", value: ((h.item.value / total) * 100).toFixed(1) + "%" }]} />
|
|
32
|
+
) : null}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, treemap, SERIES } from './chart-geometry.js'
|
|
4
|
+
// Squarified treemap; labels render only where they fit, hover dims the rest.
|
|
5
|
+
const props = defineProps({ items:{type:Array,default:()=>[]}, width:{type:Number,default:560}, height:{type:Number,default:260}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })
|
|
6
|
+
const hover = ref(null)
|
|
7
|
+
const cells = computed(() => treemap(props.items, props.width, props.height).map(c => ({ ...c, color: c.item.color || SERIES[props.items.indexOf(c.item)%SERIES.length] })))
|
|
8
|
+
const total = computed(() => props.items.reduce((s,i)=>s+i.value,0)||1)
|
|
9
|
+
</script>
|
|
10
|
+
<template>
|
|
11
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
12
|
+
<svg :width="width" :height="height" role="img">
|
|
13
|
+
<g v-for="(c,i) in cells" :key="i" @mouseenter="interactive && (hover=i)" @mouseleave="interactive && (hover=null)">
|
|
14
|
+
<rect :x="c.x+1" :y="c.y+1" :width="Math.max(1,c.w-2)" :height="Math.max(1,c.h-2)" rx="3" :fill="c.color"
|
|
15
|
+
:opacity="hover==null?.85:hover===i?1:.35" style="transition:opacity var(--dur-fast) var(--ease-out)"/>
|
|
16
|
+
<template v-if="c.w>74 && c.h>34">
|
|
17
|
+
<text :x="c.x+9" :y="c.y+18" style="font-family:var(--font-sans);font-size:11px;fill:var(--on-accent)">{{ c.item.label }}</text>
|
|
18
|
+
<text :x="c.x+9" :y="c.y+32" style="font-family:var(--font-mono);font-size:10px;fill:var(--on-accent);opacity:.85">{{ valueFormat(c.item.value) }}</text>
|
|
19
|
+
</template>
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
|
22
|
+
<div v-if="hover!=null && interactive" :style="{position:'absolute',left:(cells[hover].x+cells[hover].w/2)+'px',top:(cells[hover].y+cells[hover].h/2)+'px',transform:(cells[hover].x+cells[hover].w/2)>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'}">
|
|
23
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);white-space:nowrap;padding-bottom:2px">{{ cells[hover].item.label }}</div>
|
|
24
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);white-space:nowrap">{{ valueFormat(cells[hover].item.value) }} · {{ ((cells[hover].item.value/total)*100).toFixed(1) }}%</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface WaterfallStep { label: string; /** Signed delta; ignored when total */ value?: number; /** Re-baseline bar showing the running sum (subtotal / result) */ total?: boolean; }
|
|
3
|
+
/** Finance bridge: green gains, red losses, neutral totals, dashed running-sum connectors. */
|
|
4
|
+
export interface WaterfallChartProps {
|
|
5
|
+
steps?: WaterfallStep[];
|
|
6
|
+
width?: number; height?: number;
|
|
7
|
+
axes?: boolean; showValues?: boolean; interactive?: boolean;
|
|
8
|
+
valueFormat?: (v: number) => string;
|
|
9
|
+
style?: React.CSSProperties;
|
|
10
|
+
}
|
|
11
|
+
export declare function WaterfallChart(props: WaterfallChartProps): JSX.Element;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, waterfall, niceTicks, scaleLin } from "./chart-geometry.js";
|
|
3
|
+
import { ChartTooltip, PlotGrid, wrapStyle } from "./ChartKit.jsx";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Waterfall bridge — plan → actual, revenue → net income. steps: [{label, value}] plus
|
|
7
|
+
* {label, total:true} rows that re-baseline (subtotals/result). Positive --chart-pos,
|
|
8
|
+
* negative --chart-neg, totals --chart-total; dashed connectors carry the running sum.
|
|
9
|
+
* Hovering a bar dims the rest and reports the delta and the running total.
|
|
10
|
+
*/
|
|
11
|
+
export function WaterfallChart({ steps = [], width = 560, height = 260, axes = true, valueFormat = fmtNum, showValues = true, interactive = true, style }) {
|
|
12
|
+
const [hover, setHover] = React.useState(null);
|
|
13
|
+
const bars = waterfall(steps);
|
|
14
|
+
const pad = { l: axes ? 44 : 4, r: 8, t: showValues ? 22 : 10, b: 40 };
|
|
15
|
+
const [lo0, hi0] = [Math.min(0, ...bars.map((b) => b.y0)), Math.max(0, ...bars.map((b) => b.y1))];
|
|
16
|
+
const { ticks, lo, hi } = niceTicks(lo0, hi0, 4);
|
|
17
|
+
const sy = scaleLin(lo, hi, height - pad.b, pad.t);
|
|
18
|
+
const slot = (width - pad.l - pad.r) / bars.length;
|
|
19
|
+
const bw = Math.min(slot * 0.58, 40);
|
|
20
|
+
const fill = { pos: "var(--chart-pos)", neg: "var(--chart-neg)", total: "var(--chart-total)" };
|
|
21
|
+
const move = (e) => {
|
|
22
|
+
if (!interactive) return;
|
|
23
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
24
|
+
const i = Math.floor((e.clientX - r.left - pad.l) / slot);
|
|
25
|
+
setHover(i >= 0 && i < bars.length ? i : null);
|
|
26
|
+
};
|
|
27
|
+
const h = hover != null ? bars[hover] : null;
|
|
28
|
+
return (
|
|
29
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
30
|
+
<svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role="img">
|
|
31
|
+
{axes ? <PlotGrid w={width} h={height} pad={pad} ticks={ticks} sy={sy} xLabels={[]} sxLabel={() => 0} yFormat={valueFormat} /> : null}
|
|
32
|
+
{hover != null ? <rect x={pad.l + slot * hover} y={pad.t} width={slot} height={height - pad.t - pad.b} fill="var(--bg-hover)" /> : null}
|
|
33
|
+
{bars.map((b, i) => {
|
|
34
|
+
const x = pad.l + slot * i + slot / 2;
|
|
35
|
+
const y = sy(b.y1), hgt = Math.max(1.5, sy(b.y0) - y);
|
|
36
|
+
const runEnd = b.kind === "total" ? b.y1 : b.to;
|
|
37
|
+
return (
|
|
38
|
+
<g key={i} opacity={hover == null || hover === i ? 1 : 0.45} style={{ transition: "opacity var(--dur-fast) var(--ease-out)" }}>
|
|
39
|
+
<rect x={x - bw / 2} y={y} width={bw} height={hgt} rx="2" fill={fill[b.kind]} />
|
|
40
|
+
{i < bars.length - 1 ? <line x1={x + bw / 2} x2={pad.l + slot * (i + 1) + slot / 2 - bw / 2} y1={sy(runEnd)} y2={sy(runEnd)} stroke="var(--chart-guide)" strokeWidth="1" strokeDasharray="2 3" /> : null}
|
|
41
|
+
{showValues ? <text x={x} y={y - 5} textAnchor="middle" style={{ fontFamily: "var(--font-mono)", fontSize: 10, fill: b.kind === "neg" ? "var(--chart-neg)" : "var(--text-heading)" }}>{(b.kind === "pos" ? "+" : "") + valueFormat(b.value)}</text> : null}
|
|
42
|
+
<text x={x} y={height - pad.b + 15} textAnchor="middle" style={{ fontFamily: "var(--font-sans)", fontSize: 10, fill: "var(--chart-label)" }}>
|
|
43
|
+
{String(b.label).split("\n").map((ln, li) => <tspan key={li} x={x} dy={li ? 11 : 0}>{ln}</tspan>)}
|
|
44
|
+
</text>
|
|
45
|
+
</g>
|
|
46
|
+
);
|
|
47
|
+
})}
|
|
48
|
+
</svg>
|
|
49
|
+
{h && interactive ? (
|
|
50
|
+
<ChartTooltip x={pad.l + slot * hover + slot / 2} y={height / 2 - 10} flip={pad.l + slot * hover > width * 0.62}
|
|
51
|
+
title={String(h.label).replace("\n", " ")}
|
|
52
|
+
rows={h.kind === "total"
|
|
53
|
+
? [{ label: "running total", value: valueFormat(h.value), color: fill.total }]
|
|
54
|
+
: [{ label: h.kind === "pos" ? "gain" : "loss", value: (h.kind === "pos" ? "+" : "") + valueFormat(h.value), color: fill[h.kind] },
|
|
55
|
+
{ label: "running total", value: valueFormat(h.to) }]} />
|
|
56
|
+
) : null}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import { fmtNum, waterfall, niceTicks, scaleLin } from './chart-geometry.js'
|
|
4
|
+
// Finance bridge: green gains, red losses, neutral totals, dashed running-sum connectors.
|
|
5
|
+
const props = defineProps({ steps:{type:Array,default:()=>[]}, width:{type:Number,default:560}, height:{type:Number,default:260}, axes:{type:Boolean,default:true}, showValues:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })
|
|
6
|
+
const hover = ref(null)
|
|
7
|
+
function move(e){ if(!props.interactive) return; const r=e.currentTarget.getBoundingClientRect(); const slot=(props.width-g.value.pad.l-g.value.pad.r)/g.value.out.length; const i=Math.floor((e.clientX-r.left-g.value.pad.l)/slot); hover.value = i>=0 && i<g.value.out.length ? i : null }
|
|
8
|
+
const FILL = { pos:'var(--chart-pos)', neg:'var(--chart-neg)', total:'var(--chart-total)' }
|
|
9
|
+
const g = computed(() => {
|
|
10
|
+
const bars = waterfall(props.steps)
|
|
11
|
+
const pad = { l: props.axes?44:4, r:8, t: props.showValues?22:10, b:40 }
|
|
12
|
+
const { ticks, lo, hi } = niceTicks(Math.min(0,...bars.map(b=>b.y0)), Math.max(0,...bars.map(b=>b.y1)), 4)
|
|
13
|
+
const sy = scaleLin(lo, hi, props.height-pad.b, pad.t)
|
|
14
|
+
const slot = (props.width-pad.l-pad.r)/bars.length
|
|
15
|
+
const bw = Math.min(slot*0.58, 40)
|
|
16
|
+
return { pad, ticks, sy, out: bars.map((b,i)=>{
|
|
17
|
+
const x = pad.l + slot*i + slot/2
|
|
18
|
+
return { ...b, x, bw, y: sy(b.y1), h: Math.max(1.5, sy(b.y0)-sy(b.y1)), fill: FILL[b.kind],
|
|
19
|
+
run: sy(b.kind==='total'?b.y1:b.to), nextX: pad.l + slot*(i+1) + slot/2 - bw/2, last: i===bars.length-1,
|
|
20
|
+
lines: String(b.label).split('\\n') } }) }
|
|
21
|
+
})
|
|
22
|
+
</script>
|
|
23
|
+
<template>
|
|
24
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
25
|
+
<svg :width="width" :height="height" @mousemove="move" @mouseleave="hover=null" role="img">
|
|
26
|
+
<template v-if="axes">
|
|
27
|
+
<g v-for="t in g.ticks" :key="t">
|
|
28
|
+
<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"/>
|
|
29
|
+
<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>
|
|
30
|
+
</g>
|
|
31
|
+
</template>
|
|
32
|
+
<g v-for="(b,i) in g.out" :key="i" :opacity="hover==null||hover===i?1:0.45" style="transition:opacity var(--dur-fast) var(--ease-out)">
|
|
33
|
+
<rect :x="b.x-b.bw/2" :y="b.y" :width="b.bw" :height="b.h" rx="2" :fill="b.fill"/>
|
|
34
|
+
<line v-if="!b.last" :x1="b.x+b.bw/2" :x2="b.nextX" :y1="b.run" :y2="b.run" stroke="var(--chart-guide)" stroke-width="1" stroke-dasharray="2 3"/>
|
|
35
|
+
<text v-if="showValues" :x="b.x" :y="b.y-5" text-anchor="middle" :style="{fontFamily:'var(--font-mono)',fontSize:'10px',fill:b.kind==='neg'?'var(--chart-neg)':'var(--text-heading)'}">{{ (b.kind==='pos'?'+':'')+valueFormat(b.value) }}</text>
|
|
36
|
+
<text :x="b.x" :y="height-g.pad.b+15" text-anchor="middle" style="font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)">
|
|
37
|
+
<tspan v-for="(ln,li) in b.lines" :key="li" :x="b.x" :dy="li?11:0">{{ ln }}</tspan>
|
|
38
|
+
</text>
|
|
39
|
+
</g>
|
|
40
|
+
</svg>
|
|
41
|
+
<div v-if="hover!=null && interactive" :style="{position:'absolute',left:g.out[hover].x+'px',top:(height/2-10)+'px',transform:g.out[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'}">
|
|
42
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px">{{ String(g.out[hover].label).replace('\n',' ') }}</div>
|
|
43
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">{{ g.out[hover].kind==='total' ? valueFormat(g.out[hover].value) : ((g.out[hover].kind==='pos'?'+':'')+valueFormat(g.out[hover].value)+' → '+valueFormat(g.out[hover].to)) }}</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* World choropleth on real Natural Earth geometry (world-atlas 110m) projected with d3-geo.
|
|
4
|
+
* The host page MUST load the pinned d3 + topojson-client tags; the component reads them
|
|
5
|
+
* from window and fetches the pinned topology itself.
|
|
6
|
+
*/
|
|
7
|
+
export interface WorldMapProps {
|
|
8
|
+
/** Country name → value, e.g. { "United States": 6400, Germany: 5200 } */
|
|
9
|
+
values?: Record<string, number>;
|
|
10
|
+
width?: number; height?: number;
|
|
11
|
+
projection?: "naturalEarth" | "mercator" | "equalEarth";
|
|
12
|
+
/** "blue" sequential ramp, or "diverging" for signed values */
|
|
13
|
+
scale?: "blue" | "diverging";
|
|
14
|
+
valueFormat?: (v: number) => string;
|
|
15
|
+
legend?: boolean;
|
|
16
|
+
/** Unit shown in the legend and tooltip row label */ unit?: string;
|
|
17
|
+
interactive?: boolean;
|
|
18
|
+
onSelect?: (country: string, value: number | undefined) => void;
|
|
19
|
+
style?: React.CSSProperties;
|
|
20
|
+
}
|
|
21
|
+
export declare function WorldMap(props: WorldMapProps): JSX.Element;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, quantize } from "./chart-geometry.js";
|
|
3
|
+
import { ChartTooltip, wrapStyle } from "./ChartKit.jsx";
|
|
4
|
+
|
|
5
|
+
const HEAT = ["var(--chart-heat-1)", "var(--chart-heat-2)", "var(--chart-heat-3)", "var(--chart-heat-4)", "var(--chart-heat-5)"];
|
|
6
|
+
const TOPO = "https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json";
|
|
7
|
+
/* Natural Earth spells several countries formally; accept the everyday name too so
|
|
8
|
+
callers can write { "United States": 1 } instead of "United States of America". */
|
|
9
|
+
const ALIAS = {
|
|
10
|
+
"united states": "United States of America", usa: "United States of America", us: "United States of America",
|
|
11
|
+
uk: "United Kingdom", "great britain": "United Kingdom", britain: "United Kingdom",
|
|
12
|
+
"south korea": "South Korea", korea: "South Korea", "czech republic": "Czechia",
|
|
13
|
+
uae: "United Arab Emirates", drc: "Dem. Rep. Congo", "democratic republic of the congo": "Dem. Rep. Congo",
|
|
14
|
+
"bosnia and herzegovina": "Bosnia and Herz.", "dominican republic": "Dominican Rep.",
|
|
15
|
+
"central african republic": "Central African Rep.", "south sudan": "S. Sudan", "ivory coast": "Côte d'Ivoire",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* World choropleth. Real Natural Earth geometry (world-atlas 110m) projected with d3-geo —
|
|
20
|
+
* never hand-drawn outlines. `values` maps country name → number ("United States",
|
|
21
|
+
* "Germany"…); unmatched countries render as empty land. Hover highlights a country and
|
|
22
|
+
* pins its value; click reports it.
|
|
23
|
+
*
|
|
24
|
+
* The host page must load the pinned d3 + topojson-client tags (see the card source);
|
|
25
|
+
* the component reads them off `window` and fetches the pinned topology itself.
|
|
26
|
+
*/
|
|
27
|
+
export function WorldMap({ values = {}, width = 660, height = 340, projection = "naturalEarth", scale = "blue", valueFormat = fmtNum, legend = true, unit, interactive = true, onSelect, style }) {
|
|
28
|
+
const [geo, setGeo] = React.useState(null);
|
|
29
|
+
const [err, setErr] = React.useState(null);
|
|
30
|
+
const [hover, setHover] = React.useState(null);
|
|
31
|
+
|
|
32
|
+
React.useEffect(() => {
|
|
33
|
+
let dead = false;
|
|
34
|
+
const d3 = window.d3, topojson = window.topojson;
|
|
35
|
+
if (!d3 || !topojson) { setErr("d3 + topojson-client are required on the page"); return; }
|
|
36
|
+
fetch(TOPO).then((r) => r.json()).then((topo) => {
|
|
37
|
+
if (dead) return;
|
|
38
|
+
const fc = topojson.feature(topo, topo.objects.countries);
|
|
39
|
+
setGeo(fc);
|
|
40
|
+
}).catch(() => !dead && setErr("could not load country geometry"));
|
|
41
|
+
return () => { dead = true; };
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const paths = React.useMemo(() => {
|
|
45
|
+
if (!geo || !window.d3) return null;
|
|
46
|
+
const d3 = window.d3;
|
|
47
|
+
const proj = (projection === "mercator" ? d3.geoMercator() : projection === "equalEarth" && d3.geoEqualEarth ? d3.geoEqualEarth() : d3.geoNaturalEarth1())
|
|
48
|
+
.fitSize([width, height - (legend ? 22 : 0)], geo);
|
|
49
|
+
const gp = d3.geoPath(proj);
|
|
50
|
+
return geo.features.map((f) => ({ name: f.properties.name, d: gp(f), c: gp.centroid(f) }));
|
|
51
|
+
}, [geo, width, height, projection, legend]);
|
|
52
|
+
|
|
53
|
+
/* Resolve caller keys to Natural Earth feature names once, so both the scale and every
|
|
54
|
+
lookup agree on the same key set. */
|
|
55
|
+
const resolved = React.useMemo(() => {
|
|
56
|
+
const out = {};
|
|
57
|
+
Object.keys(values).forEach((k) => {
|
|
58
|
+
const canon = ALIAS[k.toLowerCase()] || k;
|
|
59
|
+
out[canon] = values[k];
|
|
60
|
+
});
|
|
61
|
+
return out;
|
|
62
|
+
}, [values]);
|
|
63
|
+
const nums = Object.values(resolved).filter((v) => typeof v === "number");
|
|
64
|
+
const bucket = quantize(nums, 5);
|
|
65
|
+
const lo = Math.min(...nums), hi = Math.max(...nums);
|
|
66
|
+
const fillOf = (name) => {
|
|
67
|
+
const v = resolved[name];
|
|
68
|
+
if (v == null) return "var(--chart-track)";
|
|
69
|
+
if (scale === "diverging") {
|
|
70
|
+
const amax = Math.max(Math.abs(lo), Math.abs(hi)) || 1;
|
|
71
|
+
const a = Math.round((0.18 + Math.min(1, Math.abs(v) / amax) * 0.82) * 100);
|
|
72
|
+
return "color-mix(in srgb, var(--chart-" + (v >= 0 ? "pos" : "neg") + ") " + a + "%, var(--bg-surface))";
|
|
73
|
+
}
|
|
74
|
+
return HEAT[Math.max(0, bucket(v))];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (err) return <div style={{ width, height, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--text-muted)", background: "var(--bg-inset)", borderRadius: "var(--radius-lg)", ...style }}>{err}</div>;
|
|
78
|
+
if (!paths) return <div style={{ width, height, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--text-muted)", ...style }}>loading map…</div>;
|
|
79
|
+
|
|
80
|
+
const h = hover != null ? paths[hover] : null;
|
|
81
|
+
return (
|
|
82
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
83
|
+
<svg width={width} height={height - (legend ? 22 : 0)} role="img">
|
|
84
|
+
{paths.map((p, i) => (
|
|
85
|
+
<path key={p.name + i} d={p.d} fill={fillOf(p.name)} stroke="var(--bg-surface)" strokeWidth="0.5"
|
|
86
|
+
onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
87
|
+
onClick={onSelect ? () => onSelect(p.name, resolved[p.name]) : undefined}
|
|
88
|
+
style={{ cursor: onSelect ? "pointer" : "default", outline: "none", opacity: hover == null || hover === i ? 1 : 0.75, transition: "opacity var(--dur-fast) var(--ease-out)" }} />
|
|
89
|
+
))}
|
|
90
|
+
{h ? <path d={h.d} fill="none" stroke="var(--action-primary)" strokeWidth="1.4" pointerEvents="none" /> : null}
|
|
91
|
+
</svg>
|
|
92
|
+
{legend ? (
|
|
93
|
+
<div style={{ display: "flex", alignItems: "center", gap: 6, paddingTop: 6, lineHeight: "normal" }}>
|
|
94
|
+
<span style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--chart-label)" }}>{valueFormat(lo)}</span>
|
|
95
|
+
{(scale === "diverging" ? ["var(--chart-neg)", "color-mix(in srgb, var(--chart-neg) 40%, var(--bg-surface))", "var(--chart-track)", "color-mix(in srgb, var(--chart-pos) 40%, var(--bg-surface))", "var(--chart-pos)"] : HEAT).map((c, i) => (
|
|
96
|
+
<span key={i} style={{ width: 22, height: 8, borderRadius: 2, background: c }} />
|
|
97
|
+
))}
|
|
98
|
+
<span style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--chart-label)" }}>{valueFormat(hi)}{unit ? " " + unit : ""}</span>
|
|
99
|
+
</div>
|
|
100
|
+
) : null}
|
|
101
|
+
{h && interactive ? (
|
|
102
|
+
<ChartTooltip x={h.c[0]} y={h.c[1]} flip={h.c[0] > width * 0.62} title={h.name}
|
|
103
|
+
rows={[{ label: unit || "value", value: resolved[h.name] == null ? "no data" : valueFormat(resolved[h.name]), color: resolved[h.name] == null ? undefined : fillOf(h.name) }]} />
|
|
104
|
+
) : null}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, onMounted } from 'vue'
|
|
3
|
+
import { fmtNum, quantize } from './chart-geometry.js'
|
|
4
|
+
/**
|
|
5
|
+
* World choropleth on real Natural Earth geometry (world-atlas 110m) projected with d3-geo.
|
|
6
|
+
* The host page MUST load the pinned d3 + topojson-client tags; this reads them off window.
|
|
7
|
+
*/
|
|
8
|
+
const HEAT = ['var(--chart-heat-1)','var(--chart-heat-2)','var(--chart-heat-3)','var(--chart-heat-4)','var(--chart-heat-5)']
|
|
9
|
+
const TOPO = 'https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json'
|
|
10
|
+
const ALIAS = { 'united states':'United States of America', usa:'United States of America', us:'United States of America',
|
|
11
|
+
uk:'United Kingdom', britain:'United Kingdom', 'great britain':'United Kingdom', 'czech republic':'Czechia',
|
|
12
|
+
uae:'United Arab Emirates', 'south sudan':'S. Sudan', 'dominican republic':'Dominican Rep.',
|
|
13
|
+
'bosnia and herzegovina':'Bosnia and Herz.', 'central african republic':'Central African Rep.', 'ivory coast':"Côte d'Ivoire" }
|
|
14
|
+
const props = defineProps({ values:{type:Object,default:()=>({})}, width:{type:Number,default:660}, height:{type:Number,default:340}, projection:{type:String,default:'naturalEarth'}, scale:{type:String,default:'blue'}, valueFormat:{type:Function,default:fmtNum}, legend:{type:Boolean,default:true}, unit:String, interactive:{type:Boolean,default:true}, style:Object })
|
|
15
|
+
const emit = defineEmits(['select'])
|
|
16
|
+
const geo = ref(null); const err = ref(null); const hover = ref(null)
|
|
17
|
+
onMounted(() => {
|
|
18
|
+
const d3 = window.d3, topojson = window.topojson
|
|
19
|
+
if (!d3 || !topojson) { err.value = 'd3 + topojson-client are required on the page'; return }
|
|
20
|
+
fetch(TOPO).then(r=>r.json()).then(topo=>{ geo.value = topojson.feature(topo, topo.objects.countries) })
|
|
21
|
+
.catch(()=>{ err.value = 'could not load country geometry' })
|
|
22
|
+
})
|
|
23
|
+
const resolved = computed(() => {
|
|
24
|
+
const out = {}
|
|
25
|
+
Object.keys(props.values).forEach(k=>{ out[ALIAS[k.toLowerCase()]||k] = props.values[k] })
|
|
26
|
+
return out
|
|
27
|
+
})
|
|
28
|
+
const plotH = computed(() => props.height - (props.legend ? 22 : 0))
|
|
29
|
+
const paths = computed(() => {
|
|
30
|
+
if (!geo.value || !window.d3) return null
|
|
31
|
+
const d3 = window.d3
|
|
32
|
+
const proj = (props.projection==='mercator' ? d3.geoMercator() : (props.projection==='equalEarth' && d3.geoEqualEarth ? d3.geoEqualEarth() : d3.geoNaturalEarth1())).fitSize([props.width, plotH.value], geo.value)
|
|
33
|
+
const gp = d3.geoPath(proj)
|
|
34
|
+
return geo.value.features.map(f=>({ name:f.properties.name, d:gp(f), c:gp.centroid(f) }))
|
|
35
|
+
})
|
|
36
|
+
const nums = computed(() => Object.values(resolved.value).filter(v=>typeof v==='number'))
|
|
37
|
+
const lo = computed(() => Math.min(...nums.value)); const hi = computed(() => Math.max(...nums.value))
|
|
38
|
+
const bucket = computed(() => quantize(nums.value, 5))
|
|
39
|
+
function fill(name){
|
|
40
|
+
const v = resolved.value[name]
|
|
41
|
+
if (v==null) return 'var(--chart-track)'
|
|
42
|
+
if (props.scale==='diverging') {
|
|
43
|
+
const amax = Math.max(Math.abs(lo.value), Math.abs(hi.value))||1
|
|
44
|
+
const a = Math.round((0.18+Math.min(1,Math.abs(v)/amax)*0.82)*100)
|
|
45
|
+
return 'color-mix(in srgb, var(--chart-'+(v>=0?'pos':'neg')+') '+a+'%, var(--bg-surface))'
|
|
46
|
+
}
|
|
47
|
+
return HEAT[Math.max(0, bucket.value(v))]
|
|
48
|
+
}
|
|
49
|
+
const swatches = computed(() => props.scale==='diverging'
|
|
50
|
+
? ['var(--chart-neg)','color-mix(in srgb, var(--chart-neg) 40%, var(--bg-surface))','var(--chart-track)','color-mix(in srgb, var(--chart-pos) 40%, var(--bg-surface))','var(--chart-pos)']
|
|
51
|
+
: HEAT)
|
|
52
|
+
</script>
|
|
53
|
+
<template>
|
|
54
|
+
<div v-if="err" :style="{width:width+'px',height:height+'px',display:'flex',alignItems:'center',justifyContent:'center',fontFamily:'var(--font-mono)',fontSize:'var(--text-xs)',color:'var(--text-muted)',background:'var(--bg-inset)',borderRadius:'var(--radius-lg)',...style}">{{ err }}</div>
|
|
55
|
+
<div v-else-if="!paths" :style="{width:width+'px',height:height+'px',display:'flex',alignItems:'center',justifyContent:'center',fontFamily:'var(--font-mono)',fontSize:'var(--text-xs)',color:'var(--text-muted)',...style}">loading map…</div>
|
|
56
|
+
<div v-else :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
57
|
+
<svg :width="width" :height="plotH" role="img">
|
|
58
|
+
<path v-for="(p,i) in paths" :key="p.name+i" :d="p.d" :fill="fill(p.name)" stroke="var(--bg-surface)" stroke-width="0.5"
|
|
59
|
+
@mouseenter="interactive && (hover=i)" @mouseleave="interactive && (hover=null)" @click="emit('select', p.name, resolved[p.name])"
|
|
60
|
+
:style="{opacity:hover==null||hover===i?1:0.75,transition:'opacity var(--dur-fast) var(--ease-out)'}"/>
|
|
61
|
+
<path v-if="hover!=null" :d="paths[hover].d" fill="none" stroke="var(--action-primary)" stroke-width="1.4" pointer-events="none"/>
|
|
62
|
+
</svg>
|
|
63
|
+
<div v-if="legend" style="display:flex;align-items:center;gap:6px;padding-top:6px;line-height:normal">
|
|
64
|
+
<span style="font-family:var(--font-mono);font-size:10px;color:var(--chart-label)">{{ valueFormat(lo) }}</span>
|
|
65
|
+
<span v-for="(c,i) in swatches" :key="i" :style="{width:'22px',height:'8px',borderRadius:'2px',background:c}"/>
|
|
66
|
+
<span style="font-family:var(--font-mono);font-size:10px;color:var(--chart-label)">{{ valueFormat(hi) }}{{ unit ? ' '+unit : '' }}</span>
|
|
67
|
+
</div>
|
|
68
|
+
<div v-if="hover!=null && interactive" :style="{position:'absolute',left:paths[hover].c[0]+'px',top:paths[hover].c[1]+'px',transform:paths[hover].c[0]>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'}">
|
|
69
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px">{{ paths[hover].name }}</div>
|
|
70
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">{{ resolved[paths[hover].name]==null ? 'no data' : valueFormat(resolved[paths[hover].name]) }}</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<!-- @dsCard group="Directory Index" viewport="700x240" name="charts/" subtitle="22 chart types · 3 size tiers · chart-geometry core + ChartKit furniture" -->
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css">
|
|
4
|
+
<style>body{margin:0;background:var(--bg-surface);padding:18px;font-family:var(--font-sans)}.g{display:flex;flex-wrap:wrap;gap:8px}.p{border:1px solid var(--border-card);border-radius:var(--radius-md);padding:6px 10px;font-size:var(--text-xs);color:var(--text-body);font-family:var(--font-mono)}.h{font-size:var(--text-sm);line-height:19px;color:var(--text-secondary);padding-bottom:12px}</style></head>
|
|
5
|
+
<body><div class="h">components/charts/ — the charting module. Every type has a live card under the <b>Charts</b> group; sizing tiers and color rules in <span style="font-family:var(--font-mono)">charts.prompt.md</span>. <b>WorldMap</b> additionally needs the pinned d3 + topojson tags on the page.</div>
|
|
6
|
+
<div class="g"><span class="p">Sparkline</span><span class="p">LineChart</span><span class="p">BarChart</span><span class="p">BarList</span><span class="p">DonutChart</span><span class="p">ScatterChart</span><span class="p">HeatmapChart</span><span class="p">WaterfallChart</span><span class="p">BulletChart</span><span class="p">GaugeChart</span><span class="p">FunnelChart</span><span class="p">TreemapChart</span><span class="p">RadarChart</span><span class="p">Histogram</span><span class="p">BoxPlot</span><span class="p">ParetoChart</span><span class="p">CandlestickChart</span><span class="p">SankeyChart</span><span class="p">ChordChart</span><span class="p">SunburstChart</span><span class="p">WorldMap</span><span class="p">GanttChart</span><span class="p">ChartKit</span></div>
|
|
7
|
+
</body></html>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Shared 3D chart math: yaw/pitch rotation, orthographic projection, depth sorting,
|
|
2
|
+
// face shading. Pure functions — no DOM, no tokens.
|
|
3
|
+
|
|
4
|
+
/** Rotate (x,y,z) by yaw (around Y) then pitch (around X); degrees. */
|
|
5
|
+
export function rotate3(x, y, z, yaw, pitch) {
|
|
6
|
+
const a = (yaw * Math.PI) / 180, b = (pitch * Math.PI) / 180;
|
|
7
|
+
const x1 = x * Math.cos(a) - z * Math.sin(a);
|
|
8
|
+
const z1 = x * Math.sin(a) + z * Math.cos(a);
|
|
9
|
+
const y1 = y * Math.cos(b) - z1 * Math.sin(b);
|
|
10
|
+
const z2 = y * Math.sin(b) + z1 * Math.cos(b);
|
|
11
|
+
return [x1, y1, z2];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Projector for a unit-ish box: world coords in, screen [x,y] + depth out. */
|
|
15
|
+
export function makeProj(cx, cy, scale, yaw, pitch) {
|
|
16
|
+
return (x, y, z) => {
|
|
17
|
+
const [rx, ry, rz] = rotate3(x, -y, z, yaw, pitch);
|
|
18
|
+
return { x: cx + rx * scale, y: cy + ry * scale, depth: rz };
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const facePath = (pts) => "M" + pts.map((p) => p.x.toFixed(1) + " " + p.y.toFixed(1)).join(" L") + " Z";
|
|
23
|
+
export const faceDepth = (pts) => pts.reduce((s, p) => s + p.depth, 0) / pts.length;
|
|
24
|
+
|
|
25
|
+
/** Lambert-ish shade for a quad: mixes the base color toward white/black by orientation. */
|
|
26
|
+
export function shade(color, amount) {
|
|
27
|
+
const pct = Math.round(Math.abs(amount) * 100);
|
|
28
|
+
return amount >= 0
|
|
29
|
+
? `color-mix(in srgb, ${color} ${100 - pct}%, #fff)`
|
|
30
|
+
: `color-mix(in srgb, ${color} ${100 - pct}%, #000)`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Normal-based light factor for a quad given 3 projected-space corners (screen z). */
|
|
34
|
+
export function lightOf(p0, p1, p2) {
|
|
35
|
+
const ux = p1.x - p0.x, uy = p1.y - p0.y, uz = p1.depth - p0.depth;
|
|
36
|
+
const vx = p2.x - p0.x, vy = p2.y - p0.y, vz = p2.depth - p0.depth;
|
|
37
|
+
const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
|
|
38
|
+
const len = Math.hypot(nx, ny, nz) || 1;
|
|
39
|
+
// Light from upper-left-front
|
|
40
|
+
return (nx * -0.35 + ny * -0.6 + nz * -0.72) / len;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The 6 faces of a cuboid spanning [x0,x1]×[y0,y1]×[z0,z1], projected + depth-sorted. */
|
|
44
|
+
export function cuboidFaces(P, x0, x1, y0, y1, z0, z1) {
|
|
45
|
+
const c = [
|
|
46
|
+
P(x0, y0, z0), P(x1, y0, z0), P(x1, y1, z0), P(x0, y1, z0),
|
|
47
|
+
P(x0, y0, z1), P(x1, y0, z1), P(x1, y1, z1), P(x0, y1, z1),
|
|
48
|
+
];
|
|
49
|
+
const F = [
|
|
50
|
+
{ pts: [c[0], c[1], c[2], c[3]], kind: "front" },
|
|
51
|
+
{ pts: [c[4], c[5], c[6], c[7]], kind: "back" },
|
|
52
|
+
{ pts: [c[3], c[2], c[6], c[7]], kind: "top" },
|
|
53
|
+
{ pts: [c[0], c[1], c[5], c[4]], kind: "bottom" },
|
|
54
|
+
{ pts: [c[0], c[3], c[7], c[4]], kind: "left" },
|
|
55
|
+
{ pts: [c[1], c[2], c[6], c[5]], kind: "right" },
|
|
56
|
+
];
|
|
57
|
+
F.forEach((f) => { f.depth = faceDepth(f.pts); f.d = facePath(f.pts); });
|
|
58
|
+
return F.sort((a, b) => b.depth - a.depth);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Drag-to-rotate pointer handlers: returns props to spread on the svg.
|
|
62
|
+
Drag state lives on the DOM node — a closure variable would be reset by every
|
|
63
|
+
React re-render mid-drag, which silently kills the rotation after one frame. */
|
|
64
|
+
export function dragRotate(get, set) {
|
|
65
|
+
return {
|
|
66
|
+
onPointerDown: (e) => { e.currentTarget.__kbDrag = { x: e.clientX, y: e.clientY, ...get() }; e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId); },
|
|
67
|
+
onPointerMove: (e) => {
|
|
68
|
+
const s = e.currentTarget.__kbDrag;
|
|
69
|
+
if (!s) return;
|
|
70
|
+
set({
|
|
71
|
+
yaw: s.yaw + (e.clientX - s.x) * 0.45,
|
|
72
|
+
pitch: Math.max(8, Math.min(80, s.pitch + (e.clientY - s.y) * 0.35)),
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
onPointerUp: (e) => { e.currentTarget.__kbDrag = null; },
|
|
76
|
+
onPointerLeave: (e) => { e.currentTarget.__kbDrag = null; },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Sequential heat color for t∈[0,1] on the chart heat ramp (surface shading). */
|
|
81
|
+
export function heatColor(t) {
|
|
82
|
+
const stops = ["var(--chart-heat-1)", "var(--chart-heat-2)", "var(--chart-heat-3)", "var(--chart-heat-4)", "var(--chart-heat-5)"];
|
|
83
|
+
return stops[Math.max(0, Math.min(4, Math.floor(t * 5 - 1e-9)))];
|
|
84
|
+
}
|