Skip to content

Commit 9ae8481

Browse files
committed
Fix - VueUiHeatmap - Improve label observers; add optional crosshairs
1 parent cb19db9 commit 9ae8481

File tree

6 files changed

+179
-54
lines changed

6 files changed

+179
-54
lines changed

TestingArena/ArenaVueUiHeatmap.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ const model = createModel([
124124
NUMBER("style.layout.padding.bottom", { def: 0, min: 0, max: 100 }),
125125
NUMBER("style.layout.padding.left", { def: 0, min: 0, max: 100 }),
126126
127+
CHECKBOX('style.layout.crosshairs.show', { def: true }),
128+
COLOR('style.layout.crosshairs.stroke', { def: '#1A1A1A'}),
129+
NUMBER('style.layout.crosshairs.strokeWidth', { def: 1, min: 1, max: 6}),
130+
NUMBER('style.layout.crosshairs.strokeDasharray', { def: 0, min: 0, max: 12 }),
131+
127132
NUMBER("style.layout.cells.height", { def: 36, min: 12, max: 64 }),
128133
CHECKBOX("style.layout.cells.value.show", { def: true }),
129134
NUMBER("style.layout.cells.value.fontSize", { def: 18, min: 8, max: 48 }),

src/components/vue-ui-heatmap.vue

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import { useTableResponsive } from "../useTableResponsive";
4343
import { useUserOptionState } from "../useUserOptionState";
4444
import { useTimeLabelCollision } from "../useTimeLabelCollider";
4545
import { useChartAccessibility } from "../useChartAccessibility";
46-
import { useResizeObserverEffect } from '../useResizeObserverEffect';
46+
import { useLabelObserverEffect } from '../useLabelObserverEfffect';
4747
import img from "../img";
4848
import Title from "../atoms/Title.vue"; // Must be ready in responsive mode
4949
import themes from "../themes/vue_ui_heatmap.json";
@@ -353,16 +353,28 @@ const svg = computed(() => ({
353353
}));
354354
355355
const topLabelsHeight = ref(0);
356-
const updateTopLabelsHeight = throttle((h) => { topLabelsHeight.value = h }, 100);
357-
useResizeObserverEffect({ elementRef: xAxisLabels, callback: updateTopLabelsHeight, attr: 'height' });
356+
const updateTopLabelsHeight = throttle((h) => {
357+
if (h !== topLabelsHeight.value) {
358+
topLabelsHeight.value = h;
359+
}
360+
}, 100);
361+
useLabelObserverEffect({ elementRef: xAxisLabels, callback: updateTopLabelsHeight, attr: 'height' });
358362
359363
const leftLabelsWidth = ref(0);
360-
const updateLeftLabelsWidth = throttle((h) => { leftLabelsWidth.value = h }, 100);
361-
useResizeObserverEffect({ elementRef: yAxisLabels, callback: updateLeftLabelsWidth, attr: 'width' });
364+
const updateLeftLabelsWidth = throttle((w) => {
365+
if (w !== leftLabelsWidth.value) {
366+
leftLabelsWidth.value = w;
367+
}
368+
}, 100);
369+
useLabelObserverEffect({ elementRef: yAxisLabels, callback: updateLeftLabelsWidth, attr: 'width' });
362370
363371
const xAxisSumLabelsHeight = ref(0);
364-
const updateXAxisSumLabelsHeight = throttle((h) => { xAxisSumLabelsHeight.value = h }, 100);
365-
useResizeObserverEffect({ elementRef: xAxisSums, callback: updateXAxisSumLabelsHeight, attr: 'height' });
372+
const updateXAxisSumLabelsHeight = throttle((h) => {
373+
if (h !== xAxisSumLabelsHeight.value) {
374+
xAxisSumLabelsHeight.value = h;
375+
}
376+
}, 100);
377+
useLabelObserverEffect({ elementRef: xAxisSums, callback: updateXAxisSumLabelsHeight, attr: 'height' });
366378
367379
onBeforeUnmount(() => {
368380
topLabelsHeight.value = 0;
@@ -1202,6 +1214,30 @@ defineExpose({
12021214
/>
12031215
</g>
12041216
1217+
<!-- Crosshairs -->
1218+
<g v-if="FINAL_CONFIG.style.layout.crosshairs.show && selectedClone">
1219+
<line
1220+
:x1="drawingArea.left + (FINAL_CONFIG.style.layout.cells.rowTotal.color.show ? drawingArea.sumCellXHeight : 0)"
1221+
:x2="selectedClone.x + (FINAL_CONFIG.style.layout.cells.rowTotal.color.show ? drawingArea.sumCellXHeight : 0)"
1222+
:y1="selectedClone.y + ((drawingArea.cellSize.height - cellGap) / 2)"
1223+
:y2="selectedClone.y + ((drawingArea.cellSize.height - cellGap) / 2)"
1224+
:stroke="FINAL_CONFIG.style.layout.crosshairs.stroke"
1225+
:stroke-width="FINAL_CONFIG.style.layout.crosshairs.strokeWidth"
1226+
:stroke-dasharray="FINAL_CONFIG.style.layout.crosshairs.strokeDasharray"
1227+
stroke-linecap="round"
1228+
/>
1229+
<line
1230+
:x1="selectedClone.x + (FINAL_CONFIG.style.layout.cells.rowTotal.color.show ? drawingArea.sumCellXHeight : 0) + ((drawingArea.cellSize.width - cellGap) / 2)"
1231+
:x2="selectedClone.x + (FINAL_CONFIG.style.layout.cells.rowTotal.color.show ? drawingArea.sumCellXHeight : 0) + ((drawingArea.cellSize.width - cellGap) / 2)"
1232+
:y1="selectedClone.y"
1233+
:y2="drawingArea.top"
1234+
:stroke="FINAL_CONFIG.style.layout.crosshairs.stroke"
1235+
:stroke-width="FINAL_CONFIG.style.layout.crosshairs.strokeWidth"
1236+
:stroke-dasharray="FINAL_CONFIG.style.layout.crosshairs.strokeDasharray"
1237+
stroke-linecap="round"
1238+
/>
1239+
</g>
1240+
12051241
<slot name="svg" :svg="svg"/>
12061242
</svg>
12071243

src/useConfig.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2254,6 +2254,12 @@ export function useConfig() {
22542254
height: 300, // v3
22552255
width: 1000, // v3
22562256
padding: PADDING([0, 0, 0, 0]),
2257+
crosshairs: {
2258+
show: false,
2259+
stroke: COLOR_BLACK,
2260+
strokeWidth: 1,
2261+
strokeDasharray: 0,
2262+
},
22572263
cells: {
22582264
// height: 36, // v3 deprecated
22592265
rowTotal: {

src/useLabelObserverEfffect.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { nextTick, watchEffect } from 'vue';
2+
3+
export function useLabelObserverEffect({
4+
elementRef,
5+
callback,
6+
attr,
7+
earlyReturn = false,
8+
retryFrames = 12,
9+
alsoAfterFontsReady = true
10+
}) {
11+
if (earlyReturn) return;
12+
13+
const measureValue = (element) => {
14+
if (!element) return;
15+
16+
let value;
17+
18+
if (typeof element.getBBox === 'function') {
19+
try {
20+
const box = element.getBBox();
21+
value = attr === "width" ? box.width : box.height;
22+
} catch {
23+
// whatever
24+
}
25+
}
26+
27+
if (typeof value !== 'number' || Number.isNaN(value)) {
28+
try {
29+
const rect = element.getBoundingClientRect();
30+
value = attr === 'width' ? rect.width : rect.height;
31+
} catch {
32+
value = undefined;
33+
}
34+
}
35+
36+
if (typeof value === 'number' && !Number.isNaN(value)) {
37+
callback(value);
38+
}
39+
};
40+
41+
watchEffect((onInvalidate) => {
42+
const element = elementRef.value;
43+
if (!element) return;
44+
45+
let cancelled = false;
46+
47+
// measure after render
48+
const runInitialMeasures = async () => {
49+
await nextTick();
50+
51+
for (let index = 0; index < retryFrames; index += 1) {
52+
if (cancelled) return;
53+
54+
await new Promise((resolve) => requestAnimationFrame(resolve));
55+
56+
const current = elementRef.value;
57+
if (!current) return;
58+
59+
measureValue(current);
60+
}
61+
62+
if (
63+
alsoAfterFontsReady &&
64+
typeof document !== 'undefined' &&
65+
document.fonts &&
66+
document.fonts.ready
67+
) {
68+
try {
69+
await document.fonts.ready;
70+
} catch {
71+
// whatever
72+
}
73+
if (!cancelled && elementRef.value) {
74+
// Measure after fonts settle
75+
measureValue(elementRef.value);
76+
}
77+
}
78+
};
79+
80+
runInitialMeasures();
81+
82+
const mutationObserver = new MutationObserver(() => {
83+
const current = elementRef.value;
84+
if (!current) return;
85+
requestAnimationFrame(() => {
86+
if (elementRef.value) measureValue(elementRef.value);
87+
});
88+
});
89+
90+
mutationObserver.observe(element, {
91+
childList: true,
92+
subtree: true,
93+
characterData: true,
94+
attributes: true,
95+
attributeFilter: ['transform', 'style', 'class']
96+
});
97+
98+
// Also measure on resize
99+
let resizeObserver;
100+
if (typeof ResizeObserver !== 'undefined') {
101+
const observedNode =
102+
element.ownerSVGElement ? element.ownerSVGElement : element;
103+
104+
resizeObserver = new ResizeObserver(() => {
105+
const current = elementRef.value;
106+
if (!current) return;
107+
measureValue(current);
108+
});
109+
110+
resizeObserver.observe(observedNode);
111+
}
112+
113+
onInvalidate(() => {
114+
cancelled = true;
115+
mutationObserver.disconnect();
116+
if (resizeObserver) resizeObserver.disconnect();
117+
});
118+
}, { flush: 'post' });
119+
}

src/useResizeObserverEffect.js

Lines changed: 0 additions & 47 deletions
This file was deleted.

types/vue-data-ui.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3326,6 +3326,12 @@ declare module "vue-data-ui" {
33263326
height?: number;
33273327
width?: number;
33283328
padding?: ChartPadding;
3329+
crosshairs?: {
3330+
show?: boolean;
3331+
stroke?: string;
3332+
strokeWidth?: number;
3333+
strokeDasharray?: number;
3334+
};
33293335
cells?: {
33303336
// height?: number; // v3 deprecated
33313337
columnTotal?: {

0 commit comments

Comments
 (0)