@humanforest/charts 0.5.0 → 0.5.1
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/CHANGELOG.md +46 -0
- package/package.json +2 -2
- package/src/FChartFrame.vue +37 -27
- package/src/FChartFrameSingle.vue +19 -11
- package/src/frameHeight.ts +43 -0
- package/src/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
Every @humanforest package shares one version, so one entry covers all seven packages. Newest first.
|
|
4
4
|
|
|
5
|
+
## 0.5.1 — 2026-11-03
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **ui**: add FToggle and FToggleGroup
|
|
10
|
+
- **docs**: add screen options and a width handle to recipe previews
|
|
11
|
+
- **ui**: add FPasswordInput, a password field with a show/hide toggle
|
|
12
|
+
- **ui**: show a bay's fill on the place mark
|
|
13
|
+
- **recipes**: preview web recipes at phone, tablet or any dragged width
|
|
14
|
+
- **charts,ui**: let chart frames fill the height their card gives them
|
|
15
|
+
- **ui**: let an inset card's action sit in its header row
|
|
16
|
+
- **ui**: one neutral disc for every vehicle marker, also when zoomed out
|
|
17
|
+
- **ui**: marker tasks, selection, live pulse and FPlaceMark
|
|
18
|
+
- **ui**: update the vehicle marker glyphs
|
|
19
|
+
|
|
20
|
+
### Fixes
|
|
21
|
+
|
|
22
|
+
- **docs**: let a recipe's live screen scroll
|
|
23
|
+
- **docs**: show each recipe's name once on the index
|
|
24
|
+
- **ui**: give FPlaceMark the place asset's P and house
|
|
25
|
+
- **ui**: give FAppShell a main landmark and a skip link
|
|
26
|
+
- **docs**: keep the battery-swap bike-ID field at 16px so iOS doesn't zoom
|
|
27
|
+
- **ui**: give md buttons the md fields' 16px text on touch
|
|
28
|
+
- **ui**: match md select triggers to the 16px fields on touch
|
|
29
|
+
- **ui**: give each vehicle marker its state colour again
|
|
30
|
+
- **recipes**: size the phone screens for the thumb and the glance
|
|
31
|
+
- **recipes**: make the ops screens dense, ranked and full height
|
|
32
|
+
- **recipes**: put the form and state screens on one heading scale
|
|
33
|
+
- **recipes**: anchor the auth screens and tighten their hierarchy
|
|
34
|
+
- **recipes**: move "Open full screen" above the preview
|
|
35
|
+
- **recipes**: size phone specimens to the device frame
|
|
36
|
+
- **recipes**: give the code and reset screens the sign-in frame
|
|
37
|
+
- **recipes**: keep labels and warnings legible and every control visible
|
|
38
|
+
- **recipes**: take state labels and battery tone from the platform
|
|
39
|
+
- **recipes**: keep the phone flows doing what their comments argue
|
|
40
|
+
- **recipes**: make the form recipes validate and reset as they claim
|
|
41
|
+
- support exact sidebar route matching (#10)
|
|
42
|
+
- **ui**: keep md text fields at 16px on touch so iOS doesn't zoom
|
|
43
|
+
- **ui**: let a tap through the accuracy ring too
|
|
44
|
+
- **ui**: let a tap through the position dot
|
|
45
|
+
- **ui**: keep the position dot above the map's markers
|
|
46
|
+
|
|
47
|
+
### Other changes
|
|
48
|
+
|
|
49
|
+
- Add renovate.json
|
|
50
|
+
|
|
5
51
|
## 0.5.0 — 2026-10-25
|
|
6
52
|
|
|
7
53
|
### Breaking changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanforest/charts",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"vue": "^3.5.22"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@humanforest/tokens": "^0.5.
|
|
32
|
+
"@humanforest/tokens": "^0.5.1"
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
|
35
35
|
"access": "public"
|
package/src/FChartFrame.vue
CHANGED
|
@@ -20,6 +20,7 @@ import { chartLabel, fromCategories } from './fromCategories';
|
|
|
20
20
|
import { crosshairConfig } from './crosshairConfig';
|
|
21
21
|
import { forestTooltip, type ForestTooltipOptions } from './forestTooltip';
|
|
22
22
|
import { motionDuration } from './motionDuration';
|
|
23
|
+
import { frameHeight, type FrameHeight } from './frameHeight';
|
|
23
24
|
import { indexAxes, indexContainer } from './frameIndex';
|
|
24
25
|
import { tooltipDefaults, tooltipContainer } from './presets';
|
|
25
26
|
import { useThemeVersion } from './useThemeVersion';
|
|
@@ -32,7 +33,9 @@ const props = withDefaults(
|
|
|
32
33
|
data: Datum[];
|
|
33
34
|
/** Palette categories (from useChartPalette) — series order, names, colours. */
|
|
34
35
|
categories: Record<string, BulletLegendItemInterface>;
|
|
35
|
-
height
|
|
36
|
+
/** The chart's height in px, or `fill` to take the height of the frame's container, floored at
|
|
37
|
+
* FILL_MIN_HEIGHT. For a chart in a stretched card, which the card's row makes taller. */
|
|
38
|
+
height?: FrameHeight;
|
|
36
39
|
/** Numeric field to plot on x (e.g. a timestamp). Default: the row index. */
|
|
37
40
|
xKey?: keyof Datum & string;
|
|
38
41
|
/** Blind spread onto VisXYContainer — any XYContainerConfigInterface key, presets last-wins. */
|
|
@@ -133,6 +136,9 @@ useTooltipSnap(rootEl, tipId, props.crosshair ? 'crosshair' : 'mark', () => !pro
|
|
|
133
136
|
// propagates to marks and axes. Override via the prop or container.duration.
|
|
134
137
|
const resolvedDuration = computed(() => props.duration ?? motionDuration());
|
|
135
138
|
|
|
139
|
+
// The rules for `fill` live in frameHeight.ts; a number reaches the container exactly as before.
|
|
140
|
+
const sizing = computed(() => frameHeight(props.height));
|
|
141
|
+
|
|
136
142
|
// Theme flips and palette identity changes both remount: Unovis caches resolved colours on mount.
|
|
137
143
|
const themeVersion = useThemeVersion();
|
|
138
144
|
const repaintKey = computed(() =>
|
|
@@ -146,33 +152,37 @@ const repaintKey = computed(() =>
|
|
|
146
152
|
role="img"
|
|
147
153
|
:aria-label="chartLabel(categories, ariaLabel)"
|
|
148
154
|
style="display: flex; flex-direction: column; gap: 0.75rem"
|
|
155
|
+
:style="sizing.root"
|
|
149
156
|
>
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
157
|
+
<div :style="sizing.plot">
|
|
158
|
+
<VisXYContainer
|
|
159
|
+
:key="repaintKey"
|
|
160
|
+
:data="data"
|
|
161
|
+
:height="sizing.container"
|
|
162
|
+
:duration="resolvedDuration"
|
|
163
|
+
:style="sizing.host"
|
|
164
|
+
v-bind="{ ...containerConfig, ...$attrs }"
|
|
165
|
+
>
|
|
166
|
+
<slot name="mark" :data="data" :x="acc.xAccessor" :y="acc.yAccessors" :colors="acc.colors" />
|
|
167
|
+
<VisAxis v-if="axisX !== false" v-bind="axes.axisX" />
|
|
168
|
+
<VisAxis v-if="axisY !== false" v-bind="axes.axisY" />
|
|
169
|
+
<template v-if="tooltip !== false">
|
|
170
|
+
<!-- duration 0 is deliberate, and load-bearing twice over. Unovis animates the crosshair
|
|
171
|
+
line's x through a transition, so it eases toward the row the pointer just snapped to
|
|
172
|
+
— which reads as lag on the one element that should track the pointer exactly, and
|
|
173
|
+
leaves the line's real position mid-flight while the snap is reading it as the
|
|
174
|
+
tooltip's anchor. Easing it would put the panel on the wrong side of the plot. -->
|
|
175
|
+
<VisCrosshair
|
|
176
|
+
v-if="crosshair"
|
|
177
|
+
ref="crosshairRef"
|
|
178
|
+
v-bind="crosshairProps"
|
|
179
|
+
:template="tipFn"
|
|
180
|
+
:duration="0"
|
|
181
|
+
/>
|
|
182
|
+
<VisTooltip v-bind="tooltipConfig" />
|
|
183
|
+
</template>
|
|
184
|
+
</VisXYContainer>
|
|
185
|
+
</div>
|
|
176
186
|
<VisBulletLegend
|
|
177
187
|
v-if="legend"
|
|
178
188
|
:items="acc.legendItems"
|
|
@@ -10,6 +10,7 @@ import type { BulletLegendItemInterface, TooltipConfigInterface } from '@unovis/
|
|
|
10
10
|
// self-import through it would create a module cycle.
|
|
11
11
|
import { chartLabel, fromCategories } from './fromCategories';
|
|
12
12
|
import { motionDuration } from './motionDuration';
|
|
13
|
+
import { frameHeight, type FrameHeight } from './frameHeight';
|
|
13
14
|
import { tooltipDefaults, tooltipContainer } from './presets';
|
|
14
15
|
import { useThemeVersion } from './useThemeVersion';
|
|
15
16
|
import { useTooltipSnap, nextTipId } from './snapTooltip';
|
|
@@ -21,7 +22,9 @@ const props = withDefaults(
|
|
|
21
22
|
data: Data;
|
|
22
23
|
/** Optional: series colours + legend. Omit for charts that label directly (Sankey, Graph). */
|
|
23
24
|
categories?: Record<string, BulletLegendItemInterface>;
|
|
24
|
-
height
|
|
25
|
+
/** The chart's height in px, or `fill` to take the height of the frame's container, floored at
|
|
26
|
+
* FILL_MIN_HEIGHT — the same contract as FChartFrame's. */
|
|
27
|
+
height?: FrameHeight;
|
|
25
28
|
/** Rendered only when given — single-container tooltips need `triggers` to show anything. */
|
|
26
29
|
tooltip?: Partial<TooltipConfigInterface>;
|
|
27
30
|
/** Let the panel track the pointer instead of snapping to the hovered mark. Off by default:
|
|
@@ -66,6 +69,7 @@ const tooltipConfig = computed<Partial<TooltipConfigInterface>>(() => ({
|
|
|
66
69
|
const rootEl = ref<HTMLElement>();
|
|
67
70
|
useTooltipSnap(rootEl, tipId, 'mark', () => !props.followCursor);
|
|
68
71
|
const resolvedDuration = computed(() => props.duration ?? motionDuration());
|
|
72
|
+
const sizing = computed(() => frameHeight(props.height));
|
|
69
73
|
|
|
70
74
|
const themeVersion = useThemeVersion();
|
|
71
75
|
const repaintKey = computed(() =>
|
|
@@ -79,17 +83,21 @@ const repaintKey = computed(() =>
|
|
|
79
83
|
role="img"
|
|
80
84
|
:aria-label="chartLabel(categories, ariaLabel)"
|
|
81
85
|
style="display: flex; flex-direction: column; gap: 0.75rem"
|
|
86
|
+
:style="sizing.root"
|
|
82
87
|
>
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
<div :style="sizing.plot">
|
|
89
|
+
<VisSingleContainer
|
|
90
|
+
:key="repaintKey"
|
|
91
|
+
:data="data"
|
|
92
|
+
:height="sizing.container"
|
|
93
|
+
:duration="resolvedDuration"
|
|
94
|
+
:style="sizing.host"
|
|
95
|
+
v-bind="$attrs"
|
|
96
|
+
>
|
|
97
|
+
<slot name="mark" :data="data" :colors="acc.colors" />
|
|
98
|
+
<VisTooltip v-if="tooltip" v-bind="tooltipConfig" />
|
|
99
|
+
</VisSingleContainer>
|
|
100
|
+
</div>
|
|
93
101
|
<VisBulletLegend
|
|
94
102
|
v-if="legend && acc.legendItems.length"
|
|
95
103
|
:items="acc.legendItems"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// How a frame's `height` reaches the chart. A number is the container's own height in px, handed to
|
|
2
|
+
// Unovis as it always was. `fill` takes the height of the frame's own container instead, through CSS
|
|
3
|
+
// alone: the root takes 100% of its parent, the plot box takes what the legend leaves, and the Unovis
|
|
4
|
+
// host is laid over that box. Unovis measures the host when it renders, and its own ResizeObserver
|
|
5
|
+
// redraws the chart when the box changes size, so the frame adds no resize handling of its own.
|
|
6
|
+
//
|
|
7
|
+
// ★ THE HOST IS LAID OVER THE BOX, NOT IN ITS FLOW. A Unovis container with no `height` measures its
|
|
8
|
+
// host, and an in-flow host is as tall as the SVG inside it: the SVG sizes the host and the host sizes
|
|
9
|
+
// the SVG, so the chart renders at Unovis's 300px fallback and stays there. Laid over the box, the
|
|
10
|
+
// host adds nothing to the layout and the box's height comes from the layout alone. Inline styles,
|
|
11
|
+
// not utilities: Unovis's stylesheet pins its containers to `position: relative`, and a class loses.
|
|
12
|
+
//
|
|
13
|
+
// Under a number the box is `display: contents`, which draws no box at all, so the host stays the
|
|
14
|
+
// root's flex item exactly as it was before `fill` existed.
|
|
15
|
+
|
|
16
|
+
export type FrameHeight = number | 'fill';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The plot's floor under `fill`, in px. It holds when the container is shorter, and it is the whole
|
|
20
|
+
* height when the container has none of its own (a block in normal flow), so an unconstrained `fill`
|
|
21
|
+
* chart draws at the floor rather than collapsing to nothing. 160 sits under both frames' numeric
|
|
22
|
+
* defaults (280 and 260), so a `fill` chart never asks its row for more height than a numeric one
|
|
23
|
+
* would, and it keeps a readable band of plot between an XY chart's axes.
|
|
24
|
+
*/
|
|
25
|
+
export const FILL_MIN_HEIGHT = 160;
|
|
26
|
+
|
|
27
|
+
type Style = Record<string, string>;
|
|
28
|
+
|
|
29
|
+
/** What each layer of the frame gets: the container's `height` prop, and the root, plot box and host styles. */
|
|
30
|
+
export function frameHeight(height: FrameHeight): {
|
|
31
|
+
container: number | undefined;
|
|
32
|
+
root: Style | undefined;
|
|
33
|
+
plot: Style;
|
|
34
|
+
host: Style | undefined;
|
|
35
|
+
} {
|
|
36
|
+
if (height !== 'fill') return { container: height, root: undefined, plot: { display: 'contents' }, host: undefined };
|
|
37
|
+
return {
|
|
38
|
+
container: undefined,
|
|
39
|
+
root: { height: '100%' },
|
|
40
|
+
plot: { position: 'relative', flex: '1 1 0', minHeight: `${FILL_MIN_HEIGHT}px` },
|
|
41
|
+
host: { position: 'absolute', inset: '0' },
|
|
42
|
+
};
|
|
43
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// Importing this barrel needs an SFC pipeline (Vite/Nuxt) — everything here is browser-bound.
|
|
5
5
|
export { default as FChartFrame } from './FChartFrame.vue';
|
|
6
6
|
export { default as FChartFrameSingle } from './FChartFrameSingle.vue';
|
|
7
|
+
export { FILL_MIN_HEIGHT, type FrameHeight } from './frameHeight';
|
|
7
8
|
export { default as FChartLegend } from './FChartLegend.vue';
|
|
8
9
|
// Word-sized graphic for a KPI tile's `visual` slot: shape only, no axes, no tooltip.
|
|
9
10
|
export { default as FSparkline } from './FSparkline.vue';
|