@@ -18,6 +18,7 @@ import {
1818 treeShake ,
1919 XMLNS
2020} from " ../lib" ;
21+ import { throttle } from " ../canvas-lib" ;
2122import usePanZoom from " ../usePanZoom" ;
2223import { useDag } from " ../useDag" ;
2324import { useConfig } from " ../useConfig" ;
@@ -28,10 +29,12 @@ import { useNestedProp } from "../useNestedProp";
2829import { useThemeCheck } from " ../useThemeCheck" ;
2930import { useUserOptionState } from " ../useUserOptionState" ;
3031import { useChartAccessibility } from " ../useChartAccessibility" ;
32+ import { useResponsive } from " ../useResponsive" ;
3133import Title from " ../atoms/Title.vue" ;
3234import themes from " ../themes/vue_ui_dag.json" ;
3335import BaseScanner from " ../atoms/BaseScanner.vue" ;
3436import BaseZoomControls from " ../atoms/BaseZoomControls.vue" ;
37+ import img from " ../img" ;
3538
3639const PenAndPaper = defineAsyncComponent (() => import (' ../atoms/PenAndPaper.vue' ));
3740const UserOptions = defineAsyncComponent (() => import (' ../atoms/UserOptions.vue' ));
@@ -65,20 +68,25 @@ const uid = ref(createUid());
6568
6669const chartTitle = ref (null );
6770const source = ref (null );
68- const userOptionsRef = ref (null );
71+ const zoomControls = ref (null );
6972const titleStep = ref (0 );
7073const step = ref (0 );
7174const userHovers = ref (false );
7275const 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+
7584const tooltipPosition = ref ({ x: 0 , y: 0 }); // anchor (screen coords)
7685const tooltipEdge = ref (null );
7786const tooltipRef = ref (null );
7887const tooltipStyle = ref ({ left: " 0px" , top: " 0px" });
7988const tooltipPlacement = ref (" top" );
8089
81- // Node tooltip (interactive, showOnClick)
8290const isNodeTooltip = ref (false );
8391const nodeTooltipPosition = ref ({ x: 0 , y: 0 });
8492const nodeTooltipOffset = ref ({ x: 0 , y: 0 });
@@ -90,8 +98,6 @@ const nodeTooltipPlacement = ref("top");
9098const isTooltip = ref (false );
9199const isAnnotator = ref (false );
92100
93- const FINAL_CONFIG = ref (prepareConfig ());
94-
95101const { svgRef } = useChartAccessibility ({ config: FINAL_CONFIG .value .style .chart .title });
96102const { userOptionsVisible , setUserOptionsVisibility , keepUserOptionState } = useUserOptionState ({ config: FINAL_CONFIG .value });
97103const 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
191199watch (() => 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
201212const { 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 );
370399const panZoomActive = ref (FINAL_CONFIG .value .style .chart .zoom .active );
371400
372401const {
@@ -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+
390462watch (
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(() => {
580641onBeforeUnmount (() => {
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+
585698async 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 ;
0 commit comments