@humanforest/nuxt-layer 0.1.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/LICENSE +67 -0
- package/README.md +208 -0
- package/app.config.ts +10 -0
- package/assets/icons/forest/bike-asterisk.svg +21 -0
- package/assets/icons/forest/bike-down.svg +19 -0
- package/assets/icons/forest/bike-off.svg +20 -0
- package/assets/icons/forest/bike-up.svg +19 -0
- package/assets/icons/forest/bike-x.svg +19 -0
- package/assets/icons/forest/bike.svg +18 -0
- package/assets/icons/forest/mark-fill.svg +6 -0
- package/assets/icons/forest/mark.svg +6 -0
- package/assets/icons/forest/parking.svg +3 -0
- package/assets/icons/forest/star-fill.svg +4 -0
- package/assets/icons/forest/star.svg +4 -0
- package/assets/icons/forest/traffic-light-caution.svg +8 -0
- package/assets/icons/forest/traffic-light-go.svg +8 -0
- package/assets/icons/forest/traffic-light-lit.svg +8 -0
- package/assets/icons/forest/traffic-light-stop.svg +8 -0
- package/assets/icons/forest/traffic-light.svg +8 -0
- package/nuxt.config.ts +81 -0
- package/package.json +35 -0
- package/packages/charts/src/FBoroughShape.vue +52 -0
- package/packages/charts/src/FCalendarGrid.vue +332 -0
- package/packages/charts/src/FCellLegend.vue +54 -0
- package/packages/charts/src/FChartFrame.vue +201 -0
- package/packages/charts/src/FChartFrameSingle.vue +91 -0
- package/packages/charts/src/FChartLegend.vue +26 -0
- package/packages/charts/src/FDistributionBar.vue +224 -0
- package/packages/charts/src/FDottedMap.vue +335 -0
- package/packages/charts/src/FLondonMap.vue +399 -0
- package/packages/charts/src/FSparkline.vue +198 -0
- package/packages/charts/src/FStatusTrack.vue +151 -0
- package/packages/charts/src/cellSize.ts +46 -0
- package/packages/charts/src/distribution.ts +99 -0
- package/packages/charts/src/engine.ts +15 -0
- package/packages/charts/src/forestTooltip.ts +110 -0
- package/packages/charts/src/fromCategories.ts +43 -0
- package/packages/charts/src/index.ts +87 -0
- package/packages/charts/src/londonAreas.ts +75 -0
- package/packages/charts/src/londonGrid.ts +67 -0
- package/packages/charts/src/motionDuration.ts +49 -0
- package/packages/charts/src/presets.ts +143 -0
- package/packages/charts/src/resolveVar.ts +21 -0
- package/packages/charts/src/snapTooltip.ts +154 -0
- package/packages/charts/src/tooltipStandIn.ts +58 -0
- package/packages/charts/src/useCellScale.ts +155 -0
- package/packages/charts/src/useChartPalette.ts +61 -0
- package/packages/charts/src/useChartRepaintKey.ts +22 -0
- package/packages/charts/src/useThemeVersion.ts +17 -0
- package/packages/frames/src/FBrowserFrame.vue +53 -0
- package/packages/frames/src/FDeviceFrame.vue +242 -0
- package/packages/frames/src/FFitFrame.vue +83 -0
- package/packages/frames/src/art/iphone-11.svg +124 -0
- package/packages/frames/src/art/iphone-12.svg +601 -0
- package/packages/frames/src/art/iphone-13-pro.svg +820 -0
- package/packages/frames/src/art/iphone-13.svg +849 -0
- package/packages/frames/src/art/iphone-14.svg +681 -0
- package/packages/frames/src/art/iphone-15-pro.svg +633 -0
- package/packages/frames/src/art/iphone-15.svg +713 -0
- package/packages/frames/src/art/iphone-16-plus.svg +685 -0
- package/packages/frames/src/art/iphone-16-pro-max.svg +611 -0
- package/packages/frames/src/art/iphone-16-pro.svg +631 -0
- package/packages/frames/src/art/iphone-16.svg +713 -0
- package/packages/frames/src/art/iphone-17-pro-max.svg +355 -0
- package/packages/frames/src/art/iphone-17.svg +305 -0
- package/packages/frames/src/art/iphone-air.svg +389 -0
- package/packages/frames/src/art/iphone-se-3.svg +473 -0
- package/packages/frames/src/art/iphone-x.svg +652 -0
- package/packages/frames/src/art/pixel-10-pro-xl.svg +32 -0
- package/packages/frames/src/art/pixel-10-pro.svg +32 -0
- package/packages/frames/src/art/pixel-10.svg +32 -0
- package/packages/frames/src/art/pixel-8-pro.svg +208 -0
- package/packages/frames/src/art/pixel-8.svg +208 -0
- package/packages/frames/src/art/pixel-8a.svg +8 -0
- package/packages/frames/src/art/pixel-9-pro-xl.svg +32 -0
- package/packages/frames/src/art/pixel-9-pro.svg +34 -0
- package/packages/frames/src/art/pixel-9a.svg +25 -0
- package/packages/frames/src/art.ts +29 -0
- package/packages/frames/src/devices.ts +337 -0
- package/packages/frames/src/index.ts +15 -0
- package/packages/tokens/geo/london-boroughs.json +1 -0
- package/packages/tokens/geo/london-dots-coarse.json +1 -0
- package/packages/tokens/geo/london-dots-fine.json +1 -0
- package/packages/tokens/geo/london-dots-medium.json +1 -0
- package/packages/tokens/geo/london-dots-ultra.json +1 -0
- package/packages/tokens/geo/london-wards.json +1 -0
- package/packages/tokens/glyphs/glyphs.json +1 -0
- package/packages/tokens/logos/f-rot.svg +4 -0
- package/packages/tokens/logos/f.svg +4 -0
- package/packages/tokens/logos/favicon-c2w.svg +9 -0
- package/packages/tokens/logos/favicon-console.svg +9 -0
- package/packages/tokens/logos/favicon-ds.svg +9 -0
- package/packages/tokens/logos/favicon-fleet.svg +9 -0
- package/packages/tokens/logos/favicon-flex.svg +9 -0
- package/packages/tokens/logos/favicon-plus.svg +9 -0
- package/packages/tokens/logos/favicon-radar.svg +9 -0
- package/packages/tokens/logos/favicon-tower.svg +9 -0
- package/packages/tokens/logos/lockup-horizontal.svg +17 -0
- package/packages/tokens/logos/lockup-vertical.svg +17 -0
- package/packages/tokens/logos/mark-mono.svg +4 -0
- package/packages/tokens/logos/mark.svg +7 -0
- package/packages/tokens/logos/wordmark.svg +9 -0
- package/packages/tokens/src/colourEngine.ts +604 -0
- package/packages/tokens/src/glyphs.ts +127 -0
- package/packages/tokens/src/logo.ts +50 -0
- package/packages/tokens/src/subbrands.ts +44 -0
- package/packages/tokens/src/sublogo.ts +80 -0
- package/packages/ui/icons/forest/map/forest-place-count.svg +21 -0
- package/packages/ui/icons/forest/map/forest-place-marker.svg +17 -0
- package/packages/ui/icons/forest/map/forest-vehicle-marker.svg +73 -0
- package/packages/ui/icons/forest/map/manifest.json +373 -0
- package/packages/ui/icons/forest/map/place-count-manifest.json +83 -0
- package/packages/ui/icons/forest/map/place-manifest.json +74 -0
- package/packages/ui/src/address/address.ts +125 -0
- package/packages/ui/src/address/autofill.ts +78 -0
- package/packages/ui/src/address/geocode.ts +55 -0
- package/packages/ui/src/address/index.ts +31 -0
- package/packages/ui/src/address/validate.ts +91 -0
- package/packages/ui/src/canvas/FCanvasCard.vue +97 -0
- package/packages/ui/src/canvas/FCanvasControl.vue +121 -0
- package/packages/ui/src/canvas/FCanvasControlGroup.vue +100 -0
- package/packages/ui/src/canvas/FCanvasFlyout.vue +216 -0
- package/packages/ui/src/canvas/FCanvasHud.vue +402 -0
- package/packages/ui/src/canvas/canvasSurface.ts +39 -0
- package/packages/ui/src/canvas/index.ts +17 -0
- package/packages/ui/src/contexts/ForestContext.vue +53 -0
- package/packages/ui/src/contexts/contexts.ts +153 -0
- package/packages/ui/src/contexts/index.ts +23 -0
- package/packages/ui/src/contexts/marketing-theme.ts +214 -0
- package/packages/ui/src/contexts/mobile-theme.ts +402 -0
- package/packages/ui/src/filter/FFilterBar.vue +72 -0
- package/packages/ui/src/filter/FFilterChip.vue +362 -0
- package/packages/ui/src/filter/FFilterClauses.vue +104 -0
- package/packages/ui/src/filter/FFilterTrigger.vue +183 -0
- package/packages/ui/src/filter/filter.theme.ts +27 -0
- package/packages/ui/src/filter/filterClause.ts +265 -0
- package/packages/ui/src/filter/filterPredicate.ts +134 -0
- package/packages/ui/src/filter/filterSchema.ts +82 -0
- package/packages/ui/src/filter/index.ts +36 -0
- package/packages/ui/src/forest-preset.ts +68 -0
- package/packages/ui/src/icons.ts +79 -0
- package/packages/ui/src/index.ts +17 -0
- package/packages/ui/src/kpi/FKpi.vue +253 -0
- package/packages/ui/src/kpi/FKpiGroup.vue +161 -0
- package/packages/ui/src/kpi/index.ts +20 -0
- package/packages/ui/src/kpi/kpi.theme.ts +168 -0
- package/packages/ui/src/kpi/kpiDelta.ts +82 -0
- package/packages/ui/src/kpi/kpiFormat.ts +72 -0
- package/packages/ui/src/kpi/kpiGroupState.ts +63 -0
- package/packages/ui/src/logo/FLogo.vue +52 -0
- package/packages/ui/src/logo/FSubLogo.vue +214 -0
- package/packages/ui/src/logo/index.ts +8 -0
- package/packages/ui/src/map/FMap.vue +355 -0
- package/packages/ui/src/map/FMapCompass.vue +154 -0
- package/packages/ui/src/map/FMapCoordinates.vue +134 -0
- package/packages/ui/src/map/FMapDrawTools.vue +116 -0
- package/packages/ui/src/map/FMapFullscreen.vue +92 -0
- package/packages/ui/src/map/FMapGeolocate.vue +208 -0
- package/packages/ui/src/map/FMapLayers.vue +75 -0
- package/packages/ui/src/map/FMapLegend.vue +45 -0
- package/packages/ui/src/map/FMapPopup.vue +129 -0
- package/packages/ui/src/map/FMapRamp.vue +69 -0
- package/packages/ui/src/map/FMapRegion.vue +77 -0
- package/packages/ui/src/map/FMapScale.vue +107 -0
- package/packages/ui/src/map/FMapSearch.vue +362 -0
- package/packages/ui/src/map/FMapTilt.vue +87 -0
- package/packages/ui/src/map/FMapTools.vue +61 -0
- package/packages/ui/src/map/FMapZoom.vue +81 -0
- package/packages/ui/src/map/FMarker.vue +218 -0
- package/packages/ui/src/map/FTrackScrubber.vue +94 -0
- package/packages/ui/src/map/FVehicleLegend.vue +72 -0
- package/packages/ui/src/map/FVehicleMark.vue +166 -0
- package/packages/ui/src/map/bin.ts +196 -0
- package/packages/ui/src/map/coordinates.ts +83 -0
- package/packages/ui/src/map/density.ts +401 -0
- package/packages/ui/src/map/draw.ts +356 -0
- package/packages/ui/src/map/ease.ts +67 -0
- package/packages/ui/src/map/fullscreen.ts +59 -0
- package/packages/ui/src/map/heat.ts +160 -0
- package/packages/ui/src/map/index.ts +351 -0
- package/packages/ui/src/map/liveMarkers.ts +60 -0
- package/packages/ui/src/map/map.css +116 -0
- package/packages/ui/src/map/mapColor.ts +31 -0
- package/packages/ui/src/map/mapContext.ts +12 -0
- package/packages/ui/src/map/mapLayers.ts +55 -0
- package/packages/ui/src/map/mapLegend.ts +70 -0
- package/packages/ui/src/map/mapSearch.theme.ts +79 -0
- package/packages/ui/src/map/mapSearch.ts +279 -0
- package/packages/ui/src/map/mapUrl.ts +117 -0
- package/packages/ui/src/map/markerAsset.ts +3 -0
- package/packages/ui/src/map/padding.ts +39 -0
- package/packages/ui/src/map/place.ts +192 -0
- package/packages/ui/src/map/ramp.ts +95 -0
- package/packages/ui/src/map/regionSelect.ts +163 -0
- package/packages/ui/src/map/route.ts +167 -0
- package/packages/ui/src/map/scale.ts +329 -0
- package/packages/ui/src/map/scaleBar.ts +67 -0
- package/packages/ui/src/map/selectionModel.ts +50 -0
- package/packages/ui/src/map/sources/coordinates.ts +58 -0
- package/packages/ui/src/map/sources/mapboxPlaces.ts +71 -0
- package/packages/ui/src/map/style.ts +14 -0
- package/packages/ui/src/map/track.ts +279 -0
- package/packages/ui/src/map/useFeatureSelect.ts +230 -0
- package/packages/ui/src/map/useLiveMarkers.ts +84 -0
- package/packages/ui/src/map/useMap.ts +150 -0
- package/packages/ui/src/map/useMapDraw.ts +776 -0
- package/packages/ui/src/map/useMapOverlay.ts +421 -0
- package/packages/ui/src/map/useMapTableSelection.ts +101 -0
- package/packages/ui/src/map/useMapUrlState.ts +155 -0
- package/packages/ui/src/map/useMarkerSource.ts +163 -0
- package/packages/ui/src/map/useRegionSelect.ts +309 -0
- package/packages/ui/src/map/useTokenMode.ts +61 -0
- package/packages/ui/src/map/useTrackPlayback.ts +131 -0
- package/packages/ui/src/map/useVehicleFilter.ts +51 -0
- package/packages/ui/src/map/useVisibleFeatures.ts +124 -0
- package/packages/ui/src/map/vehicle.ts +807 -0
- package/packages/ui/src/map/vehicleFilter.ts +284 -0
- package/packages/ui/src/map/vehicleLegend.ts +155 -0
- package/packages/ui/src/map/visibleFeatures.ts +192 -0
- package/packages/ui/src/map/zone.ts +291 -0
- package/packages/ui/src/shell/FAppNavbar.vue +68 -0
- package/packages/ui/src/shell/FAppShell.vue +163 -0
- package/packages/ui/src/shell/FAppSidebar.vue +215 -0
- package/packages/ui/src/shell/FPageToolbar.vue +126 -0
- package/packages/ui/src/shell/FScrollShadow.vue +43 -0
- package/packages/ui/src/shell/FSidePanel.vue +258 -0
- package/packages/ui/src/shell/FToolbarActions.vue +32 -0
- package/packages/ui/src/shell/index.ts +33 -0
- package/packages/ui/src/shell/nav.ts +37 -0
- package/packages/ui/src/shell/pageToolbar.ts +69 -0
- package/packages/ui/src/shell/shell.ts +74 -0
- package/packages/ui/src/shell/sidePanel.ts +115 -0
- package/packages/ui/src/themes/accordion.ts +36 -0
- package/packages/ui/src/themes/alert.ts +16 -0
- package/packages/ui/src/themes/auth-form.ts +19 -0
- package/packages/ui/src/themes/avatar-group.ts +9 -0
- package/packages/ui/src/themes/avatar.ts +18 -0
- package/packages/ui/src/themes/badge.ts +51 -0
- package/packages/ui/src/themes/banner.ts +21 -0
- package/packages/ui/src/themes/breadcrumb.ts +10 -0
- package/packages/ui/src/themes/button.mechanics.ts +191 -0
- package/packages/ui/src/themes/button.ts +136 -0
- package/packages/ui/src/themes/calendar.ts +29 -0
- package/packages/ui/src/themes/card.ts +67 -0
- package/packages/ui/src/themes/carousel.ts +41 -0
- package/packages/ui/src/themes/checkbox-group.ts +20 -0
- package/packages/ui/src/themes/checkbox.ts +47 -0
- package/packages/ui/src/themes/chip.ts +50 -0
- package/packages/ui/src/themes/collapsible.ts +26 -0
- package/packages/ui/src/themes/color-picker.ts +13 -0
- package/packages/ui/src/themes/command-palette.ts +37 -0
- package/packages/ui/src/themes/container.ts +14 -0
- package/packages/ui/src/themes/context-menu.ts +38 -0
- package/packages/ui/src/themes/dashboard-group.ts +11 -0
- package/packages/ui/src/themes/dashboard-navbar.ts +10 -0
- package/packages/ui/src/themes/dashboard-panel.ts +18 -0
- package/packages/ui/src/themes/dashboard-resize-handle.ts +35 -0
- package/packages/ui/src/themes/dashboard-sidebar-collapse.ts +7 -0
- package/packages/ui/src/themes/dashboard-sidebar-toggle.ts +7 -0
- package/packages/ui/src/themes/dashboard-sidebar.ts +58 -0
- package/packages/ui/src/themes/dashboard-toolbar.ts +15 -0
- package/packages/ui/src/themes/drawer.ts +45 -0
- package/packages/ui/src/themes/dropdown-menu.ts +38 -0
- package/packages/ui/src/themes/empty.ts +43 -0
- package/packages/ui/src/themes/field-group.ts +52 -0
- package/packages/ui/src/themes/file-upload.ts +38 -0
- package/packages/ui/src/themes/footer-columns.ts +13 -0
- package/packages/ui/src/themes/footer.ts +9 -0
- package/packages/ui/src/themes/form-field.ts +25 -0
- package/packages/ui/src/themes/form.ts +22 -0
- package/packages/ui/src/themes/header.ts +12 -0
- package/packages/ui/src/themes/icon.ts +61 -0
- package/packages/ui/src/themes/index.ts +99 -0
- package/packages/ui/src/themes/input-date.ts +73 -0
- package/packages/ui/src/themes/input-menu.ts +66 -0
- package/packages/ui/src/themes/input-number.ts +58 -0
- package/packages/ui/src/themes/input-rating.ts +25 -0
- package/packages/ui/src/themes/input-tags.ts +63 -0
- package/packages/ui/src/themes/input-time.ts +70 -0
- package/packages/ui/src/themes/input.ts +148 -0
- package/packages/ui/src/themes/kbd.ts +27 -0
- package/packages/ui/src/themes/link.ts +27 -0
- package/packages/ui/src/themes/listbox.ts +48 -0
- package/packages/ui/src/themes/main.ts +6 -0
- package/packages/ui/src/themes/marquee.ts +45 -0
- package/packages/ui/src/themes/modal.ts +46 -0
- package/packages/ui/src/themes/navigation-menu.ts +80 -0
- package/packages/ui/src/themes/page-anchors.ts +7 -0
- package/packages/ui/src/themes/page-aside.ts +7 -0
- package/packages/ui/src/themes/page-card.ts +22 -0
- package/packages/ui/src/themes/page-cta.ts +18 -0
- package/packages/ui/src/themes/page-feature.ts +13 -0
- package/packages/ui/src/themes/page-header.ts +16 -0
- package/packages/ui/src/themes/page-hero.ts +15 -0
- package/packages/ui/src/themes/page-links.ts +12 -0
- package/packages/ui/src/themes/page-section.ts +14 -0
- package/packages/ui/src/themes/page.ts +7 -0
- package/packages/ui/src/themes/pagination.ts +17 -0
- package/packages/ui/src/themes/pin-input.ts +73 -0
- package/packages/ui/src/themes/popover.ts +30 -0
- package/packages/ui/src/themes/progress.ts +29 -0
- package/packages/ui/src/themes/prose.ts +45 -0
- package/packages/ui/src/themes/radio-group.ts +39 -0
- package/packages/ui/src/themes/scroll-area.ts +23 -0
- package/packages/ui/src/themes/select-menu.ts +71 -0
- package/packages/ui/src/themes/select.ts +72 -0
- package/packages/ui/src/themes/separator.ts +28 -0
- package/packages/ui/src/themes/sidebar.ts +127 -0
- package/packages/ui/src/themes/skeleton.ts +15 -0
- package/packages/ui/src/themes/slideover.ts +87 -0
- package/packages/ui/src/themes/slider.ts +48 -0
- package/packages/ui/src/themes/stepper.ts +41 -0
- package/packages/ui/src/themes/switch.ts +63 -0
- package/packages/ui/src/themes/table.ts +34 -0
- package/packages/ui/src/themes/tabs.ts +74 -0
- package/packages/ui/src/themes/textarea.ts +77 -0
- package/packages/ui/src/themes/timeline.ts +25 -0
- package/packages/ui/src/themes/toast.ts +29 -0
- package/packages/ui/src/themes/toaster.ts +66 -0
- package/packages/ui/src/themes/tooltip.ts +31 -0
- package/packages/ui/src/themes/tree.ts +41 -0
- package/packages/ui/src/themes/user.ts +28 -0
- package/styles.cdn.gen.css +2047 -0
- package/theme.gen.json +1 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<script setup lang="ts" generic="D extends Record<string, unknown>">
|
|
2
|
+
// A row of segments, one per bucket, each carrying a state or an intensity. One asset per track:
|
|
3
|
+
// a dock's health across ninety days, a bike's week. Colour resolution is useCellScale's; this
|
|
4
|
+
// component is layout, labels and the interaction surface.
|
|
5
|
+
//
|
|
6
|
+
// Plain DOM rather than SVG — the segments are a flex row, so the browser does the layout, and an
|
|
7
|
+
// interactive segment can be a real <button> that focuses and announces itself.
|
|
8
|
+
import { computed, ref } from 'vue';
|
|
9
|
+
import type { BulletLegendItemInterface } from '@unovis/ts';
|
|
10
|
+
import { useCellScale, CELL_EMPTY_COLOR, type SequentialRamp } from './useCellScale';
|
|
11
|
+
import { CELL_SIZES, orElse, type CellSize } from './cellSize';
|
|
12
|
+
import FCellLegend from './FCellLegend.vue';
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(
|
|
15
|
+
defineProps<{
|
|
16
|
+
data: D[];
|
|
17
|
+
/** `state` colours by category, `intensity` buckets a number onto a sequential ramp. */
|
|
18
|
+
mode?: 'state' | 'intensity';
|
|
19
|
+
/** State mode: same map shape as `categories` elsewhere. */
|
|
20
|
+
states?: Record<string, BulletLegendItemInterface>;
|
|
21
|
+
/** State accessor. Returning null/undefined makes the segment empty. */
|
|
22
|
+
state?: (d: D) => string | null | undefined;
|
|
23
|
+
/** Intensity accessor. A non-number makes the segment empty. */
|
|
24
|
+
value?: (d: D) => number | null | undefined;
|
|
25
|
+
ramp?: SequentialRamp;
|
|
26
|
+
/** Intensity domain. Set it explicitly when two tracks are read against each other. */
|
|
27
|
+
domain?: [number, number];
|
|
28
|
+
steps?: number;
|
|
29
|
+
/** Leading gutter label — the asset this track is about. */
|
|
30
|
+
label?: string;
|
|
31
|
+
/** Size step. Sets the track height and segment spacing, and whether the label, captions,
|
|
32
|
+
* legend and tooltips default on: xs and sm are too small to carry chrome, so it defaults off
|
|
33
|
+
* there. Any explicit prop wins. */
|
|
34
|
+
size?: CellSize;
|
|
35
|
+
/** Forces the size step's chrome decision either way — label, captions, summary, legend and
|
|
36
|
+
* tooltips together. `legend` still wins over it for the legend alone. */
|
|
37
|
+
chrome?: boolean;
|
|
38
|
+
height?: number;
|
|
39
|
+
segmentGap?: number;
|
|
40
|
+
radius?: number;
|
|
41
|
+
legend?: boolean;
|
|
42
|
+
/** Start and end captions under the track, e.g. ['90 days ago', 'Today']. */
|
|
43
|
+
caption?: [string, string];
|
|
44
|
+
/** Hover and focus text. Given one, segments become focusable buttons and get the house bubble.
|
|
45
|
+
* Suppressed at xs and sm, where a segment is too thin to hit reliably. */
|
|
46
|
+
tooltip?: (d: D, i: number) => string;
|
|
47
|
+
/** Sentence for assistive tech describing the whole track. */
|
|
48
|
+
ariaLabel?: string;
|
|
49
|
+
}>(),
|
|
50
|
+
{
|
|
51
|
+
mode: 'state',
|
|
52
|
+
size: 'md',
|
|
53
|
+
steps: 5,
|
|
54
|
+
/* Vue casts an ABSENT Boolean-typed prop to false, so without these the size step could never
|
|
55
|
+
switch chrome on: `orElse` would read that false as a deliberate "off" at every size. */
|
|
56
|
+
legend: undefined,
|
|
57
|
+
chrome: undefined,
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Three tiers, narrowest first: an individual prop beats `chrome`, which beats the size step.
|
|
62
|
+
const spec = computed(() => CELL_SIZES[props.size]);
|
|
63
|
+
const height = computed(() => orElse(props.height, spec.value.trackHeight));
|
|
64
|
+
const segmentGap = computed(() => orElse(props.segmentGap, spec.value.trackGap));
|
|
65
|
+
const radius = computed(() => orElse(props.radius, spec.value.radius));
|
|
66
|
+
const showChrome = computed(() => orElse(props.chrome, spec.value.chrome));
|
|
67
|
+
const showLegend = computed(() => orElse(props.legend, showChrome.value));
|
|
68
|
+
const interactive = computed(() => !!props.tooltip && showChrome.value);
|
|
69
|
+
|
|
70
|
+
const scale = useCellScale<D>({
|
|
71
|
+
mode: props.mode,
|
|
72
|
+
states: computed(() => props.states),
|
|
73
|
+
ramp: computed(() => props.ramp ?? ('forest' as SequentialRamp)),
|
|
74
|
+
domain: computed(() => props.domain),
|
|
75
|
+
steps: computed(() => props.steps),
|
|
76
|
+
data: computed(() => props.data),
|
|
77
|
+
value: props.value,
|
|
78
|
+
state: props.state,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// The label an assistive reader gets per segment. Without a tooltip the segments are decorative and
|
|
82
|
+
// the track's own label carries the meaning, so this is only consulted when one is given.
|
|
83
|
+
const labelFor = (d: D, i: number) => props.tooltip?.(d, i) ?? '';
|
|
84
|
+
|
|
85
|
+
const trackLabel = computed(
|
|
86
|
+
() => props.ariaLabel ?? `${props.label ? `${props.label}: ` : ''}${props.data.length} segments`,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// One UTooltip for the track, re-anchored to the segment under the pointer via `reference`, rather
|
|
90
|
+
// than one per segment: a ninety-day track costs a single tooltip root either way.
|
|
91
|
+
//
|
|
92
|
+
// `tipOpen` owns visibility; the text and the anchor are left alone on close. Clearing them
|
|
93
|
+
// together with the flag emptied the bubble mid-exit-animation and dropped the anchor back to the
|
|
94
|
+
// trigger, so leaving a segment flashed a blank pill at the container's centre for ~80ms.
|
|
95
|
+
//
|
|
96
|
+
// Anchored to the segment rather than the pointer, so a keyboard focus places it too.
|
|
97
|
+
const tip = ref<{ text: string; el: Element | null }>({ text: '', el: null });
|
|
98
|
+
const tipOpen = ref(false);
|
|
99
|
+
const showTip = (event: Event, text: string) => {
|
|
100
|
+
tip.value = { text, el: event.currentTarget as Element };
|
|
101
|
+
tipOpen.value = true;
|
|
102
|
+
};
|
|
103
|
+
const hideTip = () => {
|
|
104
|
+
tipOpen.value = false;
|
|
105
|
+
};
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<template>
|
|
109
|
+
<div class="flex w-full flex-col gap-2">
|
|
110
|
+
<div class="flex items-center gap-3">
|
|
111
|
+
<span v-if="label && showChrome" class="type-label shrink-0 text-default">{{ label }}</span>
|
|
112
|
+
<UTooltip :text="tip.text" :reference="tip.el" :open="tipOpen">
|
|
113
|
+
<div
|
|
114
|
+
class="flex min-w-0 flex-1"
|
|
115
|
+
:role="interactive ? 'group' : 'img'"
|
|
116
|
+
:aria-label="trackLabel"
|
|
117
|
+
:style="{ gap: `${segmentGap}px`, height: `${height}px` }"
|
|
118
|
+
>
|
|
119
|
+
<component
|
|
120
|
+
:is="interactive ? 'button' : 'div'"
|
|
121
|
+
v-for="(d, i) in data"
|
|
122
|
+
:key="i"
|
|
123
|
+
:type="interactive ? 'button' : undefined"
|
|
124
|
+
class="min-w-0 flex-1 transition-[filter] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]"
|
|
125
|
+
:class="interactive ? 'cursor-pointer hover:brightness-110' : ''"
|
|
126
|
+
:style="{ background: scale.colorOf(d), borderRadius: `${radius}px` }"
|
|
127
|
+
:aria-label="interactive ? labelFor(d, i) : undefined"
|
|
128
|
+
:aria-hidden="interactive ? undefined : 'true'"
|
|
129
|
+
@mouseenter="interactive && showTip($event, labelFor(d, i))"
|
|
130
|
+
@mouseleave="hideTip"
|
|
131
|
+
@focus="interactive && showTip($event, labelFor(d, i))"
|
|
132
|
+
@blur="hideTip"
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
</UTooltip>
|
|
136
|
+
<span v-if="$slots.summary && showChrome" class="type-label shrink-0 text-muted"><slot name="summary" /></span>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div v-if="caption && showChrome" class="flex justify-between">
|
|
140
|
+
<span class="type-caption text-dimmed">{{ caption[0] }}</span>
|
|
141
|
+
<span class="type-caption text-dimmed">{{ caption[1] }}</span>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<FCellLegend
|
|
145
|
+
v-if="showLegend"
|
|
146
|
+
:legend="scale.legend.value"
|
|
147
|
+
:show-empty="data.some((d) => scale.isEmpty(d))"
|
|
148
|
+
:empty-color="CELL_EMPTY_COLOR"
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
</template>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Size scale for the cell components, and the chrome that comes with each step.
|
|
2
|
+
//
|
|
3
|
+
// A size is not only a cell dimension: below a certain size the furniture stops being legible and
|
|
4
|
+
// starts being noise. A 6px cell cannot carry a weekday label beside it, a legend under a 12px
|
|
5
|
+
// track is taller than the track, and a tooltip on a 6px target is a coin toss. So each step
|
|
6
|
+
// declares whether chrome is on, and the components read that as the DEFAULT — an explicit prop
|
|
7
|
+
// always wins, because a caller who asks for a legend at xs has a reason.
|
|
8
|
+
export type CellSize = 'xs' | 'sm' | 'md' | 'lg';
|
|
9
|
+
|
|
10
|
+
export interface CellSizeSpec {
|
|
11
|
+
/** Calendar cell edge, px. */
|
|
12
|
+
cell: number;
|
|
13
|
+
/** Space between cells, px. Also the calendar's month-rule bed. */
|
|
14
|
+
gap: number;
|
|
15
|
+
/** Corner radius, px. */
|
|
16
|
+
radius: number;
|
|
17
|
+
/** Status track height, px. */
|
|
18
|
+
trackHeight: number;
|
|
19
|
+
/** Space between track segments, px. */
|
|
20
|
+
trackGap: number;
|
|
21
|
+
/** Dotted map dot diameter, px. */
|
|
22
|
+
dot: number;
|
|
23
|
+
/** Space between dots, px. */
|
|
24
|
+
dotGap: number;
|
|
25
|
+
/** Labels, legend and tooltips default on at this size. */
|
|
26
|
+
chrome: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The dot steps are their own numbers rather than the cell ones. A map is ninety-six dots across
|
|
30
|
+
// where a calendar is fifty-two weeks and a track is one row, so reusing `cell` would make the
|
|
31
|
+
// smallest map wider than the largest calendar. What the steps share is the chrome decision, which
|
|
32
|
+
// is the part that has to agree across the family.
|
|
33
|
+
export const CELL_SIZES: Record<CellSize, CellSizeSpec> = {
|
|
34
|
+
// FSparkline scale: a texture, read at a glance inside a table cell or a KPI tile.
|
|
35
|
+
xs: { cell: 6, gap: 1, radius: 1, trackHeight: 12, trackGap: 1, dot: 2, dotGap: 0, chrome: false },
|
|
36
|
+
// Compact: still a picture rather than a chart. Chrome off — an 8px cell leaves no room beside it.
|
|
37
|
+
sm: { cell: 8, gap: 2, radius: 2, trackHeight: 20, trackGap: 2, dot: 3, dotGap: 1, chrome: false },
|
|
38
|
+
// The documented default, and the size the guidance is written for.
|
|
39
|
+
md: { cell: 11, gap: 3, radius: 2, trackHeight: 32, trackGap: 2, dot: 5, dotGap: 1, chrome: true },
|
|
40
|
+
// Presentation scale: a chart that owns its section.
|
|
41
|
+
lg: { cell: 14, gap: 4, radius: 3, trackHeight: 44, trackGap: 3, dot: 7, dotGap: 1, chrome: true },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** `explicit ?? fallback`, written out because `||` would swallow a deliberate 0. */
|
|
45
|
+
export const orElse = <T>(explicit: T | undefined, fallback: T): T =>
|
|
46
|
+
explicit === undefined ? fallback : explicit;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Share maths and track geometry for FDistributionBar, kept out of the SFC so both can be tested
|
|
2
|
+
// without a DOM.
|
|
3
|
+
|
|
4
|
+
export interface DistributionSegment {
|
|
5
|
+
label: string;
|
|
6
|
+
value: number;
|
|
7
|
+
/** Any CSS colour. Defaults to the palette slot for the segment's position. */
|
|
8
|
+
color?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DistributionShare {
|
|
12
|
+
segment: DistributionSegment;
|
|
13
|
+
/** Position in the original `data`, so the palette slot survives filtering. */
|
|
14
|
+
index: number;
|
|
15
|
+
/** 0–1 of the total. */
|
|
16
|
+
share: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DistributionShares {
|
|
20
|
+
parts: DistributionShare[];
|
|
21
|
+
/** 0–1 of the total that no segment accounts for. Zero unless `total` exceeds the values' sum. */
|
|
22
|
+
remainder: number;
|
|
23
|
+
total: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Shares of the whole.
|
|
28
|
+
*
|
|
29
|
+
* `total` defaults to the values' sum, which makes the bar a pure composition — the parts fill it.
|
|
30
|
+
* Passing a larger total leaves a remainder, which makes it a figure measured against its ceiling:
|
|
31
|
+
* 78% of capacity, split by what used it.
|
|
32
|
+
*
|
|
33
|
+
* A negative value is read as zero. A bar cannot draw a negative width, and folding one into the
|
|
34
|
+
* total would shrink every other segment to pay for it — the figure would be wrong rather than
|
|
35
|
+
* merely missing. A total smaller than the sum is raised to the sum for the same reason: the
|
|
36
|
+
* alternative is shares over 100% and a bar that overflows its own track.
|
|
37
|
+
*/
|
|
38
|
+
export function distributionShares(
|
|
39
|
+
data: readonly DistributionSegment[],
|
|
40
|
+
total?: number,
|
|
41
|
+
): DistributionShares {
|
|
42
|
+
const parts = data.map((segment, index) => ({
|
|
43
|
+
segment,
|
|
44
|
+
index,
|
|
45
|
+
value: Math.max(0, segment.value) || 0,
|
|
46
|
+
}));
|
|
47
|
+
const sum = parts.reduce((n, p) => n + p.value, 0);
|
|
48
|
+
const whole = Math.max(sum, total ?? 0);
|
|
49
|
+
|
|
50
|
+
// No whole means no shares to state. Returning zeros rather than dividing keeps NaN out of the
|
|
51
|
+
// grid template, where it would collapse every track instead of rendering an empty bar.
|
|
52
|
+
if (whole <= 0) {
|
|
53
|
+
return { parts: parts.map((p) => ({ segment: p.segment, index: p.index, share: 0 })), remainder: 0, total: 0 };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
parts: parts.map((p) => ({ segment: p.segment, index: p.index, share: p.value / whole })),
|
|
58
|
+
remainder: (whole - sum) / whole,
|
|
59
|
+
total: whole,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* One track's `border-radius`.
|
|
65
|
+
*
|
|
66
|
+
* Split (the default), every track is its own pill — the gap is what separates them, and the round
|
|
67
|
+
* ends are what make each one read as a separate object.
|
|
68
|
+
*
|
|
69
|
+
* Joined, the gap is gone, so pills would butt into each other and pinch at every seam. The bar has
|
|
70
|
+
* to be shaped as ONE object instead: the outside corners of the whole bar are round and every
|
|
71
|
+
* internal seam is square. A single track is both ends at once, so it stays a full pill.
|
|
72
|
+
*
|
|
73
|
+
* `count` is the number of tracks actually drawn — zero-share segments are filtered out before this
|
|
74
|
+
* and the remainder, when there is one, counts as the last track. So `index` is a position among
|
|
75
|
+
* drawn tracks, NOT an index into `data`.
|
|
76
|
+
*
|
|
77
|
+
* Left-to-right, matching the drawing order the component documents; a right-to-left bar would need
|
|
78
|
+
* the logical corner properties instead of this shorthand.
|
|
79
|
+
*/
|
|
80
|
+
export function segmentRadius(index: number, count: number, radius: number, joined: boolean): string {
|
|
81
|
+
const r = `${radius}px`;
|
|
82
|
+
if (!joined) return r;
|
|
83
|
+
const first = index === 0;
|
|
84
|
+
const last = index === count - 1;
|
|
85
|
+
if (first && last) return r;
|
|
86
|
+
if (first) return `${r} 0 0 ${r}`;
|
|
87
|
+
if (last) return `0 ${r} ${r} 0`;
|
|
88
|
+
return '0';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A share as a percentage. Whole numbers: at the width this component is read at, a decimal place
|
|
93
|
+
* is precision the bar itself cannot show. A non-zero share that would round to nothing is stated
|
|
94
|
+
* as "<1%" rather than "0%", which would read as a category that isn't there.
|
|
95
|
+
*/
|
|
96
|
+
export function formatShare(share: number, locale = 'en-GB'): string {
|
|
97
|
+
if (share > 0 && share < 0.005) return `<1%`;
|
|
98
|
+
return new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 0 }).format(share);
|
|
99
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Palette data seam. The colour engine is canonical at packages/tokens/src/colourEngine.ts — the
|
|
2
|
+
// package whose CSS and JSON it generates — and is reached here in exactly one place, the same way
|
|
3
|
+
// scripts/dataviz.ts reaches it at build time. Everything else in this package goes through this
|
|
4
|
+
// re-export, so a future move of the engine touches one line.
|
|
5
|
+
//
|
|
6
|
+
// Relative, not `@humanforest/tokens/colourEngine`: the Nuxt layer mirrors these trees at
|
|
7
|
+
// repo-relative paths and never rewrites their imports, and a bare specifier would need tokens
|
|
8
|
+
// resolvable from the layer — which it is not, since nothing @humanforest/* is published yet.
|
|
9
|
+
export {
|
|
10
|
+
DATAVIZ,
|
|
11
|
+
DATAVIZ_PAIRS,
|
|
12
|
+
DATAVIZ_CATEGORICAL,
|
|
13
|
+
DATAVIZ_SEQUENTIAL,
|
|
14
|
+
DATAVIZ_DIVERGING,
|
|
15
|
+
} from '@humanforest/tokens/colourEngine';
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Crosshair/tooltip template builder. Unovis crosshair and tooltip config take a function that
|
|
2
|
+
// returns an HTML STRING, so this is code, not config. The markup is the house tooltip — a title
|
|
3
|
+
// row, then a swatch/label/value grid with tabular numerals — styled through the --vis-tooltip-*
|
|
4
|
+
// custom properties the adapter themes, so nothing here hardcodes a colour.
|
|
5
|
+
import type { BulletLegendItemInterface } from '@unovis/ts';
|
|
6
|
+
|
|
7
|
+
// Every interpolated string passes through here: the output is innerHTML on the tooltip element,
|
|
8
|
+
// and titles, series names and formatted values are arbitrary text.
|
|
9
|
+
const esc = (value: unknown): string =>
|
|
10
|
+
String(value)
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
.replace(/'/g, ''');
|
|
16
|
+
|
|
17
|
+
export interface ForestTooltipOptions<Datum> {
|
|
18
|
+
/** Title text for the hovered row. Default: the datum's first property value. */
|
|
19
|
+
titleFormatter?: (d: Datum) => string | number;
|
|
20
|
+
/** Formats each series value. Default: the raw value, stringified. */
|
|
21
|
+
valueFormatter?: (value: number) => string | number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const TITLE_STYLE = (hasRows: boolean) =>
|
|
25
|
+
'color:var(--vis-tooltip-title-color,#000);' +
|
|
26
|
+
'text-transform:var(--vis-tooltip-title-text-transform,capitalize);' +
|
|
27
|
+
`border-bottom:${hasRows ? 'var(--vis-tooltip-title-border-bottom,1px solid #e5e7eb)' : 'none'};` +
|
|
28
|
+
'padding:var(--vis-tooltip-title-padding,0.75rem 0.75rem 0.5rem 0.75rem);' +
|
|
29
|
+
`margin:${hasRows ? 'var(--vis-tooltip-title-margin,0 0 0.25rem 0)' : '0'};` +
|
|
30
|
+
'font-size:var(--vis-tooltip-title-font-size,0.875rem);' +
|
|
31
|
+
'line-height:var(--vis-tooltip-title-line-height,100%);' +
|
|
32
|
+
'font-weight:var(--vis-tooltip-title-font-weight,600)';
|
|
33
|
+
|
|
34
|
+
const GRID_STYLE =
|
|
35
|
+
'display:grid;grid-template-columns:auto 1fr auto;align-items:center;' +
|
|
36
|
+
'gap:var(--vis-tooltip-content-gap,0.25rem 0.5rem);' +
|
|
37
|
+
'padding:var(--vis-tooltip-content-padding,0 0.75rem 0.5rem 0.75rem)';
|
|
38
|
+
|
|
39
|
+
const DOT_STYLE =
|
|
40
|
+
'width:8px;height:8px;aspect-ratio:1;flex-shrink:0;' +
|
|
41
|
+
'border-radius:var(--vis-tooltip-dot-border-radius,4px);' +
|
|
42
|
+
'margin:var(--vis-tooltip-dot-margin,0);';
|
|
43
|
+
|
|
44
|
+
const LABEL_STYLE =
|
|
45
|
+
'font-weight:var(--vis-tooltip-label-font-weight,400);' +
|
|
46
|
+
'font-size:var(--vis-tooltip-label-font-size,0.875rem);' +
|
|
47
|
+
'color:var(--vis-tooltip-label-color,inherit);' +
|
|
48
|
+
'margin:var(--vis-tooltip-label-margin,0 1rem 0 0);white-space:nowrap';
|
|
49
|
+
|
|
50
|
+
const VALUE_STYLE =
|
|
51
|
+
'font-size:var(--vis-tooltip-value-font-size,0.875rem);' +
|
|
52
|
+
'font-weight:var(--vis-tooltip-value-font-weight,600);' +
|
|
53
|
+
'color:var(--vis-tooltip-value-color,inherit);' +
|
|
54
|
+
'text-align:right;font-variant-numeric:tabular-nums';
|
|
55
|
+
|
|
56
|
+
export interface ForestTipRow {
|
|
57
|
+
/** Swatch dot colour (any CSS colour string). Omit for a row without a dot. */
|
|
58
|
+
swatch?: string;
|
|
59
|
+
label: string;
|
|
60
|
+
value: string | number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The house tooltip chrome — title bar plus swatch/label/value grid — as a plain string builder,
|
|
65
|
+
* for bespoke templates (Donut/Bubble/Gantt/Treemap `triggers`, custom crosshairs) that need the
|
|
66
|
+
* house look without the categories-map plumbing. The chrome carries its own padding: the adapter
|
|
67
|
+
* zeroes --vis-tooltip-padding on the container, so a template built outside this frame renders
|
|
68
|
+
* cramped against the tooltip edge.
|
|
69
|
+
*/
|
|
70
|
+
export function forestTipFrame(title: string | number, rows: ForestTipRow[] = []): string {
|
|
71
|
+
const body = rows
|
|
72
|
+
.map((r) =>
|
|
73
|
+
`<span style="${DOT_STYLE}${r.swatch ? `background-color:${esc(r.swatch)}` : 'visibility:hidden'}"></span>` +
|
|
74
|
+
`<span style="${LABEL_STYLE}">${esc(r.label)}</span>` +
|
|
75
|
+
`<span style="${VALUE_STYLE}">${esc(r.value)}</span>`,
|
|
76
|
+
)
|
|
77
|
+
.join('');
|
|
78
|
+
return (
|
|
79
|
+
'<div style="display:flex;flex-direction:column">' +
|
|
80
|
+
`<div style="${TITLE_STYLE(rows.length > 0)}">${esc(title)}</div>` +
|
|
81
|
+
(rows.length > 0 ? `<div style="${GRID_STYLE}">${body}</div>` : '') +
|
|
82
|
+
'</div>'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Builds the template function for `<VisCrosshair :template>` (or tooltip `triggers`). Rows are
|
|
88
|
+
* the category keys present on the hovered datum, in category order; a series without a string
|
|
89
|
+
* colour wears the adapter's --vis-colorN slot, mirroring the legend.
|
|
90
|
+
*/
|
|
91
|
+
export function forestTooltip<Datum extends Record<string, unknown>>(
|
|
92
|
+
categories: Record<string, BulletLegendItemInterface>,
|
|
93
|
+
opts: ForestTooltipOptions<Datum> = {},
|
|
94
|
+
): (d: Datum) => string {
|
|
95
|
+
const keys = Object.keys(categories);
|
|
96
|
+
return (d: Datum): string => {
|
|
97
|
+
const present = keys.filter((k) => k in d);
|
|
98
|
+
const title = opts.titleFormatter ? opts.titleFormatter(d) : Object.values(d)[0];
|
|
99
|
+
const rows: ForestTipRow[] = present.map((k, i) => {
|
|
100
|
+
const item = categories[k];
|
|
101
|
+
const raw = d[k];
|
|
102
|
+
return {
|
|
103
|
+
swatch: typeof item.color === 'string' ? item.color : `var(--vis-color${i})`,
|
|
104
|
+
label: String(item.name),
|
|
105
|
+
value: opts.valueFormatter ? opts.valueFormatter(Number(raw)) : String(raw),
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
return forestTipFrame(String(title), rows);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The derivation every direct-Unovis call site hand-writes: a `categories` map (as returned by
|
|
2
|
+
// useChartPalette's accessors) unpacked into the ordered arrays Unovis components actually take.
|
|
3
|
+
import type { BulletLegendItemInterface } from '@unovis/ts';
|
|
4
|
+
|
|
5
|
+
export interface ChartAccessors<Datum> {
|
|
6
|
+
/** Ordered series colours; a category without a string colour falls back to the adapter's --vis-colorN slot. */
|
|
7
|
+
colors: string[];
|
|
8
|
+
/** Ordered legend entries for `<VisBulletLegend :items>`; multi-colour series show their first colour. */
|
|
9
|
+
legendItems: BulletLegendItemInterface[];
|
|
10
|
+
/**
|
|
11
|
+
* One accessor per series key, in category order. Values pass through untouched — a null or
|
|
12
|
+
* undefined cell stays null/undefined (Unovis' NumericAccessor accepts both and renders the
|
|
13
|
+
* gap); coercing would plot missing data as zero.
|
|
14
|
+
*/
|
|
15
|
+
yAccessors: Array<(d: Datum) => number | null | undefined>;
|
|
16
|
+
/** X accessor: the row's `xKey` value when one is given, otherwise the array index —
|
|
17
|
+
* the house convention: rows are evenly spaced, the axis formats the label. */
|
|
18
|
+
xAccessor: (d: Datum, i: number) => number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Derives `{ colors, legendItems, yAccessors, xAccessor }` from a categories map.
|
|
23
|
+
* `data` is never read — pass it (or the datum type parameter) to anchor the accessors' typing.
|
|
24
|
+
* `xKey` names a numeric field (e.g. a timestamp) to plot on x instead of the row index.
|
|
25
|
+
*/
|
|
26
|
+
export function fromCategories<Datum extends Record<string, unknown>>(
|
|
27
|
+
categories: Record<string, BulletLegendItemInterface>,
|
|
28
|
+
data?: readonly Datum[],
|
|
29
|
+
xKey?: keyof Datum & string,
|
|
30
|
+
): ChartAccessors<Datum> {
|
|
31
|
+
void data;
|
|
32
|
+
const keys = Object.keys(categories);
|
|
33
|
+
const colorOf = (item: BulletLegendItemInterface, i: number): string =>
|
|
34
|
+
typeof item.color === 'string' ? item.color
|
|
35
|
+
: Array.isArray(item.color) && typeof item.color[0] === 'string' ? item.color[0]
|
|
36
|
+
: `var(--vis-color${i})`;
|
|
37
|
+
return {
|
|
38
|
+
colors: keys.map((k, i) => colorOf(categories[k], i)),
|
|
39
|
+
legendItems: keys.map((k, i) => ({ ...categories[k], color: colorOf(categories[k], i) })),
|
|
40
|
+
yAccessors: keys.map((k) => (d: Datum) => d[k] as number | null | undefined),
|
|
41
|
+
xAccessor: xKey ? (d) => d[xKey] as number : (_d, i) => i,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @humanforest/charts — the chart frames plus palette accessors and helpers for building charts
|
|
2
|
+
// directly on @unovis/vue. The frames wrap ceremony only (container, axes, legend,
|
|
3
|
+
// tooltip/crosshair, repaint, motion); marks stay real @unovis/vue components in the #mark slot.
|
|
4
|
+
// Importing this barrel needs an SFC pipeline (Vite/Nuxt) — everything here is browser-bound.
|
|
5
|
+
export { default as FChartFrame } from './FChartFrame.vue';
|
|
6
|
+
export { default as FChartFrameSingle } from './FChartFrameSingle.vue';
|
|
7
|
+
export { default as FChartLegend } from './FChartLegend.vue';
|
|
8
|
+
// Word-sized graphic for a KPI tile's `visual` slot: shape only, no axes, no tooltip.
|
|
9
|
+
export { default as FSparkline } from './FSparkline.vue';
|
|
10
|
+
// The other tile-sized graphic: one bar split into its parts, for what a figure is made of.
|
|
11
|
+
export { default as FDistributionBar } from './FDistributionBar.vue';
|
|
12
|
+
export {
|
|
13
|
+
distributionShares,
|
|
14
|
+
formatShare,
|
|
15
|
+
type DistributionSegment,
|
|
16
|
+
type DistributionShare,
|
|
17
|
+
type DistributionShares,
|
|
18
|
+
} from './distribution';
|
|
19
|
+
// Cell components — regular DOM on a grid, not Unovis marks. State or intensity per bucket.
|
|
20
|
+
export { default as FStatusTrack } from './FStatusTrack.vue';
|
|
21
|
+
export { default as FCalendarGrid } from './FCalendarGrid.vue';
|
|
22
|
+
export { default as FDottedMap } from './FDottedMap.vue';
|
|
23
|
+
export { default as FLondonMap } from './FLondonMap.vue';
|
|
24
|
+
// A single borough as a styleable graphic (currentColor glyph), not a data-filled chart.
|
|
25
|
+
export { default as FBoroughShape } from './FBoroughShape.vue';
|
|
26
|
+
export {
|
|
27
|
+
LONDON_GRID,
|
|
28
|
+
RIVER_COLOR,
|
|
29
|
+
loadLondonGrid,
|
|
30
|
+
dotAt,
|
|
31
|
+
dotKey,
|
|
32
|
+
type LondonGrid,
|
|
33
|
+
type DotDensity,
|
|
34
|
+
} from './londonGrid';
|
|
35
|
+
export {
|
|
36
|
+
LONDON_BOROUGHS,
|
|
37
|
+
loadLondonWards,
|
|
38
|
+
indexAreas,
|
|
39
|
+
type LondonArea,
|
|
40
|
+
type LondonAreaSet,
|
|
41
|
+
type AreaLevel,
|
|
42
|
+
} from './londonAreas';
|
|
43
|
+
export { default as FCellLegend } from './FCellLegend.vue';
|
|
44
|
+
export { CELL_SIZES, orElse, type CellSize, type CellSizeSpec } from './cellSize';
|
|
45
|
+
export {
|
|
46
|
+
useCellScale,
|
|
47
|
+
CELL_EMPTY_COLOR,
|
|
48
|
+
type CellScaleOptions,
|
|
49
|
+
type CellScale,
|
|
50
|
+
// The interface keeps its unprefixed name — the F prefix marks components, not types. It no
|
|
51
|
+
// longer collides with the component now that one is FCellLegend, but the public alias stays as
|
|
52
|
+
// it was so consumers do not have to change.
|
|
53
|
+
type CellLegend as CellLegendData,
|
|
54
|
+
type CellLegendEntry,
|
|
55
|
+
type SequentialRamp,
|
|
56
|
+
} from './useCellScale';
|
|
57
|
+
export { useThemeVersion } from './useThemeVersion';
|
|
58
|
+
export { resolveVar } from './resolveVar';
|
|
59
|
+
export {
|
|
60
|
+
useChartPalette,
|
|
61
|
+
safeVar,
|
|
62
|
+
vividVar,
|
|
63
|
+
pairsVar,
|
|
64
|
+
categoricalVar,
|
|
65
|
+
sequentialVar,
|
|
66
|
+
divergingVar,
|
|
67
|
+
type SeriesDef,
|
|
68
|
+
type CategoricalPalette,
|
|
69
|
+
} from './useChartPalette';
|
|
70
|
+
export {
|
|
71
|
+
axisX,
|
|
72
|
+
axisY,
|
|
73
|
+
legendDefaults,
|
|
74
|
+
tooltipDefaults,
|
|
75
|
+
houseTooltip,
|
|
76
|
+
tooltipContainer,
|
|
77
|
+
treemapDefaults,
|
|
78
|
+
nestedDonutDefaults,
|
|
79
|
+
} from './presets';
|
|
80
|
+
export { fromCategories, type ChartAccessors } from './fromCategories';
|
|
81
|
+
export { forestTooltip, forestTipFrame, type ForestTooltipOptions, type ForestTipRow } from './forestTooltip';
|
|
82
|
+
export { useChartRepaintKey } from './useChartRepaintKey';
|
|
83
|
+
export { motionDuration, type MotionToken } from './motionDuration';
|
|
84
|
+
// For a Vue app with no Nuxt UI. Five components wrap their marks in `<UTooltip>` — a name, not an
|
|
85
|
+
// import — and this answers that name with a trigger-only stand-in. Inert wherever Nuxt UI's own
|
|
86
|
+
// build plugin already resolved the tag, which is every app that has Nuxt UI.
|
|
87
|
+
export { chartsWithoutNuxtUI, TooltipStandIn } from './tooltipStandIn';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Geometry data seam for the vector maps, the same shape as engine.ts and londonGrid.ts: the
|
|
2
|
+
// generated paths are reached across the workspace boundary in exactly one place.
|
|
3
|
+
//
|
|
4
|
+
// Built by scripts/londonAreas.ts and not editable by hand. Both levels share one viewBox, which is
|
|
5
|
+
// what lets a ward map carry a borough outline over it.
|
|
6
|
+
import boroughs from '@humanforest/tokens/geo/london-boroughs.json';
|
|
7
|
+
|
|
8
|
+
export interface LondonArea {
|
|
9
|
+
/** ONS code — E09… for a borough, E05… for a ward. */
|
|
10
|
+
code: string;
|
|
11
|
+
name: string;
|
|
12
|
+
/** SVG path data, in viewBox units. */
|
|
13
|
+
d: string;
|
|
14
|
+
/** Label anchor: the area-weighted centroid of the area's largest ring. */
|
|
15
|
+
cx: number;
|
|
16
|
+
cy: number;
|
|
17
|
+
/**
|
|
18
|
+
* Bounding box of that ring, in viewBox units: origin (`x`, `y`) then size (`w`, `h`). `w`/`h`
|
|
19
|
+
* decide whether a label fits; the full box lets a map frame itself to one area, no path parsing.
|
|
20
|
+
*/
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
w: number;
|
|
24
|
+
h: number;
|
|
25
|
+
/** Wards only — the code of the borough containing this ward. */
|
|
26
|
+
borough?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface LondonAreaSet {
|
|
30
|
+
viewBox: [number, number, number, number];
|
|
31
|
+
bbox: [number, number, number, number];
|
|
32
|
+
areas: LondonArea[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type AreaLevel = 'borough' | 'ward';
|
|
36
|
+
|
|
37
|
+
export const LONDON_BOROUGHS = boroughs as unknown as LondonAreaSet;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The wards, fetched on first use.
|
|
41
|
+
*
|
|
42
|
+
* Wards are three times the weight of the boroughs and most maps never draw them, so they are a
|
|
43
|
+
* separate chunk rather than part of the barrel. The promise is cached, so concurrent callers and
|
|
44
|
+
* re-renders share one request.
|
|
45
|
+
*/
|
|
46
|
+
let wardsPromise: Promise<LondonAreaSet> | null = null;
|
|
47
|
+
export const loadLondonWards = (): Promise<LondonAreaSet> => {
|
|
48
|
+
wardsPromise ??= import('@humanforest/tokens/geo/london-wards.json').then(
|
|
49
|
+
(m) => (m.default ?? m) as unknown as LondonAreaSet,
|
|
50
|
+
);
|
|
51
|
+
return wardsPromise;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Look an area up by ONS code or by name, case-insensitively.
|
|
56
|
+
*
|
|
57
|
+
* Callers' data is keyed either way in practice — a warehouse table holds codes, a spreadsheet
|
|
58
|
+
* someone typed holds names — and requiring one of them would mean every caller writing the same
|
|
59
|
+
* lookup. Codes are checked first because they are unambiguous; two boroughs never share a name, but
|
|
60
|
+
* ward names repeat across boroughs, so a ward is only findable by name when that name is unique.
|
|
61
|
+
*/
|
|
62
|
+
export const indexAreas = (areas: LondonArea[]): Map<string, LondonArea> => {
|
|
63
|
+
const byKey = new Map<string, LondonArea>();
|
|
64
|
+
const nameCount = new Map<string, number>();
|
|
65
|
+
for (const a of areas) {
|
|
66
|
+
const n = a.name.toLowerCase();
|
|
67
|
+
nameCount.set(n, (nameCount.get(n) ?? 0) + 1);
|
|
68
|
+
}
|
|
69
|
+
for (const a of areas) {
|
|
70
|
+
byKey.set(a.code.toLowerCase(), a);
|
|
71
|
+
const n = a.name.toLowerCase();
|
|
72
|
+
if (nameCount.get(n) === 1) byKey.set(n, a);
|
|
73
|
+
}
|
|
74
|
+
return byKey;
|
|
75
|
+
};
|