Skip to content

Commit 90851c2

Browse files
committed
Improvement - VueUiCirclePack - Improve placement algo, add #data-label slot, add @selectDatapoint emit
1 parent 49387b4 commit 90851c2

File tree

1 file changed

+68
-55
lines changed

1 file changed

+68
-55
lines changed

src/components/vue-ui-circle-pack.vue

Lines changed: 68 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
convertColorToHex,
99
convertCustomPalette,
1010
createCsvContent,
11+
createTSpans,
1112
createUid,
1213
darkenHexColor,
1314
dataLabel,
@@ -46,6 +47,8 @@ const props = defineProps({
4647
},
4748
});
4849
50+
const emit = defineEmits(['selectDatapoint']);
51+
4952
const { vue_ui_circle_pack: DEFAULT_CONFIG } = useConfig();
5053
5154
const isDataset = computed(() => {
@@ -151,10 +154,10 @@ function findInitialPosition(placedCircles, radius, width, height) {
151154
const spacing = radius * 2;
152155
153156
for (let circle of placedCircles) {
154-
for (let angle = 0; angle < 360; angle += 30) {
157+
for (let angle = 0; angle < 360; angle += 1) {
155158
let rad = (angle * Math.PI) / 180;
156159
let x = circle.x + spacing * Math.cos(rad);
157-
let y = circle.y + spacing * Math.sin(rad);
160+
let y = circle.y - spacing * Math.sin(rad);
158161
159162
let candidate = { x, y, radius };
160163
@@ -169,16 +172,22 @@ function findInitialPosition(placedCircles, radius, width, height) {
169172
}
170173
}
171174
}
172-
173175
return null;
174176
}
175177
176-
function packCircles(dp, width, height, maxRadius, offsetX = 0, offsetY = 0) {
177-
const maxDataPoint = Math.max(...dp.map(d => d.value));
178-
179-
const radii = dp.map((d, index) => ({
180-
...d,
181-
radius: (d.value / maxDataPoint) * maxRadius,
178+
function packCircles({
179+
datapoints,
180+
width,
181+
height,
182+
maxRadius,
183+
offsetX = 0,
184+
offsetY = 0
185+
}) {
186+
const maxDataPoint = Math.max(...datapoints.map(dp => dp.value));
187+
188+
const radii = datapoints.map((dp, index) => ({
189+
...dp,
190+
radius: (dp.value / maxDataPoint) * maxRadius,
182191
index
183192
}));
184193
@@ -195,11 +204,11 @@ function packCircles(dp, width, height, maxRadius, offsetX = 0, offsetY = 0) {
195204
});
196205
197206
for (let circleData of sortedCircles.slice(1)) {
198-
let { radius, ...el } = circleData;
207+
let { radius, ...dp } = circleData;
199208
let position = findInitialPosition(placedCircles, radius, width, height);
200209
201210
if (position) {
202-
placedCircles.push({ ...el, x: position.x, y: position.y, radius });
211+
placedCircles.push({ ...dp, x: position.x, y: position.y, radius });
203212
} else {
204213
let bestFit = null;
205214
let minOverlap = Infinity;
@@ -210,7 +219,7 @@ function packCircles(dp, width, height, maxRadius, offsetX = 0, offsetY = 0) {
210219
let x = circle.x + (radius + circle.radius) * Math.cos(rad);
211220
let y = circle.y + (radius + circle.radius) * Math.sin(rad);
212221
213-
let candidate = { ...el, x, y, radius };
222+
let candidate = { ...dp, x, y, radius };
214223
215224
if (
216225
x - radius >= 0 &&
@@ -263,13 +272,12 @@ function calcOffsetY(radius, offset) {
263272
264273
async function packSingleSet() {
265274
circles.value =
266-
packCircles(
267-
formattedDataset.value,
268-
// Huge plane size to place all datapoints
269-
10000,
270-
10000,
271-
32
272-
);
275+
packCircles({
276+
datapoints: formattedDataset.value,
277+
width: 10000,
278+
height: 10000,
279+
maxRadius: 32
280+
});
273281
}
274282
275283
const svg = computed(() => {
@@ -665,6 +673,7 @@ defineExpose({
665673
:rx="circle.radius"
666674
@mouseenter="() => zoomTo(circle)"
667675
@mouseout="zoom = null"
676+
@click="emit('selectDatapoint', circle)"
668677
/>
669678
<rect
670679
v-if="$slots.pattern"
@@ -681,41 +690,45 @@ defineExpose({
681690
}"
682691
/>
683692
684-
<!-- LABEL NAME -->
685-
<text
686-
v-if="FINAL_CONFIG.style.chart.circles.labels.name.show && circle.name"
687-
:style="{
688-
pointerEvents: 'none',
689-
transition: 'opacity 0.2s ease-in-out'
690-
}"
691-
:opacity="zoom ? 0.2 : 1"
692-
:x="circle.x"
693-
:y="circle.y + calcOffsetY(circle.radius, FINAL_CONFIG.style.chart.circles.labels.name.offsetY) - circle.radius / 6"
694-
:font-size="(circle.radius / 3) * FINAL_CONFIG.style.chart.circles.labels.name.fontSizeRatio"
695-
:fill="!FINAL_CONFIG.style.chart.circles.labels.name.color ? adaptColorToBackground(circle.color) : FINAL_CONFIG.style.chart.circles.labels.name.color"
696-
:font-weight="FINAL_CONFIG.style.chart.circles.labels.name.bold ? 'bold' : 'normal'"
697-
text-anchor="middle"
698-
>
699-
{{ circle.name }}
700-
</text>
701-
702-
<!-- LABEL VALUE -->
703-
<text
704-
v-if="FINAL_CONFIG.style.chart.circles.labels.value.show"
705-
:style="{
706-
pointerEvents: 'none',
707-
transition: 'opacity 0.2s ease-in-out'
708-
}"
709-
:opacity="zoom ? 0.2 : 1"
710-
:x="circle.x"
711-
:y="circle.y + calcOffsetY(circle.radius, FINAL_CONFIG.style.chart.circles.labels.value.offsetY) + circle.radius / 3"
712-
:font-size="getValueFontSize(circle) * FINAL_CONFIG.style.chart.circles.labels.value.fontSizeRatio"
713-
:fill="!FINAL_CONFIG.style.chart.circles.labels.value.color ? adaptColorToBackground(circle.color) : FINAL_CONFIG.style.chart.circles.labels.value.color"
714-
:font-weight="FINAL_CONFIG.style.chart.circles.labels.value.bold ? 'bold' : 'normal'"
715-
text-anchor="middle"
716-
>
717-
{{ getCircleLabel(circle) }}
718-
</text>
693+
<slot name="data-label" v-if="$slots['data-label']" v-bind="{ ...circle, createTSpans, fontSize: { name: (circle.radius / 3) * FINAL_CONFIG.style.chart.circles.labels.name.fontSizeRatio, value: getValueFontSize(circle) * FINAL_CONFIG.style.chart.circles.labels.value.fontSizeRatio}, color: !FINAL_CONFIG.style.chart.circles.labels.name.color ? adaptColorToBackground(circle.color) : FINAL_CONFIG.style.chart.circles.labels.name.color }"/>
694+
695+
<template v-else>
696+
<!-- LABEL NAME -->
697+
<text
698+
v-if="FINAL_CONFIG.style.chart.circles.labels.name.show && circle.name"
699+
:style="{
700+
pointerEvents: 'none',
701+
transition: 'opacity 0.2s ease-in-out'
702+
}"
703+
:opacity="zoom ? 0.2 : 1"
704+
:x="circle.x"
705+
:y="circle.y + calcOffsetY(circle.radius, FINAL_CONFIG.style.chart.circles.labels.name.offsetY) - circle.radius / 6"
706+
:font-size="(circle.radius / 3) * FINAL_CONFIG.style.chart.circles.labels.name.fontSizeRatio"
707+
:fill="!FINAL_CONFIG.style.chart.circles.labels.name.color ? adaptColorToBackground(circle.color) : FINAL_CONFIG.style.chart.circles.labels.name.color"
708+
:font-weight="FINAL_CONFIG.style.chart.circles.labels.name.bold ? 'bold' : 'normal'"
709+
text-anchor="middle"
710+
>
711+
{{ circle.name }}
712+
</text>
713+
714+
<!-- LABEL VALUE -->
715+
<text
716+
v-if="FINAL_CONFIG.style.chart.circles.labels.value.show"
717+
:style="{
718+
pointerEvents: 'none',
719+
transition: 'opacity 0.2s ease-in-out'
720+
}"
721+
:opacity="zoom ? 0.2 : 1"
722+
:x="circle.x"
723+
:y="circle.y + calcOffsetY(circle.radius, FINAL_CONFIG.style.chart.circles.labels.value.offsetY) + circle.radius / 3"
724+
:font-size="getValueFontSize(circle) * FINAL_CONFIG.style.chart.circles.labels.value.fontSizeRatio"
725+
:fill="!FINAL_CONFIG.style.chart.circles.labels.value.color ? adaptColorToBackground(circle.color) : FINAL_CONFIG.style.chart.circles.labels.value.color"
726+
:font-weight="FINAL_CONFIG.style.chart.circles.labels.value.bold ? 'bold' : 'normal'"
727+
text-anchor="middle"
728+
>
729+
{{ getCircleLabel(circle) }}
730+
</text>
731+
</template>
719732
720733
<!-- DONUTS -->
721734
<template v-for="donut in donuts">
@@ -737,7 +750,7 @@ defineExpose({
737750
:r="currentRadius"
738751
:opacity="zoomOpacity"
739752
:stroke="FINAL_CONFIG.style.chart.circles.stroke"
740-
:fill="FINAL_CONFIG.style.chart.circles.gradient.show ? `url(#${zoom.id})`: zoom.color"
753+
:fill="FINAL_CONFIG.style.chart.circles.gradient.show ? `url(#${zoom.id})`: zoom.color"
741754
/>
742755
743756
<g v-if="$slots['zoom-label']" :style="{ pointerEvents: 'none' }">

0 commit comments

Comments
 (0)