Skip to content

Commit c80feef

Browse files
committed
Improvement - VueUiMolecule - Remove direction pad and use better zoom and pan feature
1 parent f3068eb commit c80feef

File tree

6 files changed

+55
-177
lines changed

6 files changed

+55
-177
lines changed

src/atoms/BaseIcon.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { XMLNS } from "../lib";
55
const props = defineProps({
66
name: String,
77
size: {
8-
type: Number,
8+
type: [Number, String],
99
default: 24
1010
},
1111
stroke: {

src/atoms/RecursiveCircles.vue

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,23 @@
1111
:stroke="hoveredUid && hoveredUid === node.uid ? strokeHovered : stroke"
1212
:stroke-width="hoveredUid && hoveredUid === node.uid ? node.circleRadius / 6 : node.circleRadius / 12"
1313
style="cursor:pointer"
14-
@click.stop="zoom(node)"
14+
@click="click(node)"
1515
@mouseover="hover(node)"
1616
@mouseleave="hover(null)"
1717
/>
18+
<foreignObject
19+
v-if="$slots.node"
20+
:x="coordinate.x - node.circleRadius"
21+
:y="coordinate.y - node.circleRadius"
22+
:height="node.circleRadius * 2"
23+
:width="node.circleRadius * 2"
24+
style="overflow: visible"
25+
@click.stop="click(node)"
26+
@mouseover="hover(node)"
27+
@mouseleave="hover(null)"
28+
>
29+
<slot name="node" v-bind="{ node }"/>
30+
</foreignObject>
1831
</template>
1932
<template v-if="node.nodes && node.nodes.length > 0">
2033
<RecursiveCircles
@@ -23,9 +36,13 @@
2336
:stroke="stroke"
2437
:strokeHovered="strokeHovered"
2538
:hoveredUid="hoveredUid"
26-
@zoom="zoom"
39+
@click="click"
2740
@hover="hover"
28-
/>
41+
>
42+
<template #node="{ node }">
43+
<slot name="node" v-bind="{ node }"/>
44+
</template>
45+
</RecursiveCircles>
2946
</template>
3047
</template>
3148
</template>
@@ -61,10 +78,10 @@ const props = defineProps({
6178
},
6279
});
6380
64-
const emit = defineEmits(["zoom", 'hover']);
81+
const emit = defineEmits(["click", 'hover']);
6582
66-
function zoom(node) {
67-
emit('zoom', node)
83+
function click(node) {
84+
emit('click', node)
6885
}
6986
7087
function hover(node) {

src/components/vue-data-ui.vue

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ const toggleAnimation = ref(() => null);
201201
const pauseAnimation = ref(() => null);
202202
const resumeAnimation = ref(() => null);
203203
const toggleAnnotator = ref(() => null);
204+
const selectNode = ref(() => null);
204205
205206
onMounted(() => {
206207
if (isError.value) {
@@ -283,6 +284,9 @@ watch(currentComponentRef, async (newRef) => {
283284
if (newRef.toggleAnnotator) {
284285
toggleAnnotator.value = newRef.toggleAnnotator;
285286
}
287+
if (newRef.selectNode) {
288+
selectNode.value = newRef.selectNode;
289+
}
286290
}
287291
})
288292
@@ -312,7 +316,8 @@ const getEventHandlers = () => {
312316
'toggleAnimation',
313317
'pauseAnimation',
314318
'resumeAnimation',
315-
'toggleAnnotator'
319+
'toggleAnnotator',
320+
'selectNode'
316321
];
317322
const handlers = {};
318323
eventNames.forEach(event => {
@@ -358,7 +363,8 @@ defineExpose({
358363
pauseAnimation,
359364
resumeAnimation,
360365
toggleAnimation,
361-
toggleAnnotator
366+
toggleAnnotator,
367+
selectNode
362368
});
363369
364370
const notSupported = computed(() => {

src/components/vue-ui-molecule.cy.js

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('<VueUiMolecule />', () => {
2020
subtitle: true,
2121
dataTable: true,
2222
tooltipCallback: () => {
23-
cy.get('[data-cy="recursive-circle"]').first().trigger('mouseover');
23+
cy.get('[data-cy="recursive-circle"]').first().trigger('mouseover', { force: true });
2424
}
2525
});
2626

@@ -35,51 +35,6 @@ describe('<VueUiMolecule />', () => {
3535
cy.get('[data-cy="recursive-label"]').should('not.exist')
3636
cy.get('[data-cy="user-options-label"]').click();
3737
cy.get('[data-cy="recursive-label"]').should('exist').and('be.visible').and('have.length', 76);
38-
39-
cy.log('zoom');
40-
cy.get('[data-cy="recursive-circle"]').eq(2).click();
41-
cy.get('[data-cy="cluster-svg"]').invoke('attr', 'viewBox').should('eq', '329.302356570748 158.64462809917356 82.71074380165288 82.71074380165288');
42-
43-
cy.log('direction pad');
44-
cy.log('move left');
45-
cy.get('[data-cy="direction-pad-left"]').then(btn => {
46-
for(let i = 0; i < 10; i += 1) {
47-
cy.wait(10)
48-
cy.wrap(btn).click()
49-
}
50-
});
51-
cy.get('[data-cy="cluster-svg"]').invoke('attr', 'viewBox').should('eq', '279.7155797112438 158.64462809917356 82.71074380165288 82.71074380165288');
52-
53-
cy.log('move top');
54-
cy.get('[data-cy="direction-pad-top"]').then(btn => {
55-
for(let i = 0; i < 10; i += 1) {
56-
cy.wait(10)
57-
cy.wrap(btn).click()
58-
}
59-
});
60-
cy.get('[data-cy="cluster-svg"]').invoke('attr', 'viewBox').should('eq', '279.7155797112438 109.05785123966936 82.71074380165288 82.71074380165288');
61-
62-
cy.log('move right');
63-
cy.get('[data-cy="direction-pad-right"]').then(btn => {
64-
for(let i = 0; i < 10; i += 1) {
65-
cy.wait(10)
66-
cy.wrap(btn).click()
67-
}
68-
});
69-
cy.get('[data-cy="cluster-svg"]').invoke('attr', 'viewBox').should('eq', '329.302356570748 109.05785123966936 82.71074380165288 82.71074380165288');
70-
71-
cy.log('move down');
72-
cy.get('[data-cy="direction-pad-bottom"]').then(btn => {
73-
for(let i = 0; i < 10; i += 1) {
74-
cy.wait(10)
75-
cy.wrap(btn).click()
76-
}
77-
});
78-
cy.get('[data-cy="cluster-svg"]').invoke('attr', 'viewBox').should('eq', '329.302356570748 158.64462809917356 82.71074380165288 82.71074380165288');
79-
80-
cy.log('reset zoom');
81-
cy.get('[data-cy="direction-pad-reset"]').click();
82-
cy.get('[data-cy="cluster-svg"]').invoke('attr', 'viewBox').should('eq', '-0.15999999999996817 -0.1599999999999966 400.32 400.32');
8338
});
8439
});
8540
});

src/components/vue-ui-molecule.vue

Lines changed: 21 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import Tooltip from "../atoms/Tooltip.vue";
2323
import RecursiveCircles from "../atoms/RecursiveCircles.vue";
2424
import RecursiveLinks from "../atoms/RecursiveLinks.vue";
2525
import RecursiveLabels from "../atoms/RecursiveLabels.vue";
26-
import BaseDirectionPad from "../atoms/BaseDirectionPad.vue";
2726
import Skeleton from "./vue-ui-skeleton.vue";
2827
import Accordion from "./vue-ui-accordion.vue";
2928
import { useNestedProp } from "../useNestedProp";
@@ -33,6 +32,7 @@ import PackageVersion from "../atoms/PackageVersion.vue";
3332
import PenAndPaper from "../atoms/PenAndPaper.vue";
3433
import { useUserOptionState } from "../useUserOptionState";
3534
import { useChartAccessibility } from "../useChartAccessibility";
35+
import usePanZoom from "../usePanZoom";
3636
3737
const { vue_ui_molecule: DEFAULT_CONFIG } = useConfig();
3838
@@ -51,6 +51,8 @@ const props = defineProps({
5151
},
5252
});
5353
54+
const emit = defineEmits(['selectNode']);
55+
5456
const isDataset = computed(() => {
5557
return !!props.dataset && props.dataset.length;
5658
});
@@ -73,10 +75,6 @@ const details = ref(null);
7375
const isTooltip = ref(false);
7476
const tooltipContent = ref("");
7577
const moleculeChart = ref(null);
76-
const zoomedId = ref(null);
77-
const isZoom = ref(false);
78-
const zoomReference = ref(null);
79-
const selectedNode = ref(null);
8078
const step = ref(0);
8179
const titleStep = ref(0);
8280
const tableStep = ref(0);
@@ -254,90 +252,6 @@ const convertedDataset = computed(() => {
254252
return processNodes(props.dataset);
255253
})
256254
257-
function restoreViewBox() {
258-
isZoom.value = false;
259-
zoomedId.value = null;
260-
selectedNode.value = null;
261-
zoomReference.value = null;
262-
zoomOnNode({
263-
polygonPath: {
264-
coordinates: [{x: svg.value.width / 2, y: svg.value.height / 2}]
265-
},
266-
circleRadius: 24,
267-
})
268-
}
269-
270-
const currentAnimationFrame = ref(null);
271-
272-
273-
function zoomOnNode(node) {
274-
moleculeChart.value.focus();
275-
276-
nextTick(() => {
277-
if (currentAnimationFrame.value) {
278-
cancelAnimationFrame(currentAnimationFrame.value);
279-
}
280-
const vb = dynamicViewBox.value.split(' ');
281-
const startX = parseFloat(vb[0]);
282-
const startY = parseFloat(vb[1]);
283-
const startWidth = parseFloat(vb[2]);
284-
const startHeight = parseFloat(vb[3]);
285-
const { x, y } = node.polygonPath.coordinates[0];
286-
const { circleRadius } = node;
287-
const sizer = 8.34;
288-
const targetX = x - circleRadius * sizer;
289-
const targetY = y - circleRadius * sizer;
290-
const targetWidth = circleRadius * sizer * 2;
291-
const targetHeight = circleRadius * sizer * 2;
292-
293-
const distance = Math.sqrt((targetX - startX) ** 2 + (targetY - startY) ** 2);
294-
const numSteps = Math.min(1200, Math.max(20, Math.floor(distance / 10)));
295-
const stepX = (targetX - startX) / numSteps;
296-
const stepY = (targetY - startY) / numSteps;
297-
const stepWidth = (targetWidth - startWidth) / numSteps;
298-
const stepHeight = (targetHeight - startHeight) / numSteps;
299-
let currentStep = 0;
300-
301-
function animateZoom() {
302-
dynamicViewBox.value = `${startX + stepX * currentStep} ${startY + stepY * currentStep} ${startWidth + stepWidth * currentStep} ${startHeight + stepHeight * currentStep}`;
303-
currentStep += FINAL_CONFIG.value.style.chart.zoom.speed;
304-
305-
if (currentStep <= numSteps) {
306-
currentAnimationFrame.value = requestAnimationFrame(animateZoom);
307-
}
308-
309-
}
310-
animateZoom();
311-
})
312-
}
313-
314-
function zoom(node) {
315-
if(zoomedId.value === node.uid) {
316-
restoreViewBox();
317-
318-
} else {
319-
zoomedId.value = node.uid;
320-
selectedNode.value = node;
321-
zoomReference.value = {
322-
circleRadius: node.circleRadius,
323-
polygonPath: {
324-
coordinates: [{ x: node.polygonPath.coordinates[0].x, y: node.polygonPath.coordinates[0].y }]
325-
}
326-
}
327-
zoomOnNode(node)
328-
isZoom.value = node.uid !== convertedDataset.value[0].uid
329-
}
330-
}
331-
332-
function unzoom(event) {
333-
if(event.target.nodeName !== 'circle') {
334-
restoreViewBox();
335-
isZoom.value = false;
336-
} else {
337-
return;
338-
}
339-
}
340-
341255
const dataTooltipSlot = ref(null);
342256
343257
function createTooltipContent(node) {
@@ -391,22 +305,6 @@ function hover(node) {
391305
}
392306
}
393307
394-
function move(direction) {
395-
if(direction === "right") {
396-
zoomReference.value.polygonPath.coordinates[0].x += zoomReference.value.circleRadius;
397-
}
398-
if(direction === "left") {
399-
zoomReference.value.polygonPath.coordinates[0].x -= zoomReference.value.circleRadius;
400-
}
401-
if(direction === "top") {
402-
zoomReference.value.polygonPath.coordinates[0].y -= zoomReference.value.circleRadius;
403-
}
404-
if(direction === "bottom") {
405-
zoomReference.value.polygonPath.coordinates[0].y += zoomReference.value.circleRadius;
406-
}
407-
zoomOnNode(zoomReference.value);
408-
}
409-
410308
function convertDatasetToCSVFormat(dataset) {
411309
const flattenedData = [];
412310
@@ -521,6 +419,17 @@ function toggleAnnotator() {
521419
isAnnotator.value = !isAnnotator.value;
522420
}
523421
422+
const { viewBox } = usePanZoom(svgRef, {
423+
x: 0,
424+
y: 0,
425+
width: svg.value.width <= 0 ? 10 : svg.value.width,
426+
height: svg.value.height <= 0 ? 10 : svg.value.height,
427+
}, FINAL_CONFIG.value.style.chart.zoom.speed)
428+
429+
function selectNode(node) {
430+
emit('selectNode', node)
431+
}
432+
524433
defineExpose({
525434
getData,
526435
generatePdf,
@@ -641,10 +550,9 @@ defineExpose({
641550
:xmlns="XMLNS"
642551
v-if="isDataset"
643552
data-cy="cluster-svg"
644-
:viewBox="dynamicViewBox"
553+
:viewBox="`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`"
645554
:class="{ 'vue-data-ui-fullscreen--on': isFullscreen, 'vue-data-ui-fulscreen--off': !isFullscreen }"
646555
:style="`overflow: hidden; background:transparent;color:${FINAL_CONFIG.style.chart.color}`"
647-
@click.stop="unzoom($event)"
648556
>
649557
<PackageVersion />
650558

@@ -681,9 +589,13 @@ defineExpose({
681589
:hoveredUid="hoveredUid"
682590
:stroke="FINAL_CONFIG.style.chart.nodes.stroke"
683591
:strokeHovered="FINAL_CONFIG.style.chart.nodes.strokeHovered"
684-
@zoom="zoom"
592+
@click="selectNode"
685593
@hover="hover"
686-
/>
594+
>
595+
<template #node="{ node }">
596+
<slot name="node" v-bind="{ node }"/>
597+
</template>
598+
</RecursiveCircles>
687599
<RecursiveLabels
688600
v-if="mutableConfig.showDataLabels"
689601
:dataset="convertedDataset"
@@ -693,18 +605,6 @@ defineExpose({
693605
<slot name="svg" :svg="svg"/>
694606
</svg>
695607

696-
<BaseDirectionPad
697-
v-if="isZoom"
698-
:key="`direction_pad_${step}`"
699-
:color="FINAL_CONFIG.style.chart.color"
700-
:isFullscreen="isFullscreen"
701-
@moveLeft="move('left')"
702-
@moveRight="move('right')"
703-
@moveTop="move('top')"
704-
@moveBottom="move('bottom')"
705-
@reset="restoreViewBox(); isZoom = false"
706-
/>
707-
708608
<div v-if="$slots.watermark" class="vue-data-ui-watermark">
709609
<slot name="watermark" v-bind="{ isPrinting: isPrinting || isImaging }"/>
710610
</div>

types/vue-data-ui.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1147,7 +1147,7 @@ declare module "vue-data-ui" {
11471147
name: VueUiIconName;
11481148
stroke?: string;
11491149
strokeWidth?: number;
1150-
size?: number;
1150+
size?: number | string;
11511151
isSpin?: boolean;
11521152
}>;
11531153

0 commit comments

Comments
 (0)