Skip to content

Commit c2093be

Browse files
committed
Improvement - VueUiDonut - Make datalabel markers for small arcs more elegant
1 parent 8d3b889 commit c2093be

File tree

3 files changed

+594
-224
lines changed

3 files changed

+594
-224
lines changed

TestingArena/ArenaVueUiDonut.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,12 @@ const model = createModel([
161161
NUMBER("style.chart.padding.right", { def: 24, min: 0, max: 100 }),
162162
NUMBER("style.chart.padding.bottom", { def: 0, min: 0, max: 100 }),
163163
NUMBER("style.chart.padding.left", { def: 24, min: 0, max: 100 }),
164-
CHECKBOX("style.chart.layout.curvedMarkers", { def: false }),
164+
CHECKBOX("style.chart.layout.curvedMarkers", { def: true }),
165165
CHECKBOX("style.chart.layout.labels.dataLabels.show", { def: true, label: "show", category: "labels" }),
166166
CHECKBOX("style.chart.layout.labels.dataLabels.oneLine", {def: true }),
167-
NUMBER("style.chart.layout.labels.dataLabels.hideUnderValue", { def: 3, min: 0, max: 100, label: "hideUnderValue", category: "labels" }),
167+
NUMBER("style.chart.layout.labels.dataLabels.hideUnderValue", { def: 0, min: 0, max: 100, label: "hideUnderValue", category: "labels" }),
168168
NUMBER("style.chart.layout.labels.dataLabels.smallArcClusterThreshold", { def: 8, min: 0, max: 100 }),
169+
NUMBER("style.chart.layout.labels.dataLabels.smallArcClusterFontSize", { def: 12, min: 8, max: 48 }),
169170
CHECKBOX("style.chart.layout.labels.dataLabels.useLabelSlots", { def: false }),
170171
TEXT("style.chart.layout.labels.dataLabels.prefix", { def: "", label: "prefix", category: "labels" }),
171172
TEXT("style.chart.layout.labels.dataLabels.suffix", { def: "", label: "suffix", category: "labels" }),

src/components/vue-ui-donut.vue

Lines changed: 14 additions & 222 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { useNestedProp } from "../useNestedProp";
5252
import { useThemeCheck } from "../useThemeCheck";
5353
import { useUserOptionState } from "../useUserOptionState";
5454
import { useChartAccessibility } from "../useChartAccessibility";
55+
import { useSmallArcLayoutsClassic } from "../useSmallArcLayouts";
5556
import img from "../img";
5657
import Shape from "../atoms/Shape.vue";
5758
import Title from "../atoms/Title.vue"; // Must be ready in responsive mode
@@ -968,230 +969,21 @@ function isSmallArc(arc, seriesIndex) {
968969
);
969970
}
970971
971-
const smallArcLayoutsClassic = computed(() => {
972-
if (FINAL_CONFIG.value.type !== "classic") return {};
973-
974-
const layouts = {};
975-
const arcs = noGhostDonut.value || [];
976-
if (!arcs.length) return layouts;
977-
978-
const configObject = FINAL_CONFIG.value;
979-
const centerX = svg.value.width / 2;
980-
const centerY = svg.value.height / 2;
981-
982-
const topPadding = padding.value.top + 16;
983-
const bottomPadding = svg.value.height - padding.value.bottom - 16;
984-
985-
// Base height for a single small label (percentage + one name line)
986-
const baseLineHeight = labels_inline_fontSize.value * 1.5;
987-
988-
const markerTextGap = 8;
989-
const radialOffset = 6;
990-
991-
const leftBandMarkerX = centerX - (minSize.value + radialOffset);
992-
const rightBandMarkerX = centerX + (minSize.value + radialOffset);
993-
994-
const isCurved = !!configObject.style.chart.layout.curvedMarkers;
995-
996-
function makeConnectorPathForSmallArcs({ midX, midY, bandX, bandY }) {
997-
if (!isCurved) {
998-
const elbowX = midX;
999-
const elbowY = bandY;
1000-
return `M ${midX} ${midY} L ${elbowX} ${elbowY} L ${bandX} ${bandY}`;
1001-
}
1002-
1003-
const side = bandX < centerX ? -1 : 1;
1004-
const offset = 12;
1005-
1006-
const controlPointX = (midX + bandX) / 2 + side * offset;
1007-
const controlPointY = midY + (bandY - midY) * 0.5;
1008-
1009-
return `M ${midX} ${midY} Q ${controlPointX} ${controlPointY} ${bandX} ${bandY}`;
1010-
}
1011-
1012-
// All small arcs (top and bottom), excluding segregated / animating ones
1013-
const candidates = arcs
1014-
.map((arc, index) => {
1015-
const { x: midX, y: midY } = findArcMidpoint(arc.path);
1016-
const inlineMarkerX = calcMarkerOffsetX(arc).x;
1017-
const inlineMarkerY = calcMarkerOffsetY(arc) - 3.5;
1018-
1019-
const nameLines = String(arc.name ?? "").split(/\n/g);
1020-
const extraNameLines = Math.max(0, nameLines.length - 1);
1021-
1022-
const lineHeight = labels_inline_fontSize.value * 1.2;
1023-
const extraHeight = extraNameLines * lineHeight;
1024-
1025-
const labelHeight = baseLineHeight + extraHeight;
1026-
1027-
return {
1028-
arc,
1029-
index,
1030-
midX,
1031-
midY,
1032-
inlineMarkerX,
1033-
inlineMarkerY,
1034-
labelHeight,
1035-
};
1036-
})
1037-
.filter(({ arc }) => {
1038-
const seriesIndex = arc.seriesIndex ?? 0;
1039-
1040-
// Do not include arcs that are being animated or are segregated
1041-
if (animatingIndex.value === seriesIndex) return false;
1042-
if (segregated.value.includes(seriesIndex)) return false;
1043-
1044-
return isSmallArc(arc, seriesIndex);
1045-
});
1046-
1047-
const topLeftCandidates = [];
1048-
const topRightCandidates = [];
1049-
const bottomLeftCandidates = [];
1050-
const bottomRightCandidates = [];
1051-
1052-
candidates.forEach(candidate => {
1053-
const isTop = candidate.inlineMarkerY < centerY;
1054-
const isLeft = candidate.inlineMarkerX < centerX;
1055-
1056-
if (isTop && isLeft) topLeftCandidates.push(candidate);
1057-
else if (isTop && !isLeft) topRightCandidates.push(candidate);
1058-
else if (!isTop && isLeft) bottomLeftCandidates.push(candidate);
1059-
else bottomRightCandidates.push(candidate);
1060-
});
1061-
1062-
const sortByVerticalPositionAscending = (a, b) =>
1063-
a.inlineMarkerY - b.inlineMarkerY || a.index - b.index;
1064-
1065-
const sortByVerticalPositionDescending = (a, b) =>
1066-
b.inlineMarkerY - a.inlineMarkerY || a.index - b.index;
1067-
1068-
topLeftCandidates.sort(sortByVerticalPositionAscending);
1069-
topRightCandidates.sort(sortByVerticalPositionAscending);
1070-
bottomLeftCandidates.sort(sortByVerticalPositionDescending);
1071-
bottomRightCandidates.sort(sortByVerticalPositionDescending);
1072-
1073-
// TOP LEFT BAND (always clustered if any)
1074-
let currentTopLeftY = topPadding;
1075-
topLeftCandidates.forEach(candidate => {
1076-
const { index, midX, midY, labelHeight } = candidate;
1077-
1078-
const labelY = currentTopLeftY;
1079-
const bandMarkerX = leftBandMarkerX;
1080-
const bandMarkerY = labelY;
1081-
1082-
const connectorPath = makeConnectorPathForSmallArcs({
1083-
midX,
1084-
midY,
1085-
bandX: bandMarkerX,
1086-
bandY: bandMarkerY,
1087-
});
1088-
1089-
layouts[index] = {
1090-
side: "left",
1091-
labelX: bandMarkerX - markerTextGap,
1092-
labelY: labelY + labels_inline_fontSize.value / 3,
1093-
textAnchor: "end",
1094-
markerX: bandMarkerX,
1095-
markerY: bandMarkerY,
1096-
connectorPath,
1097-
};
1098-
1099-
currentTopLeftY += labelHeight;
1100-
});
1101-
1102-
// TOP RIGHT BAND (always clustered if any)
1103-
let currentTopRightY = topPadding;
1104-
topRightCandidates.forEach(candidate => {
1105-
const { index, midX, midY, labelHeight } = candidate;
1106-
1107-
const labelY = currentTopRightY;
1108-
const bandMarkerX = rightBandMarkerX;
1109-
const bandMarkerY = labelY;
1110-
1111-
const connectorPath = makeConnectorPathForSmallArcs({
1112-
midX,
1113-
midY,
1114-
bandX: bandMarkerX,
1115-
bandY: bandMarkerY,
1116-
});
1117-
1118-
layouts[index] = {
1119-
side: "right",
1120-
labelX: bandMarkerX + markerTextGap,
1121-
labelY: labelY + labels_inline_fontSize.value / 3,
1122-
textAnchor: "start",
1123-
markerX: bandMarkerX,
1124-
markerY: bandMarkerY,
1125-
connectorPath,
1126-
};
1127-
1128-
currentTopRightY += labelHeight;
1129-
});
1130-
1131-
// BOTTOM LEFT BAND
1132-
if (bottomLeftCandidates.length > 1) {
1133-
let currentBottomLeftY = bottomPadding;
1134-
bottomLeftCandidates.forEach(candidate => {
1135-
const { index, midX, midY, labelHeight } = candidate;
1136-
1137-
currentBottomLeftY -= labelHeight;
1138-
const labelY = currentBottomLeftY;
1139-
const bandMarkerX = leftBandMarkerX;
1140-
const bandMarkerY = labelY;
1141-
1142-
const connectorPath = makeConnectorPathForSmallArcs({
1143-
midX,
1144-
midY,
1145-
bandX: bandMarkerX,
1146-
bandY: bandMarkerY,
1147-
});
1148-
1149-
layouts[index] = {
1150-
side: "left",
1151-
labelX: bandMarkerX - markerTextGap,
1152-
labelY: labelY + labels_inline_fontSize.value / 3,
1153-
textAnchor: "end",
1154-
markerX: bandMarkerX,
1155-
markerY: bandMarkerY,
1156-
connectorPath,
1157-
};
1158-
});
1159-
}
1160-
1161-
// BOTTOM RIGHT BAND
1162-
if (bottomRightCandidates.length > 1) {
1163-
let currentBottomRightY = bottomPadding;
1164-
bottomRightCandidates.forEach(candidate => {
1165-
const { index, midX, midY, labelHeight } = candidate;
1166-
1167-
currentBottomRightY -= labelHeight;
1168-
const labelY = currentBottomRightY;
1169-
const bandMarkerX = rightBandMarkerX;
1170-
const bandMarkerY = labelY;
1171-
1172-
const connectorPath = makeConnectorPathForSmallArcs({
1173-
midX,
1174-
midY,
1175-
bandX: bandMarkerX,
1176-
bandY: bandMarkerY,
1177-
});
1178-
1179-
layouts[index] = {
1180-
side: "right",
1181-
labelX: bandMarkerX + markerTextGap,
1182-
labelY: labelY + labels_inline_fontSize.value / 3,
1183-
textAnchor: "start",
1184-
markerX: bandMarkerX,
1185-
markerY: bandMarkerY,
1186-
connectorPath,
1187-
};
1188-
});
1189-
}
1190-
1191-
return layouts
972+
const { smallArcLayoutsClassic } = useSmallArcLayoutsClassic({
973+
FINAL_CONFIG,
974+
noGhostDonut,
975+
svg,
976+
padding,
977+
labels_inline_fontSize,
978+
minSize,
979+
findArcMidpoint,
980+
calcMarkerOffsetX,
981+
calcMarkerOffsetY,
982+
animatingIndex,
983+
segregated,
984+
isSmallArc
1192985
});
1193986
1194-
1195987
function displayArcPercentage(arc, stepBreakdown) {
1196988
const p = arc.value / sumValues(stepBreakdown);
1197989
return isNaN(p) ? 0 : applyDataLabel(

0 commit comments

Comments
 (0)