Skip to content

Commit baedb5e

Browse files
committed
Improvement - VueUiDag - Add optional chart dimensions & responsive mode
1 parent 06deb96 commit baedb5e

File tree

5 files changed

+168
-33
lines changed

5 files changed

+168
-33
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ However the following charts can be made fully responsive, making them better to
769769
| VueUiChestnut | - |
770770
| VueUiChord ||
771771
| VueUiCirclePack ||
772-
| VueUiDag | - |
772+
| VueUiDag | |
773773
| VueUiDonut ||
774774
| VueUiDonutEvolution ||
775775
| VueUiDumbbell ||

TestingArena/ArenaVueUiDag.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const dataset = ref({
6969
const model = createModel([
7070
CHECKBOX('loading', { def: false }),
7171
CHECKBOX('debug', { def: true }),
72+
CHECKBOX('responsive', { def: false }),
7273
CHECKBOX('userOptions.buttons.pdf', { def: true }),
7374
CHECKBOX('userOptions.buttons.img', { def: true }),
7475
CHECKBOX('userOptions.buttons.svg', { def: true }),
@@ -198,6 +199,15 @@ const configTheme = computed(() => ({
198199
<Box comp="VueUiDag" :dataset="dataset" :config="config">
199200
<template #title>VueUiDag</template>
200201

202+
<template #responsive>
203+
<div style="width: 600px; height: 600px; resize: both; overflow: auto; background: white">
204+
<LocalVueUiDag :dataset="dataset" :config="{
205+
...config,
206+
responsive: true
207+
}"/>
208+
</div>
209+
</template>
210+
201211
<template #local>
202212
<LocalVueUiDag :dataset="dataset" :config="config" ref="local">
203213
<!-- <template #node-label="{ node }">

src/components/vue-ui-dag.vue

Lines changed: 151 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
treeShake,
1919
XMLNS
2020
} from "../lib";
21+
import { throttle } from "../canvas-lib";
2122
import usePanZoom from "../usePanZoom";
2223
import { useDag } from "../useDag";
2324
import { useConfig } from "../useConfig";
@@ -28,10 +29,12 @@ import { useNestedProp } from "../useNestedProp";
2829
import { useThemeCheck } from "../useThemeCheck";
2930
import { useUserOptionState } from "../useUserOptionState";
3031
import { useChartAccessibility } from "../useChartAccessibility";
32+
import { useResponsive } from "../useResponsive";
3133
import Title from "../atoms/Title.vue";
3234
import themes from "../themes/vue_ui_dag.json";
3335
import BaseScanner from "../atoms/BaseScanner.vue";
3436
import BaseZoomControls from "../atoms/BaseZoomControls.vue";
37+
import img from "../img";
3538
3639
const PenAndPaper = defineAsyncComponent(() => import('../atoms/PenAndPaper.vue'));
3740
const UserOptions = defineAsyncComponent(() => import('../atoms/UserOptions.vue'));
@@ -65,20 +68,25 @@ const uid = ref(createUid());
6568
6669
const chartTitle = ref(null);
6770
const source = ref(null);
68-
const userOptionsRef = ref(null);
71+
const zoomControls = ref(null);
6972
const titleStep = ref(0);
7073
const step = ref(0);
7174
const userHovers = ref(false);
7275
const isDataset = ref(false);
7376
74-
// Midpoint tooltip (existing)
77+
const resizeObserver = ref(null);
78+
const observedEl = ref(null);
79+
80+
const FINAL_CONFIG = ref(prepareConfig());
81+
const WIDTH = ref(FINAL_CONFIG.value.style.chart.width);
82+
const HEIGHT = ref(FINAL_CONFIG.value.style.chart.height);
83+
7584
const tooltipPosition = ref({ x: 0, y: 0 }); // anchor (screen coords)
7685
const tooltipEdge = ref(null);
7786
const tooltipRef = ref(null);
7887
const tooltipStyle = ref({ left: "0px", top: "0px" });
7988
const tooltipPlacement = ref("top");
8089
81-
// Node tooltip (interactive, showOnClick)
8290
const isNodeTooltip = ref(false);
8391
const nodeTooltipPosition = ref({ x: 0, y: 0 });
8492
const nodeTooltipOffset = ref({ x: 0, y: 0 });
@@ -90,8 +98,6 @@ const nodeTooltipPlacement = ref("top");
9098
const isTooltip = ref(false);
9199
const isAnnotator = ref(false);
92100
93-
const FINAL_CONFIG = ref(prepareConfig());
94-
95101
const { svgRef } = useChartAccessibility({ config: FINAL_CONFIG.value.style.chart.title });
96102
const { userOptionsVisible, setUserOptionsVisibility, keepUserOptionState } = useUserOptionState({ config: FINAL_CONFIG.value });
97103
const direction = ref(FINAL_CONFIG.value.style.chart.layout.rankDirection);
@@ -186,7 +192,9 @@ onMounted(() => {
186192
manualLoading.value = true;
187193
}
188194
isDataset.value = true;
189-
})
195+
196+
setupResponsive();
197+
});
190198
191199
watch(() => props.config, (_newCfg) => {
192200
if (!loading.value) {
@@ -195,7 +203,10 @@ watch(() => props.config, (_newCfg) => {
195203
userOptionsVisible.value = !FINAL_CONFIG.value.userOptions.showOnChartHover;
196204
titleStep.value += 1;
197205
direction.value = FINAL_CONFIG.value.style.chart.layout.rankDirection;
206+
WIDTH.value = FINAL_CONFIG.value.style.chart.width;
207+
HEIGHT.value = FINAL_CONFIG.value.style.chart.height;
198208
panZoomActive.value = FINAL_CONFIG.value.style.chart.zoom.active;
209+
setupResponsive();
199210
}, { deep: true });
200211
201212
const { isPrinting, isImaging, generatePdf, generateImage } = usePrinter({
@@ -339,7 +350,7 @@ const dagConfiguration = computed(() => {
339350
};
340351
});
341352
342-
const { layoutData, lastError, arrowMarkerIdentifier, recomputeLayout } = useDag({
353+
const { layoutData, lastError, arrowMarkerIdentifier } = useDag({
343354
nodes: initialNodes,
344355
edges: initialEdges,
345356
configuration: dagConfiguration
@@ -364,9 +375,27 @@ function makeMarkerId(color) {
364375
return `${arrowMarkerIdentifier}-${String(color).replace(/[^a-zA-Z0-9_-]/g, "_")}`;
365376
}
366377
378+
const userViewBox = computed(() => {
379+
const widthRaw = WIDTH.value;
380+
const heightRaw = HEIGHT.value;
367381
368-
const highlightedNodeId = ref(null);
382+
const width = Number(widthRaw);
383+
const height = Number(heightRaw);
384+
385+
const hasWidth = Number.isFinite(width) && width > 0;
386+
const hasHeight = Number.isFinite(height) && height > 0;
387+
388+
if (!hasWidth && !hasHeight) {
389+
return null;
390+
}
391+
392+
return {
393+
width: hasWidth ? width : null,
394+
height: hasHeight ? height : null
395+
};
396+
});
369397
398+
const highlightedNodeId = ref(null);
370399
const panZoomActive = ref(FINAL_CONFIG.value.style.chart.zoom.active);
371400
372401
const {
@@ -387,27 +416,61 @@ function toggleZoom() {
387416
panZoomActive.value = !panZoomActive.value;
388417
}
389418
419+
function updateInitialViewBoxFromLayout() {
420+
const layoutViewBox = layoutData.value && layoutData.value.viewBox;
421+
if (!layoutViewBox) return;
422+
423+
const parts = String(layoutViewBox).split(" ").map(Number);
424+
if (parts.length !== 4) return;
425+
426+
const [layoutX, layoutY, layoutWidth, layoutHeight] = parts;
427+
428+
if (
429+
!Number.isFinite(layoutX) ||
430+
!Number.isFinite(layoutY) ||
431+
!Number.isFinite(layoutWidth) ||
432+
!Number.isFinite(layoutHeight)
433+
) {
434+
return;
435+
}
436+
437+
let targetWidth = layoutWidth;
438+
let targetHeight = layoutHeight;
439+
let targetX = layoutX;
440+
let targetY = layoutY;
441+
442+
const userVb = userViewBox.value;
443+
if (userVb) {
444+
if (userVb.width !== null) {
445+
targetWidth = userVb.width;
446+
}
447+
if (userVb.height !== null) {
448+
targetHeight = userVb.height;
449+
}
450+
451+
// Center layout
452+
targetX = layoutX - (targetWidth - layoutWidth) / 2;
453+
targetY = layoutY - (targetHeight - layoutHeight) / 2;
454+
}
455+
456+
setInitialViewBox(
457+
{ x: targetX, y: targetY, width: targetWidth, height: targetHeight },
458+
{ overwriteCurrentIfNotZoomed: true }
459+
);
460+
}
461+
390462
watch(
391463
() => layoutData.value && layoutData.value.viewBox,
392-
newViewBox => {
393-
if (!newViewBox) return;
394-
395-
const [x, y, width, height] = newViewBox.split(" ").map(Number);
396-
397-
if (
398-
Number.isFinite(x) &&
399-
Number.isFinite(y) &&
400-
Number.isFinite(width) &&
401-
Number.isFinite(height)
402-
) {
403-
setInitialViewBox(
404-
{ x, y, width, height },
405-
{ overwriteCurrentIfNotZoomed: true }
406-
);
407-
}
464+
() => {
465+
updateInitialViewBoxFromLayout();
408466
},
409-
{
410-
immediate: true
467+
{ immediate: true }
468+
);
469+
470+
watch(
471+
() => userViewBox.value,
472+
() => {
473+
updateInitialViewBoxFromLayout();
411474
}
412475
);
413476
@@ -517,8 +580,6 @@ async function showNodeTooltip(node) {
517580
const nodeWidthSvg = FINAL_CONFIG.value.style.chart.layout.nodeWidth;
518581
const nodeHeightSvg = FINAL_CONFIG.value.style.chart.layout.nodeHeight;
519582
520-
// const scaleX = Math.hypot(ctm.a, ctm.c);
521-
// const scaleY = Math.hypot(ctm.b, ctm.d);
522583
const scaleX = ctm.a;
523584
const scaleY = ctm.d;
524585
@@ -580,8 +641,60 @@ onMounted(() => {
580641
onBeforeUnmount(() => {
581642
document.removeEventListener("mousedown", handleDocumentClick);
582643
document.removeEventListener("keydown", handleDocumentKeydown);
644+
645+
if (resizeObserver.value) {
646+
if (observedEl.value) {
647+
resizeObserver.value.unobserve(observedEl.value);
648+
}
649+
resizeObserver.value.disconnect();
650+
}
583651
});
584652
653+
function setupResponsive() {
654+
if (!FINAL_CONFIG.value.responsive) {
655+
if (resizeObserver.value) {
656+
if (observedEl.value) {
657+
resizeObserver.value.unobserve(observedEl.value);
658+
}
659+
resizeObserver.value.disconnect();
660+
resizeObserver.value = null;
661+
observedEl.value = null;
662+
}
663+
return;
664+
}
665+
666+
const handleResize = throttle(() => {
667+
if (!dagChart.value) return;
668+
669+
const { width, height } = useResponsive({
670+
chart: dagChart.value,
671+
title: FINAL_CONFIG.value.style.chart.title.text ? chartTitle.value : null,
672+
legend: FINAL_CONFIG.value.style.chart.controls.show ? zoomControls.value.$el : null,
673+
source: source.value
674+
});
675+
676+
requestAnimationFrame(() => {
677+
WIDTH.value = Math.max(0.1, width);
678+
HEIGHT.value = Math.max(0.1, height - 12);
679+
});
680+
});
681+
682+
if (resizeObserver.value) {
683+
if (observedEl.value) {
684+
resizeObserver.value.unobserve(observedEl.value);
685+
}
686+
resizeObserver.value.disconnect();
687+
}
688+
689+
resizeObserver.value = new ResizeObserver(handleResize);
690+
observedEl.value = dagChart.value ? dagChart.value.parentNode : null;
691+
if (observedEl.value) {
692+
resizeObserver.value.observe(observedEl.value);
693+
}
694+
695+
handleResize();
696+
}
697+
585698
async function getImage({ scale = 2} = {}) {
586699
if (!dagChart.value) return
587700
const { width, height } = dagChart.value.getBoundingClientRect()
@@ -614,12 +727,12 @@ defineExpose({
614727
resetZoom,
615728
switchDirection
616729
})
617-
618730
</script>
619731
732+
620733
<template>
621734
<div
622-
:class="`vue-data-ui-component vue-ui-dag ${isFullscreen ? 'vue-data-ui-wrapper-fullscreen' : ''}`"
735+
:class="`vue-data-ui-component vue-ui-dag ${isFullscreen ? 'vue-data-ui-wrapper-fullscreen' : ''} ${FINAL_CONFIG.responsive ? 'vue-ui-dag-responsive' : ''}`"
623736
:id="`dag_${uid}`"
624737
ref="dagChart"
625738
:style="{
@@ -739,7 +852,8 @@ defineExpose({
739852
/>
740853
</div>
741854
742-
<BaseZoomControls
855+
<BaseZoomControls
856+
ref="zoomControls"
743857
v-if="FINAL_CONFIG.style.chart.controls.position === 'top' && !loading && FINAL_CONFIG.style.chart.controls.show"
744858
:config="FINAL_CONFIG"
745859
:scale="scale"
@@ -981,7 +1095,8 @@ defineExpose({
9811095
</Teleport>
9821096
</Transition>
9831097
984-
<BaseZoomControls
1098+
<BaseZoomControls
1099+
ref="zoomControls"
9851100
v-if="FINAL_CONFIG.style.chart.controls.position === 'bottom' && !loading && FINAL_CONFIG.style.chart.controls.show"
9861101
:config="FINAL_CONFIG"
9871102
:scale="scale"
@@ -1010,6 +1125,10 @@ defineExpose({
10101125
position: relative;
10111126
}
10121127
1128+
.vue-ui-dag-responsive {
1129+
width: 100%;
1130+
}
1131+
10131132
.dag-chart-error {
10141133
color: #b00020;
10151134
font-size: 13px;

src/useConfig.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6769,6 +6769,7 @@ export function useConfig() {
67696769
const vue_ui_dag = {
67706770
loading: false,
67716771
debug: false,
6772+
responsive: false,
67726773
theme: '',
67736774
userOptions: USER_OPTIONS({
67746775
tooltip: false,
@@ -6785,6 +6786,8 @@ export function useConfig() {
67856786
chart: {
67866787
backgroundColor: COLOR_WHITE,
67876788
color: COLOR_BLACK,
6789+
width: null,
6790+
height: null,
67886791
layout: {
67896792
rankDirection: 'TB',
67906793
rankSeparation: 60,

types/vue-data-ui.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9455,10 +9455,13 @@ declare module "vue-data-ui" {
94559455
loading?: boolean;
94569456
debug?: boolean;
94579457
theme?: Theme;
9458+
responsive?: boolean;
94589459
userOptions?: ChartUserOptions;
94599460
style?: {
94609461
fontFamily?: string;
94619462
chart?: {
9463+
width?: number | null;
9464+
height?: number | null;
94629465
backgroundColor?: string;
94639466
color?: string;
94649467
layout?: {

0 commit comments

Comments
 (0)