Skip to content

Commit 2e2cb48

Browse files
committed
Improvement - VueUiDag - Offset edge paths to hide tip under arrows
1 parent 451886d commit 2e2cb48

File tree

4 files changed

+92
-30
lines changed

4 files changed

+92
-30
lines changed

TestingArena/ArenaVueUiDag.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,11 @@ const configTheme = computed(() => ({
224224
<template #tooltip-node="{ node }">
225225
{{ node }}
226226
</template> -->
227+
<!-- <template #free-node-label="{ node }">
228+
<text :x="node.x" text-anchor="middle" :y="node.y + node.height" fill="red">
229+
{{ node.label }}
230+
</text>
231+
</template> -->
227232
</LocalVueUiDag>
228233
</template>
229234

src/DAG/layout.js

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ const edgeDefaults = {
100100
labeloffset: 10,
101101
labelpos: "r",
102102
};
103-
const edgeAttrs = ["labelpos"];
103+
const edgeAttrs = ["labelpos", "arrowshape"]; // must be lowercase
104104

105105
function buildLayoutGraph(inputGraph) {
106106
const graph = canonicalize(inputGraph.graph());
@@ -250,13 +250,37 @@ function translateGraph(graph) {
250250
}
251251

252252
function assignNodeIntersects(graph) {
253-
graph.edges().forEach(edgeObj => {
254-
const edge = graph.edge(edgeObj);
255-
const nodeV = graph.node(edgeObj.v);
256-
const nodeW = graph.node(edgeObj.w);
253+
const ENDPOINT_OFFSET = 4;
254+
255+
function shortenPoint(point, neighborPoint, offset) {
256+
if (!offset || !neighborPoint) {
257+
return point;
258+
}
259+
260+
const deltaX = neighborPoint.x - point.x;
261+
const deltaY = neighborPoint.y - point.y;
262+
const segmentLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
263+
264+
if (!segmentLength || segmentLength <= offset) {
265+
return point;
266+
}
267+
268+
const ratio = offset / segmentLength;
269+
270+
return {
271+
x: point.x + deltaX * ratio,
272+
y: point.y + deltaY * ratio,
273+
};
274+
}
275+
276+
graph.edges().forEach(edgeObject => {
277+
const edge = graph.edge(edgeObject);
278+
const nodeV = graph.node(edgeObject.v);
279+
const nodeW = graph.node(edgeObject.w);
257280

258281
let firstPoint;
259282
let lastPoint;
283+
260284
if (!edge.points || !edge.points.length) {
261285
edge.points = [];
262286
firstPoint = nodeW;
@@ -266,11 +290,39 @@ function assignNodeIntersects(graph) {
266290
lastPoint = edge.points[edge.points.length - 1];
267291
}
268292

269-
edge.points.unshift(intersectRect(nodeV, firstPoint));
270-
edge.points.push(intersectRect(nodeW, lastPoint));
293+
const rawStart = intersectRect(nodeV, firstPoint);
294+
const rawEnd = intersectRect(nodeW, lastPoint);
295+
296+
const startNeighborPoint =
297+
edge.points.length ? edge.points[0] : firstPoint;
298+
const endNeighborPoint =
299+
edge.points.length ? edge.points[edge.points.length - 1] : lastPoint;
300+
301+
const arrowShape = edge.arrowshape;
302+
const shouldApplyOffset =
303+
arrowShape === "normal" || arrowShape === "vee";
304+
305+
const isReversed = !!edge.reversed;
306+
307+
let shortenedStart = rawStart;
308+
let shortenedEnd = rawEnd;
309+
310+
if (shouldApplyOffset) {
311+
if (isReversed) {
312+
// This side will become the FINAL tip after reversePointsForReversedEdges
313+
shortenedStart = shortenPoint(rawStart, startNeighborPoint, ENDPOINT_OFFSET);
314+
} else {
315+
// Normal case: FINAL tip is at current end
316+
shortenedEnd = shortenPoint(rawEnd, endNeighborPoint, ENDPOINT_OFFSET);
317+
}
318+
}
319+
320+
edge.points.unshift(shortenedStart);
321+
edge.points.push(shortenedEnd);
271322
});
272323
}
273324

325+
274326
function fixupEdgeLabelCoords(graph) {
275327
graph.edges().forEach(edgeObj => {
276328
const edge = graph.edge(edgeObj);

src/components/vue-ui-dag.vue

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ function prepareConfig() {
170170
171171
const debug = computed(() => !!FINAL_CONFIG.value.debug);
172172
173-
onMounted(() => {
173+
onMounted(async () => {
174174
if (objectIsEmpty(props.dataset)) {
175175
error({
176176
componentName: 'VueUiDag',
@@ -193,10 +193,11 @@ onMounted(() => {
193193
}
194194
isDataset.value = true;
195195
196+
await nextTick();
196197
setupResponsive();
197198
});
198199
199-
watch(() => props.config, (_newCfg) => {
200+
watch(() => props.config, async (_newCfg) => {
200201
if (!loading.value) {
201202
FINAL_CONFIG.value = prepareConfig();
202203
}
@@ -206,6 +207,7 @@ watch(() => props.config, (_newCfg) => {
206207
WIDTH.value = FINAL_CONFIG.value.style.chart.width;
207208
HEIGHT.value = FINAL_CONFIG.value.style.chart.height;
208209
panZoomActive.value = FINAL_CONFIG.value.style.chart.zoom.active;
210+
await nextTick();
209211
setupResponsive();
210212
}, { deep: true });
211213
@@ -669,7 +671,7 @@ function setupResponsive() {
669671
const { width, height } = useResponsive({
670672
chart: dagChart.value,
671673
title: FINAL_CONFIG.value.style.chart.title.text ? chartTitle.value : null,
672-
legend: FINAL_CONFIG.value.style.chart.controls.show ? zoomControls.value.$el : null,
674+
legend: FINAL_CONFIG.value.style.chart.controls.show ? zoomControls.value?.$el : null,
673675
source: source.value
674676
});
675677
@@ -886,33 +888,32 @@ defineExpose({
886888
:id="makeMarkerId(color)"
887889
:markerWidth="layoutData.arrowSize"
888890
:markerHeight="layoutData.arrowSize"
889-
:refX="layoutData.arrowSize - 1"
891+
:refX="layoutData.arrowSize - 3"
890892
:refY="layoutData.arrowSize / 2"
891893
orient="auto"
892894
markerUnits="strokeWidth"
893895
>
894-
<!-- `normal` arrow -->
895-
<path
896-
v-if="layoutData.arrowShape === 'normal'"
897-
:d="`M 0 0 L ${layoutData.arrowSize} ${layoutData.arrowSize/2} L 0 ${layoutData.arrowSize} Z`"
898-
:fill="color"
899-
:stroke="color"
900-
stroke-width="0"
901-
/>
896+
<!-- `normal` arrow -->
897+
<path
898+
v-if="layoutData.arrowShape === 'normal'"
899+
:d="`M 0 0 L ${layoutData.arrowSize} ${layoutData.arrowSize/2} L 0 ${layoutData.arrowSize} Z`"
900+
:fill="color"
901+
:stroke="color"
902+
stroke-width="0"
903+
/>
902904
903-
<!-- `vee` arrow -->
904-
<path
905-
v-else
906-
:d="`M 0 0 L ${layoutData.arrowSize} ${layoutData.arrowSize/2} L 0 ${layoutData.arrowSize} L ${layoutData.arrowSize / 3} ${layoutData.arrowSize / 2} Z`"
907-
:fill="color"
908-
:stroke="color"
909-
stroke-width="0"
910-
/>
905+
<!-- `vee` arrow -->
906+
<path
907+
v-else
908+
:d="`M 0 0 L ${layoutData.arrowSize} ${layoutData.arrowSize/2} L 0 ${layoutData.arrowSize} L ${layoutData.arrowSize / 3} ${layoutData.arrowSize / 2} Z`"
909+
:fill="color"
910+
:stroke="color"
911+
stroke-width="0"
912+
/>
911913
</marker>
912914
</template>
913915
</defs>
914916
915-
916917
<!-- Edges -->
917918
<g class="vue-ui-dag-edges">
918919
<template v-for="edge in layoutData.edges" :key="edge.id">
@@ -986,7 +987,7 @@ defineExpose({
986987
<!-- default label, multiline when provided with /n -->
987988
<text
988989
data-cy-node-label
989-
v-else
990+
v-else-if="!$slots['free-node-label']"
990991
:x="node.x"
991992
:y="node.y + FINAL_CONFIG.style.chart.nodes.labels.fontSize / 3"
992993
text-anchor="middle"
@@ -1003,6 +1004,9 @@ defineExpose({
10031004
autoOffset: true
10041005
})"
10051006
/>
1007+
<g v-if="$slots['free-node-label']">
1008+
<slot name="free-node-label" v-bind="{ node, layoutData }"/>
1009+
</g>
10061010
</template>
10071011
10081012
<!-- Full `node` slot to customize the node entirely using a div -->

src/useDag.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ export function useDag(options) {
105105
rawEdges.forEach(edge => {
106106
graphInstance.setEdge(edge.from, edge.to, {
107107
weight: edge.weight ?? 1,
108-
minlen: edge.minLength ?? 1
108+
minlen: edge.minLength ?? 1,
109+
arrowShape: finalConfiguration.arrowShape ?? 'normal'
109110
});
110111
});
111112

0 commit comments

Comments
 (0)