[READ-ONLY] Mirror of https://github.com/graphieros/vue-data-ui. An open source user-empowering data visualization Vue 3 components library for eloquent data storytelling vue-data-ui.graphieros.com/
charts components-library dashboard data-storytelling data-visualization donut gauge heatmap quadrant radar rating-stars scatter screenshot skeleton-loader sparkline table vue-data-ui vue3 vuejs vuejs3
3

Configure Feed

Select the types of activity you want to include in your feed.

feat(VueUiCirclePack): add support for recursive children

graphieros (May 3, 2026, 9:44 AM +0200) 51a954fe 3743d0f2

+1177 -71
+67 -2
TestingArena/ArenaVueUiCirclePack.vue
··· 37 37 return datapoints; 38 38 } 39 39 40 - const dataset = ref(makeDs({ name: 'Pack 1', qty: 6, maxVal: 12000 })); 40 + // const dataset = ref(makeDs({ name: 'Pack 1', qty: 6, maxVal: 12000 })); 41 + 42 + const dataset = ref([ 43 + { 44 + name: 'Datapoint 4', 45 + value: 4.052934104580613, 46 + children: [ 47 + { 48 + name: 'Datapoint 4', 49 + value: 4.052934104580613, 50 + color: 'green', 51 + children: [ 52 + { 53 + name: 'Datapoint 4', 54 + value: 4.052934104580613, 55 + color: 'red', 56 + children: [ 57 + { 58 + name: 'Datapoint 4', 59 + value: 4.052934104580613, 60 + }, 61 + { 62 + name: 'Datapoint 5', 63 + value: 3.0087145243250264, 64 + }, 65 + { 66 + name: 'Datapoint 4', 67 + value: 4.052934104580613, 68 + }, 69 + { 70 + name: 'Datapoint 5', 71 + value: 3.0087145243250264, 72 + }, 73 + ], 74 + }, 75 + { 76 + name: 'Datapoint 5', 77 + value: 3.0087145243250264, 78 + }, 79 + ], 80 + }, 81 + { 82 + name: 'Datapoint 5', 83 + value: 3.0087145243250264, 84 + children: [ 85 + { 86 + name: 'Datapoint 4', 87 + value: 4.052934104580613, 88 + }, 89 + { 90 + name: 'Datapoint 5', 91 + value: 2.0087145243250264, 92 + }, 93 + { 94 + name: 'Datapoint 4', 95 + value: 4.052934104580613, 96 + }, 97 + ], 98 + }, 99 + ], 100 + }, 101 + { 102 + name: 'Datapoint 5', 103 + value: 3.0087145243250264, 104 + }, 105 + ]); 41 106 42 107 onMounted(() => { 43 108 // setTimeout(() => { ··· 94 159 TEXT('style.chart.title.text', { def: 'Title' }), 95 160 TEXT('style.chart.title.subtitle.text', { def: 'Subtitle' }), 96 161 COLOR('style.chart.circles.stroke', { def: '#000000' }), 97 - NUMBER('style.chart.circles.strokeWidth', { def: 1, min: 0, max: 12 }), 162 + NUMBER('style.chart.circles.strokeWidth', { def: 0, min: 0, max: 12 }), 98 163 CHECKBOX('style.chart.circles.gradient.show', { def: true }), 99 164 RANGE('style.chart.circles.gradient.intensity', { 100 165 def: 40,
+2 -2
manual-testing/backend/database.json
··· 1279 1279 "description": "Implement nested circle packing. Use scoreCircle for each parent", 1280 1280 "priority": 0, 1281 1281 "author": "graphieros", 1282 - "done": false, 1282 + "done": true, 1283 1283 "exchanges": [], 1284 1284 "type": "feature", 1285 1285 "withComponentCheckList": false, ··· 1288 1288 "customCheckList": {}, 1289 1289 "id": "fa6454ad-e3e6-4d3c-8019-8ad0eedd296e", 1290 1290 "createdAt": 1776606115056, 1291 - "updatedAt": 1776606115056 1291 + "updatedAt": 1777793416469 1292 1292 }, 1293 1293 { 1294 1294 "title": "Stepper",
+909 -65
src/components/vue-ui-circle-pack.vue
··· 45 45 import Title from '../atoms/Title.vue'; // Must be ready in responsive mode 46 46 import themes from '../themes/vue_ui_circle_pack.json'; 47 47 import BaseScanner from '../atoms/BaseScanner.vue'; 48 + import usePanZoom from '../usePanZoom'; 48 49 import A11yDataTable from '../atoms/A11yDataTable.vue'; 49 50 50 51 const Tooltip = defineAsyncComponent(() => import('../atoms/Tooltip.vue')); ··· 206 207 207 208 // Reset mutable config 208 209 mutableConfig.value.showTable = FINAL_CONFIG.value.table.show; 210 + mutableConfig.value.showTooltip = 211 + FINAL_CONFIG.value.style.chart.tooltip.show; 212 + mutableConfig.value.showZoom = 213 + FINAL_CONFIG.value.style.chart.zoom?.show ?? false; 209 214 }, 210 215 { deep: true }, 211 216 ); ··· 226 231 const mutableConfig = ref({ 227 232 showTable: FINAL_CONFIG.value.table.show, 228 233 showTooltip: FINAL_CONFIG.value.style.chart.tooltip.show, 234 + showZoom: FINAL_CONFIG.value.style.chart.zoom?.show ?? false, 229 235 }); 230 236 231 237 // v3 - Essential to make shifting between loading config and final config work ··· 235 241 mutableConfig.value = { 236 242 showTable: FINAL_CONFIG.value.table.show, 237 243 showTooltip: FINAL_CONFIG.value.style.chart.tooltip.show, 244 + showZoom: FINAL_CONFIG.value.style.chart.zoom?.show ?? false, 238 245 }; 239 246 }, 240 247 { immediate: true }, ··· 268 275 }); 269 276 270 277 const circles = ref([]); 271 - const viewBox = ref('0 0 100 100'); 278 + const flattenedDataset = ref([]); 272 279 const resizeObserver = ref(null); 273 280 const observedEl = ref(null); 274 281 ··· 278 285 h: targetHeight, 279 286 }; 280 287 281 - const freshDataset = formattedDataset.value.map((c) => ({ ...c })); 288 + const freshDataset = formattedDataset.value.map(cloneDatapoint); 289 + 282 290 if (!freshDataset.length) { 291 + const baseViewBox = { 292 + x: 0, 293 + y: 0, 294 + width: targetWidth, 295 + height: targetHeight, 296 + }; 297 + 283 298 circles.value = []; 284 - boundValues.value = [0, 0, targetWidth, targetHeight]; 285 - viewBox.value = `0 0 ${targetWidth} ${targetHeight}`; 299 + flattenedDataset.value = []; 300 + boundValues.value = [ 301 + baseViewBox.x, 302 + baseViewBox.y, 303 + baseViewBox.width, 304 + baseViewBox.height, 305 + ]; 306 + setInitialViewBox(baseViewBox); 286 307 return; 287 308 } 288 309 289 - const packed = pack(freshDataset, targetHeight, targetWidth); 290 - const [x0, y0, bw, bh] = bounds(packed, 1); 310 + const packedRoots = pack(freshDataset, targetHeight, targetWidth); 311 + const [x0, y0, bw, bh] = bounds(packedRoots, 1); 291 312 292 - // Uniform scale to fit raw bounds into targetWidth & targetHeight 293 313 const scale = Math.min( 294 314 bw ? targetWidth / bw : 1, 295 315 bh ? targetHeight / bh : 1, 296 316 ); 317 + 297 318 const offsetX = (targetWidth - bw * scale) / 2; 298 319 const offsetY = (targetHeight - bh * scale) / 2; 299 - circles.value = packed.map((c) => ({ 300 - ...c, 301 - x: (c.x - x0) * scale + offsetX, 302 - y: (c.y - y0) * scale + offsetY, 303 - r: c.r * scale, 320 + 321 + const roots = packedRoots.map((circle) => { 322 + return { 323 + ...circle, 324 + x: (circle.x - x0) * scale + offsetX, 325 + y: (circle.y - y0) * scale + offsetY, 326 + r: circle.r * scale, 327 + }; 328 + }); 329 + 330 + circles.value = flattenPackedCircles(roots); 331 + flattenedDataset.value = circles.value; 332 + 333 + const baseViewBox = { 334 + x: 0, 335 + y: 0, 336 + width: targetWidth, 337 + height: targetHeight, 338 + }; 339 + 340 + const initialViewBox = getViewBoxIncludingParentTooltips(baseViewBox); 341 + 342 + boundValues.value = [ 343 + initialViewBox.x, 344 + initialViewBox.y, 345 + initialViewBox.width, 346 + initialViewBox.height, 347 + ]; 348 + 349 + setInitialViewBox(initialViewBox); 350 + } 351 + 352 + function cloneDatapoint(datapoint) { 353 + return { 354 + ...datapoint, 355 + children: Array.isArray(datapoint.children) 356 + ? datapoint.children.map(cloneDatapoint) 357 + : [], 358 + }; 359 + } 360 + 361 + function getDatapointValue(datapoint) { 362 + const ownValue = Number(datapoint?.value); 363 + if (Number.isFinite(ownValue) && ownValue > 0) { 364 + return ownValue; 365 + } 366 + 367 + if (!Array.isArray(datapoint?.children)) { 368 + return 0; 369 + } 370 + 371 + return datapoint.children.reduce((sum, child) => { 372 + return sum + getDatapointValue(child); 373 + }, 0); 374 + } 375 + 376 + function getDatapointColor(datapoint, colorIndex) { 377 + const themePalette = 378 + themePalettes[FINAL_CONFIG.value.theme || 'default'] || palette; 379 + const resolvedPalette = customPalette.value.length 380 + ? customPalette.value 381 + : themePalette.length 382 + ? themePalette 383 + : palette; 384 + 385 + return ( 386 + convertColorToHex(datapoint.color) || 387 + resolvedPalette[colorIndex % resolvedPalette.length] || 388 + palette[colorIndex % palette.length] 389 + ); 390 + } 391 + 392 + function getLeafCount(datapoint) { 393 + if (!Array.isArray(datapoint.children) || !datapoint.children.length) { 394 + return 1; 395 + } 396 + 397 + return datapoint.children.reduce((sum, child) => { 398 + return sum + getLeafCount(child); 399 + }, 0); 400 + } 401 + 402 + function normalizeDataset(source, parent = null, depth = 0, path = []) { 403 + if (!Array.isArray(source)) { 404 + return []; 405 + } 406 + 407 + return source 408 + .map((datapoint, index) => { 409 + const value = getDatapointValue(datapoint); 410 + 411 + if (!Number.isFinite(value) || value <= 0) { 412 + return null; 413 + } 414 + 415 + const id = createUid(); 416 + const hierarchyPath = [...path, index]; 417 + const colorIndex = hierarchyPath.reduce((sum, item, i) => { 418 + return sum + item + i; 419 + }, 0); 420 + 421 + const color = 422 + datapoint.color || parent?.color 423 + ? convertColorToHex(datapoint.color || parent?.color) 424 + : getDatapointColor(datapoint, colorIndex); 425 + 426 + const children = normalizeDataset( 427 + datapoint.children, 428 + { 429 + id, 430 + name: datapoint.name, 431 + color, 432 + rootId: parent?.rootId ?? id, 433 + }, 434 + depth + 1, 435 + hierarchyPath, 436 + ); 437 + 438 + return { 439 + ...datapoint, 440 + value, 441 + r: value, 442 + id, 443 + color, 444 + depth, 445 + parentId: parent?.id ?? null, 446 + parentName: parent?.name ?? null, 447 + rootId: parent?.rootId ?? id, 448 + hasChildren: children.length > 0, 449 + childCount: children.length, 450 + leafCount: children.length ? getLeafCount({ children }) : 1, 451 + hierarchyPath, 452 + children, 453 + }; 454 + }) 455 + .filter(Boolean); 456 + } 457 + 458 + function packNestedChildren(parent) { 459 + if (!Array.isArray(parent.children) || !parent.children.length) { 460 + return []; 461 + } 462 + 463 + const innerPadding = 0.9; 464 + const targetSize = parent.r * 2; 465 + 466 + if (!targetSize) { 467 + return []; 468 + } 469 + 470 + const packed = pack( 471 + parent.children.map(cloneDatapoint), 472 + targetSize, 473 + targetSize, 474 + ); 475 + 476 + const [x0, y0, width, height] = bounds(packed, 1); 477 + 478 + const scale = 479 + Math.min( 480 + width ? targetSize / width : 1, 481 + height ? targetSize / height : 1, 482 + ) * innerPadding; 483 + 484 + const fittedWidth = width * scale; 485 + const fittedHeight = height * scale; 486 + 487 + const offsetX = parent.x - fittedWidth / 2; 488 + const offsetY = parent.y - fittedHeight / 2; 489 + 490 + return packed.map((child) => ({ 491 + ...child, 492 + x: (child.x - x0) * scale + offsetX, 493 + y: (child.y - y0) * scale + offsetY, 494 + r: child.r * scale, 304 495 })); 496 + } 305 497 306 - boundValues.value = [0, 0, targetWidth, targetHeight]; 307 - viewBox.value = `0 0 ${targetWidth} ${targetHeight}`; 498 + function flattenPackedCircles(source) { 499 + const result = []; 500 + 501 + source.forEach((circle) => { 502 + const children = packNestedChildren(circle); 503 + result.push({ 504 + ...circle, 505 + children, 506 + }); 507 + result.push(...flattenPackedCircles(children)); 508 + }); 509 + 510 + return result; 511 + } 512 + 513 + function isDescendantOf(circle, possibleParent) { 514 + if (!circle || !possibleParent) return false; 515 + if (!Array.isArray(circle.hierarchyPath)) return false; 516 + if (!Array.isArray(possibleParent.hierarchyPath)) return false; 517 + if (circle.hierarchyPath.length <= possibleParent.hierarchyPath.length) 518 + return false; 519 + 520 + return possibleParent.hierarchyPath.every((pathItem, index) => { 521 + return circle.hierarchyPath[index] === pathItem; 522 + }); 523 + } 524 + 525 + function canShowCircleLabel(circle) { 526 + if (circle.hasChildren) { 527 + return FINAL_CONFIG.value.style.chart.circles.labels.parents.show; 528 + } 529 + return FINAL_CONFIG.value.style.chart.circles.labels.children.show; 308 530 } 309 531 310 532 function setupResponsiveObserver() { ··· 398 620 return convertCustomPalette(FINAL_CONFIG.value.customPalette); 399 621 }); 400 622 401 - const formattedDataset = computed(() => { 402 - return FINAL_DATASET.value 403 - .map((ds, i) => { 404 - const color = 405 - convertColorToHex(ds.color) || 406 - customPalette.value[i] || 407 - themePalettes[FINAL_CONFIG.value.theme || 'default'][ 408 - i % 409 - themePalettes[FINAL_CONFIG.value.theme || 'default'] 410 - .length 411 - ] || 412 - palette[i] || 413 - palette[i % palette.length]; 414 - 415 - return { 416 - ...ds, 417 - r: ds.value, 418 - id: createUid(), 419 - color, 420 - }; 421 - }) 422 - .filter( 423 - (ds) => ![null, undefined, Infinity, -Infinity].includes(ds.value), 424 - ); 425 - }); 623 + const formattedDataset = computed(() => normalizeDataset(FINAL_DATASET.value)); 426 624 427 625 const maxRadius = computed(() => { 428 626 return circles.value.length ··· 473 671 datapoint, 474 672 seriesIndex, 475 673 config: FINAL_CONFIG.value, 476 - series: formattedDataset.value, 674 + series: flattenedDataset.value, 477 675 }; 478 676 isTooltip.value = true; 479 677 const customFormat = FINAL_CONFIG.value.style.chart.tooltip.customFormat; ··· 484 682 const customFormatString = customFormat({ 485 683 seriesIndex, 486 684 datapoint, 487 - series: formattedDataset.value, 685 + series: flattenedDataset.value, 488 686 config: FINAL_CONFIG.value, 489 687 }); 490 688 if (typeof customFormatString === 'string') { ··· 552 750 isAnnotator.value = !isAnnotator.value; 553 751 } 554 752 753 + const active = computed( 754 + () => !isAnnotator.value && mutableConfig.value.showZoom, 755 + ); 756 + 757 + const { viewBox, resetZoom, isZoom, setInitialViewBox } = usePanZoom( 758 + svgRef, 759 + { 760 + x: 0, 761 + y: 0, 762 + width: Math.max(10, SIZE.value.w), 763 + height: Math.max(10, SIZE.value.h), 764 + }, 765 + FINAL_CONFIG.value.style.chart.zoom?.speed ?? 1, 766 + active, 767 + ); 768 + 769 + function rectanglesCollide(a, b, gap = 0) { 770 + return !( 771 + a.x + a.width + gap < b.x || 772 + b.x + b.width + gap < a.x || 773 + a.y + a.height + gap < b.y || 774 + b.y + b.height + gap < a.y 775 + ); 776 + } 777 + 778 + function rectangleCollidesWithCircle(rectangle, circle, gap = 0) { 779 + const closestX = Math.min( 780 + Math.max(circle.x, rectangle.x - gap), 781 + rectangle.x + rectangle.width + gap, 782 + ); 783 + const closestY = Math.min( 784 + Math.max(circle.y, rectangle.y - gap), 785 + rectangle.y + rectangle.height + gap, 786 + ); 787 + const distanceX = circle.x - closestX; 788 + const distanceY = circle.y - closestY; 789 + return distanceX * distanceX + distanceY * distanceY <= circle.r * circle.r; 790 + } 791 + 792 + function clampRectangleToViewBox(rectangle, sourceViewBox) { 793 + const margin = Math.max( 794 + 4, 795 + Math.min(sourceViewBox.width, sourceViewBox.height) * 0.01, 796 + ); 797 + 798 + return { 799 + ...rectangle, 800 + x: Math.min( 801 + Math.max(rectangle.x, sourceViewBox.x + margin), 802 + sourceViewBox.x + sourceViewBox.width - rectangle.width - margin, 803 + ), 804 + y: Math.min( 805 + Math.max(rectangle.y, sourceViewBox.y + margin), 806 + sourceViewBox.y + sourceViewBox.height - rectangle.height - margin, 807 + ), 808 + }; 809 + } 810 + 811 + function getParentTooltipTextLines(circle) { 812 + const lines = [circle.name]; 813 + if (FINAL_CONFIG.value.style.chart.circles.labels.value.show) { 814 + lines.push(getCircleLabel(circle)); 815 + } 816 + return lines.filter(Boolean); 817 + } 818 + 819 + function getParentTooltipCandidateRect( 820 + circle, 821 + width, 822 + height, 823 + placement, 824 + distance, 825 + ) { 826 + const centerX = circle.x; 827 + const centerY = circle.y; 828 + 829 + const placements = { 830 + right: { 831 + x: centerX + circle.r + distance, 832 + y: centerY - height / 2, 833 + anchorX: centerX + circle.r, 834 + anchorY: centerY, 835 + }, 836 + left: { 837 + x: centerX - circle.r - distance - width, 838 + y: centerY - height / 2, 839 + anchorX: centerX - circle.r, 840 + anchorY: centerY, 841 + }, 842 + top: { 843 + x: centerX - width / 2, 844 + y: centerY - circle.r - distance - height, 845 + anchorX: centerX, 846 + anchorY: centerY - circle.r, 847 + }, 848 + bottom: { 849 + x: centerX - width / 2, 850 + y: centerY + circle.r + distance, 851 + anchorX: centerX, 852 + anchorY: centerY + circle.r, 853 + }, 854 + topRight: { 855 + x: centerX + circle.r * 0.7 + distance, 856 + y: centerY - circle.r * 0.7 - distance - height, 857 + anchorX: centerX + circle.r * 0.7, 858 + anchorY: centerY - circle.r * 0.7, 859 + }, 860 + topLeft: { 861 + x: centerX - circle.r * 0.7 - distance - width, 862 + y: centerY - circle.r * 0.7 - distance - height, 863 + anchorX: centerX - circle.r * 0.7, 864 + anchorY: centerY - circle.r * 0.7, 865 + }, 866 + bottomRight: { 867 + x: centerX + circle.r * 0.7 + distance, 868 + y: centerY + circle.r * 0.7 + distance, 869 + anchorX: centerX + circle.r * 0.7, 870 + anchorY: centerY + circle.r * 0.7, 871 + }, 872 + bottomLeft: { 873 + x: centerX - circle.r * 0.7 - distance - width, 874 + y: centerY + circle.r * 0.7 + distance, 875 + anchorX: centerX - circle.r * 0.7, 876 + anchorY: centerY + circle.r * 0.7, 877 + }, 878 + }; 879 + 880 + return placements[placement]; 881 + } 882 + 883 + function getCircleAncestors(circle, circleById) { 884 + const ancestors = []; 885 + let parent = circleById.get(circle.parentId); 886 + while (parent) { 887 + ancestors.push(parent); 888 + parent = circleById.get(parent.parentId); 889 + } 890 + return ancestors; 891 + } 892 + 893 + function getParentTooltipExternalCandidateRect( 894 + circle, 895 + containerCircle, 896 + width, 897 + height, 898 + placement, 899 + distance, 900 + ) { 901 + const centerX = circle.x; 902 + const centerY = circle.y; 903 + const container = containerCircle || circle; 904 + const diagonalOffset = distance * 0.6; 905 + 906 + const placements = { 907 + right: { 908 + x: container.x + container.r + distance, 909 + y: centerY - height / 2, 910 + }, 911 + left: { 912 + x: container.x - container.r - distance - width, 913 + y: centerY - height / 2, 914 + }, 915 + top: { 916 + x: centerX - width / 2, 917 + y: container.y - container.r - distance - height, 918 + }, 919 + bottom: { 920 + x: centerX - width / 2, 921 + y: container.y + container.r + distance, 922 + }, 923 + topRight: { 924 + x: container.x + container.r + distance, 925 + y: container.y - container.r - distance - height - diagonalOffset, 926 + }, 927 + topLeft: { 928 + x: container.x - container.r - distance - width, 929 + y: container.y - container.r - distance - height - diagonalOffset, 930 + }, 931 + bottomRight: { 932 + x: container.x + container.r + distance, 933 + y: container.y + container.r + distance + diagonalOffset, 934 + }, 935 + bottomLeft: { 936 + x: container.x - container.r - distance - width, 937 + y: container.y + container.r + distance + diagonalOffset, 938 + }, 939 + }; 940 + 941 + const rectangle = placements[placement]; 942 + const targetX = rectangle.x + width / 2; 943 + const targetY = rectangle.y + height / 2; 944 + const angle = Math.atan2(targetY - centerY, targetX - centerX); 945 + 946 + return { 947 + ...rectangle, 948 + anchorX: centerX + Math.cos(angle) * circle.r, 949 + anchorY: centerY + Math.sin(angle) * circle.r, 950 + }; 951 + } 952 + 953 + function createParentTooltipItems(sourceViewBox, options = {}) { 954 + const { clamp = true } = options; 955 + 956 + if ( 957 + !circles.value.length || 958 + !SIZE.value.w || 959 + !SIZE.value.h || 960 + !FINAL_CONFIG.value.style.chart.parentTooltips.show 961 + ) { 962 + return []; 963 + } 964 + 965 + const fontSize = Math.max( 966 + 8, 967 + FINAL_CONFIG.value.style.chart.parentTooltips.fontSizeRatio * 10, 968 + ); 969 + const lineHeight = fontSize * 1.25; 970 + const paddingX = fontSize * 0.75; 971 + const paddingY = fontSize * 0.55; 972 + const distance = Math.max( 973 + 8, 974 + Math.min(sourceViewBox.width, sourceViewBox.height) * 0.025, 975 + ); 976 + const collisionGap = Math.max( 977 + 2, 978 + Math.min(sourceViewBox.width, sourceViewBox.height) * 0.006, 979 + ); 980 + 981 + const placements = [ 982 + 'right', 983 + 'left', 984 + 'top', 985 + 'bottom', 986 + 'topRight', 987 + 'topLeft', 988 + 'bottomRight', 989 + 'bottomLeft', 990 + ]; 991 + 992 + const tooltips = []; 993 + const circleById = new Map( 994 + circles.value.map((circle) => [circle.id, circle]), 995 + ); 996 + 997 + const parentCircles = circles.value 998 + .filter((circle) => circle.hasChildren && circle.name) 999 + .sort((a, b) => b.r - a.r); 1000 + 1001 + parentCircles.forEach((circle) => { 1002 + const ancestors = getCircleAncestors(circle, circleById); 1003 + const outermostAncestor = ancestors.at(-1); 1004 + const lines = getParentTooltipTextLines(circle); 1005 + 1006 + const longestLine = lines.reduce((max, line) => { 1007 + return Math.max(max, String(line).length); 1008 + }, 0); 1009 + 1010 + const width = Math.max( 1011 + fontSize * 5, 1012 + longestLine * fontSize * 0.62 + paddingX * 2, 1013 + ); 1014 + const height = lines.length * lineHeight + paddingY * 2; 1015 + 1016 + const externalCandidates = outermostAncestor 1017 + ? placements.map((placement) => 1018 + getParentTooltipExternalCandidateRect( 1019 + circle, 1020 + outermostAncestor, 1021 + width, 1022 + height, 1023 + placement, 1024 + distance, 1025 + ), 1026 + ) 1027 + : []; 1028 + 1029 + const localCandidates = placements.map((placement) => 1030 + getParentTooltipCandidateRect( 1031 + circle, 1032 + width, 1033 + height, 1034 + placement, 1035 + distance, 1036 + ), 1037 + ); 1038 + 1039 + const candidates = [...externalCandidates, ...localCandidates].map( 1040 + (raw) => { 1041 + const rectangle = clamp 1042 + ? clampRectangleToViewBox( 1043 + { 1044 + x: raw.x, 1045 + y: raw.y, 1046 + width, 1047 + height, 1048 + }, 1049 + sourceViewBox, 1050 + ) 1051 + : { 1052 + x: raw.x, 1053 + y: raw.y, 1054 + width, 1055 + height, 1056 + }; 1057 + 1058 + return { 1059 + ...rectangle, 1060 + anchorX: raw.anchorX, 1061 + anchorY: raw.anchorY, 1062 + lineX: rectangle.x + rectangle.width / 2, 1063 + lineY: rectangle.y + rectangle.height / 2, 1064 + }; 1065 + }, 1066 + ); 1067 + 1068 + const validCandidate = candidates.find((candidate) => { 1069 + const collidesWithPreviousTooltip = tooltips.some((tooltip) => 1070 + rectanglesCollide(candidate, tooltip, collisionGap), 1071 + ); 1072 + 1073 + if (collidesWithPreviousTooltip) return false; 1074 + 1075 + return !circles.value.some((otherCircle) => 1076 + rectangleCollidesWithCircle( 1077 + candidate, 1078 + otherCircle, 1079 + collisionGap, 1080 + ), 1081 + ); 1082 + }); 1083 + 1084 + if (!validCandidate) return; 1085 + 1086 + tooltips.push({ 1087 + ...validCandidate, 1088 + datapoint: circle, 1089 + id: `parent_tooltip_${circle.id}`, 1090 + color: circle.color, 1091 + lines, 1092 + fontSize, 1093 + lineHeight, 1094 + paddingX, 1095 + paddingY, 1096 + }); 1097 + }); 1098 + 1099 + return tooltips; 1100 + } 1101 + 1102 + const parentTooltipItems = computed(() => { 1103 + return createParentTooltipItems(viewBox.value, { 1104 + clamp: true, 1105 + }); 1106 + }); 1107 + 1108 + function getViewBoxIncludingParentTooltips(baseViewBox) { 1109 + const tooltips = createParentTooltipItems(baseViewBox, { 1110 + clamp: false, 1111 + }); 1112 + 1113 + if (!tooltips.length) { 1114 + return baseViewBox; 1115 + } 1116 + 1117 + const minX = Math.min( 1118 + baseViewBox.x, 1119 + ...tooltips.map((tooltip) => tooltip.x), 1120 + ); 1121 + const minY = Math.min( 1122 + baseViewBox.y, 1123 + ...tooltips.map((tooltip) => tooltip.y), 1124 + ); 1125 + const maxX = Math.max( 1126 + baseViewBox.x + baseViewBox.width, 1127 + ...tooltips.map((tooltip) => tooltip.x + tooltip.width), 1128 + ); 1129 + const maxY = Math.max( 1130 + baseViewBox.y + baseViewBox.height, 1131 + ...tooltips.map((tooltip) => tooltip.y + tooltip.height), 1132 + ); 1133 + 1134 + const margin = Math.max( 1135 + 8, 1136 + Math.min(baseViewBox.width, baseViewBox.height) * 0.025, 1137 + ); 1138 + 1139 + return { 1140 + x: minX - margin, 1141 + y: minY - margin, 1142 + width: maxX - minX + margin * 2, 1143 + height: maxY - minY + margin * 2, 1144 + }; 1145 + } 1146 + 1147 + function buildHierarchyRows(datapoints) { 1148 + const rows = []; 1149 + 1150 + function walk(items, depth = 0, parentName = '') { 1151 + items.forEach((datapoint) => { 1152 + rows.push({ 1153 + name: datapoint.name, 1154 + value: datapoint.value, 1155 + color: datapoint.color, 1156 + parentName, 1157 + depth, 1158 + }); 1159 + 1160 + if ( 1161 + Array.isArray(datapoint.children) && 1162 + datapoint.children.length 1163 + ) { 1164 + walk(datapoint.children, depth + 1, datapoint.name); 1165 + } 1166 + }); 1167 + } 1168 + 1169 + walk(datapoints); 1170 + return rows; 1171 + } 1172 + 555 1173 const table = computed(() => { 556 - const head = formattedDataset.value 557 - .map((d) => { 558 - return { 559 - name: d.name, 560 - value: d.value, 561 - color: d.color, 562 - }; 563 - }) 1174 + const hierarchy = buildHierarchyRows(formattedDataset.value); 1175 + 1176 + const head = flattenedDataset.value 1177 + .map((d) => ({ 1178 + name: d.name, 1179 + value: d.value, 1180 + color: d.color, 1181 + })) 564 1182 .toSorted((a, b) => b.value - a.value); 565 1183 566 - const body = head.map((h) => h.value); 567 - return { head, body }; 1184 + const body = hierarchy.map((row) => row.value); 1185 + 1186 + return { head, body, hierarchy }; 1187 + }); 1188 + 1189 + const csvTable = computed(() => { 1190 + return buildHierarchyRows(formattedDataset.value).map((row) => ({ 1191 + ...row, 1192 + name: `${'- '.repeat(row.depth)}${row.name}`, 1193 + })); 568 1194 }); 569 1195 570 1196 function generateCsv(callback = null) { 571 1197 nextTick(() => { 572 - const labels = table.value.head.map((h, i) => { 573 - return [[h.name], [table.value.body[i]]]; 1198 + const labels = csvTable.value.map((row) => { 1199 + return [ 1200 + [row.name], 1201 + [row.parentName || ''], 1202 + [row.depth], 1203 + [row.value], 1204 + ]; 574 1205 }); 1206 + 575 1207 const tableXls = [ 576 1208 [FINAL_CONFIG.value.style.chart.title.text], 577 1209 [FINAL_CONFIG.value.style.chart.title.subtitle.text], 578 - [[''], [FINAL_CONFIG.value.table.columnNames.value]], 1210 + [ 1211 + [FINAL_CONFIG.value.table.columnNames.datapoint], 1212 + ['Parent'], 1213 + ['Depth'], 1214 + [FINAL_CONFIG.value.table.columnNames.value], 1215 + ], 579 1216 ].concat(labels); 580 1217 581 1218 const csvContent = createCsvContent(tableXls); ··· 596 1233 const dataTable = computed(() => { 597 1234 const head = [ 598 1235 FINAL_CONFIG.value.table.columnNames.datapoint, 1236 + FINAL_CONFIG.value.table.columnNames.parent, 1237 + FINAL_CONFIG.value.table.columnNames.depth, 599 1238 FINAL_CONFIG.value.table.columnNames.value, 600 1239 ]; 601 1240 602 - const body = table.value.head.map((h, i) => { 1241 + const body = table.value.hierarchy.map((row) => { 603 1242 const label = dataLabel({ 604 1243 p: FINAL_CONFIG.value.style.chart.circles.labels.value.prefix, 605 - v: table.value.body[i], 1244 + v: row.value, 606 1245 s: FINAL_CONFIG.value.style.chart.circles.labels.value.suffix, 607 1246 r: FINAL_CONFIG.value.style.chart.circles.labels.value.rounding, 608 1247 }); 1248 + 609 1249 return [ 610 1250 { 611 - color: h.color, 612 - name: h.name, 1251 + color: row.color, 1252 + name: `${' '.repeat(row.depth)}${row.name}`, 613 1253 }, 1254 + row.parentName ?? '', 1255 + row.depth ?? 0, 614 1256 label, 615 1257 ]; 616 1258 }); ··· 631 1273 632 1274 const colNames = [ 633 1275 FINAL_CONFIG.value.table.columnNames.datapoint, 1276 + FINAL_CONFIG.value.table.columnNames.parent, 1277 + FINAL_CONFIG.value.table.columnNames.depth, 634 1278 FINAL_CONFIG.value.table.columnNames.value, 635 1279 ]; 636 1280 ··· 650 1294 mutableConfig.value.showTooltip = !mutableConfig.value.showTooltip; 651 1295 } 652 1296 1297 + function toggleZoom() { 1298 + mutableConfig.value.showZoom = !mutableConfig.value.showZoom; 1299 + } 1300 + 653 1301 function getData() { 654 1302 return formattedDataset.value; 655 1303 } ··· 784 1432 emit('copyAlt', { 785 1433 config: FINAL_CONFIG.value, 786 1434 dataset: formattedDataset.value, 1435 + flattenedDataset: flattenedDataset.value, 787 1436 }); 788 1437 if (!FINAL_CONFIG.value.userOptions.callbacks.altCopy) { 789 1438 console.warn( ··· 795 1444 FINAL_CONFIG.value.userOptions.callbacks.altCopy({ 796 1445 config: FINAL_CONFIG.value, 797 1446 dataset: formattedDataset.value, 1447 + flattenedDataset: flattenedDataset.value, 798 1448 }), 799 1449 ); 800 1450 } ··· 903 1553 return { headers, rows }; 904 1554 }); 905 1555 1556 + const textColor = computed(() => FINAL_CONFIG.value.style.chart.color); 1557 + 906 1558 defineExpose({ 907 1559 getData, 908 1560 getImage, ··· 914 1566 toggleAnnotator, 915 1567 toggleFullscreen, 916 1568 copyAlt, 1569 + toggleZoom, 1570 + resetZoom, 917 1571 }); 918 1572 </script> 919 1573 ··· 1031 1685 :isAnnotation="isAnnotator" 1032 1686 :tableDialog="FINAL_CONFIG.table.useDialog" 1033 1687 :isCursorPointer="isCursorPointer" 1688 + :hasZoom="FINAL_CONFIG.userOptions.buttons.zoom" 1689 + :isZoom="mutableConfig.showZoom" 1034 1690 @toggleFullscreen="toggleFullscreen" 1035 1691 @generatePdf="generatePdf" 1036 1692 @generateCsv="generateCsv" ··· 1039 1695 @toggleTable="toggleTable" 1040 1696 @toggleTooltip="toggleTooltip" 1041 1697 @toggleAnnotator="toggleAnnotator" 1698 + @toggleZoom="toggleZoom" 1042 1699 @copyAlt="copyAlt" 1043 1700 :style="{ 1044 1701 visibility: keepUserOptionState ··· 1088 1745 /> 1089 1746 </template> 1090 1747 <template 1748 + v-if="$slots.optionZoom" 1749 + #optionZoom="{ toggleZoom, isZoomLocked }" 1750 + > 1751 + <slot name="optionZoom" v-bind="{ toggleZoom, isZoomLocked }" /> 1752 + </template> 1753 + <template 1091 1754 v-if="$slots.optionAltCopy" 1092 1755 #optionAltCopy="{ altCopy: c }" 1093 1756 > ··· 1105 1768 ref="svgRef" 1106 1769 :xmlns="XMLNS" 1107 1770 :aria-describedby="`chart-instructions-${uid}`" 1108 - :viewBox="viewBox" 1771 + :viewBox="`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`" 1109 1772 preserveAspectRatio="xMidYMid meet" 1110 1773 :class="{ 1111 1774 'vue-data-ui-fullscreen--on': isFullscreen, 1112 1775 'vue-data-ui-fulscreen--off': !isFullscreen, 1113 1776 'not-responsive': !FINAL_CONFIG.responsive, 1114 1777 }" 1115 - :style="`display:block;${FINAL_CONFIG.responsive ? 'width:100%;height:auto' : 'height:100%;'};overflow:visible;background:transparent;color:${FINAL_CONFIG.style.chart.color};background:${FINAL_CONFIG.style.chart.backgroundColor};`" 1778 + :style="`display:block;${FINAL_CONFIG.responsive ? 'width:100%;height:auto' : 'height:100%;'};overflow:${mutableConfig.showZoom ? 'hidden' : 'visible'};background:transparent;color:${FINAL_CONFIG.style.chart.color};background:${FINAL_CONFIG.style.chart.backgroundColor};`" 1116 1779 tabindex="0" 1117 1780 @focus="onSvgFocus" 1118 1781 @blur="onSvgBlur" ··· 1210 1873 <!-- OVERLAYS --> 1211 1874 <template v-for="(circle, i) in circles" :key="circle.id"> 1212 1875 <rect 1876 + v-if="!circle.hasChildren" 1213 1877 data-cy="datapoint-circle-overlay" 1214 1878 :x="circle.x - circle.r" 1215 1879 :y="circle.y - circle.r" ··· 1242 1906 <template v-for="(circle, i) in circles" :key="circle.id"> 1243 1907 <slot 1244 1908 name="data-label" 1245 - v-if="$slots['data-label']" 1909 + v-if=" 1910 + $slots['data-label'] && canShowCircleLabel(circle) 1911 + " 1246 1912 v-bind="{ 1247 1913 ...circle, 1248 1914 createTSpans, ··· 1270 1936 data-cy="label-name" 1271 1937 v-if=" 1272 1938 FINAL_CONFIG.style.chart.circles.labels.name 1273 - .show && circle.name 1939 + .show && 1940 + circle.name && 1941 + canShowCircleLabel(circle) 1274 1942 " 1275 1943 :style="{ 1276 1944 pointerEvents: 'none', ··· 1314 1982 data-cy="label-value" 1315 1983 v-if=" 1316 1984 FINAL_CONFIG.style.chart.circles.labels.value 1317 - .show 1985 + .show && canShowCircleLabel(circle) 1318 1986 " 1319 1987 :style="{ 1320 1988 pointerEvents: 'none', ··· 1358 2026 name="circle" 1359 2027 v-bind="{ 1360 2028 ...circle, 2029 + showLabel: canShowCircleLabel(circle), 1361 2030 isSelected: selectedDatapoint?.id === circle.id, 2031 + isDescendantOfSelected: isDescendantOf( 2032 + circle, 2033 + selectedDatapoint, 2034 + ), 1362 2035 uid: `${i}_${uid}`, 1363 2036 }" 1364 2037 /> 1365 2038 </template> 1366 2039 2040 + <!-- PARENT SVG TOOLTIPS --> 2041 + <g 2042 + v-if=" 2043 + parentTooltipItems.length && 2044 + FINAL_CONFIG.style.chart.parentTooltips.show 2045 + " 2046 + data-cy="parent-svg-tooltips" 2047 + :style="{ pointerEvents: 'none' }" 2048 + > 2049 + <g 2050 + v-for="tooltip in parentTooltipItems" 2051 + :key="tooltip.id" 2052 + data-cy="parent-svg-tooltip" 2053 + > 2054 + <line 2055 + :x1="tooltip.anchorX" 2056 + :y1="tooltip.anchorY" 2057 + :x2="tooltip.lineX" 2058 + :y2="tooltip.lineY" 2059 + :stroke=" 2060 + FINAL_CONFIG.style.chart.parentTooltips.link 2061 + .useSerieColor 2062 + ? tooltip.color 2063 + : FINAL_CONFIG.style.chart.parentTooltips 2064 + .link.stroke 2065 + " 2066 + :stroke-width=" 2067 + FINAL_CONFIG.style.chart.parentTooltips.link 2068 + .strokeWidth 2069 + " 2070 + stroke-linecap="round" 2071 + :stroke-dasharray=" 2072 + FINAL_CONFIG.style.chart.parentTooltips.link 2073 + .strokeDasharray 2074 + " 2075 + :opacity=" 2076 + FINAL_CONFIG.style.chart.parentTooltips.link 2077 + .opacity 2078 + " 2079 + /> 2080 + <rect 2081 + :x="tooltip.x" 2082 + :y="tooltip.y" 2083 + :width="tooltip.width" 2084 + :height="tooltip.height" 2085 + :rx=" 2086 + Math.max(3, tooltip.fontSize / 2.5) * 2087 + FINAL_CONFIG.style.chart.parentTooltips 2088 + .borderRadiusRatio 2089 + " 2090 + :fill=" 2091 + FINAL_CONFIG.style.chart.parentTooltips 2092 + .backgroundColor 2093 + " 2094 + :stroke=" 2095 + FINAL_CONFIG.style.chart.parentTooltips 2096 + .useSerieColor 2097 + ? tooltip.color 2098 + : FINAL_CONFIG.style.chart.parentTooltips 2099 + .stroke 2100 + " 2101 + :stroke-width=" 2102 + FINAL_CONFIG.style.chart.parentTooltips 2103 + .strokeWidth 2104 + " 2105 + :filter=" 2106 + FINAL_CONFIG.style.chart.parentTooltips.filter 2107 + " 2108 + /> 2109 + 2110 + <slot name="parent-tooltip" v-bind="{ ...tooltip }"> 2111 + <circle 2112 + :cx="tooltip.x + tooltip.paddingX * 1.3" 2113 + :cy=" 2114 + tooltip.y + 2115 + tooltip.paddingY + 2116 + tooltip.lineHeight / 2 2117 + " 2118 + :r="tooltip.fontSize * 0.35" 2119 + :fill="tooltip.color" 2120 + /> 2121 + <text 2122 + :x=" 2123 + tooltip.x + 2124 + tooltip.paddingX + 2125 + tooltip.fontSize 2126 + " 2127 + :y=" 2128 + tooltip.y + 2129 + tooltip.paddingY + 2130 + tooltip.fontSize 2131 + " 2132 + :font-size="tooltip.fontSize" 2133 + :fill=" 2134 + FINAL_CONFIG.style.chart.parentTooltips 2135 + .color 2136 + " 2137 + :font-family="FINAL_CONFIG.style.fontFamily" 2138 + text-anchor="start" 2139 + > 2140 + <tspan 2141 + v-for="(line, lineIndex) in tooltip.lines" 2142 + :key="`${tooltip.id}_${lineIndex}`" 2143 + :x=" 2144 + tooltip.x + 2145 + tooltip.paddingX + 2146 + tooltip.fontSize 2147 + " 2148 + :dy=" 2149 + lineIndex === 0 ? 0 : tooltip.lineHeight 2150 + " 2151 + > 2152 + {{ line }} 2153 + </tspan> 2154 + </text> 2155 + </slot> 2156 + </g> 2157 + </g> 2158 + 1367 2159 <slot 1368 2160 name="svg" 1369 2161 :svg="{ 1370 2162 drawingArea: { 1371 - width: SIZE.w, 1372 - height: SIZE.h, 2163 + ...viewBox, 1373 2164 }, 1374 2165 width: SIZE.w, 1375 2166 height: SIZE.h, ··· 1406 2197 isCallbackSvg, 1407 2198 }" 1408 2199 /> 2200 + </div> 2201 + 2202 + <div v-if="isZoom" data-dom-to-png-ignore class="reset-wrapper"> 2203 + <slot name="reset-action" :reset="resetZoom"> 2204 + <button 2205 + data-cy-reset 2206 + tabindex="0" 2207 + role="button" 2208 + class="vue-data-ui-refresh-button" 2209 + :style="{ 2210 + background: FINAL_CONFIG.style.chart.backgroundColor, 2211 + cursor: isCursorPointer ? 'pointer' : 'default', 2212 + }" 2213 + @click="resetZoom(true)" 2214 + > 2215 + <BaseIcon 2216 + name="refresh" 2217 + :stroke="FINAL_CONFIG.style.chart.color" 2218 + /> 2219 + </button> 2220 + </slot> 1409 2221 </div> 1410 2222 1411 2223 <div v-if="$slots.source" ref="source" dir="auto"> ··· 1562 2374 clip: rect(0 0 0 0); 1563 2375 white-space: normal; 1564 2376 border: 0; 2377 + } 2378 + 2379 + .reset-wrapper { 2380 + display: flex; 2381 + flex-direction: row; 2382 + align-items: center; 2383 + justify-content: center; 2384 + padding: 0 24px; 2385 + height: 40px; 2386 + position: absolute; 2387 + bottom: 12px; 2388 + right: 0; 2389 + } 2390 + 2391 + .vue-data-ui-refresh-button { 2392 + outline: none; 2393 + border: none; 2394 + background: transparent; 2395 + height: 36px; 2396 + width: 36px; 2397 + display: flex; 2398 + align-items: center; 2399 + justify-content: center; 2400 + border-radius: 50%; 2401 + transition: transform 0.2s ease-in-out; 2402 + transform-origin: center; 2403 + &:focus { 2404 + outline: 1px solid v-bind(textColor); 2405 + } 2406 + &:hover { 2407 + transform: rotate(-90deg); 2408 + } 1565 2409 } 1566 2410 </style>
+3 -1
src/components/vue-ui-molecule.vue
··· 841 841 ); 842 842 } 843 843 844 + const textColor = computed(() => FINAL_CONFIG.value.style.chart.color); 845 + 844 846 defineExpose({ 845 847 getData, 846 848 getImage, ··· 1303 1305 transition: transform 0.2s ease-in-out; 1304 1306 transform-origin: center; 1305 1307 &:focus { 1306 - outline: 1px solid v-bind(slicerColor); 1308 + outline: 1px solid v-bind(textColor); 1307 1309 } 1308 1310 &:hover { 1309 1311 transform: rotate(-90deg);
+2 -1
src/components/vue-ui-sparkline.vue
··· 1279 1279 <linearGradient 1280 1280 v-if=" 1281 1281 FINAL_CONFIG.temperatureColors.show && 1282 - !!temperatureColors && !isFlatTemperature 1282 + !!temperatureColors && 1283 + !isFlatTemperature 1283 1284 " 1284 1285 :id="`temperature_grad_sparkline_${uid}`" 1285 1286 gradientTransform="rotate(90)"
+56
src/themes/vue_ui_circle_pack.json
··· 28 28 "backgroundOpacity": 70, 29 29 "color": "#CCCCCC", 30 30 "borderColor": "#3A3A3A" 31 + }, 32 + "parentTooltips": { 33 + "color": "#CCCCCC", 34 + "backgroundColor": "#1A1A1A", 35 + "stroke": "#CCCCCC", 36 + "link": { 37 + "stroke": "#CCCCCC" 38 + } 31 39 } 32 40 } 33 41 } ··· 52 60 "backgroundOpacity": 70, 53 61 "color": "#CCCCCC", 54 62 "borderColor": "#5A5A5A" 63 + }, 64 + "parentTooltips": { 65 + "color": "#CCCCCC", 66 + "backgroundColor": "#1A1A1A", 67 + "stroke": "#CCCCCC", 68 + "link": { 69 + "stroke": "#CCCCCC" 70 + } 55 71 } 56 72 } 57 73 } ··· 72 88 "backgroundOpacity": 30, 73 89 "color": "#424242", 74 90 "borderColor": "#FF8A65" 91 + }, 92 + "parentTooltips": { 93 + "color": "#424242", 94 + "backgroundColor": "#FFF8E1", 95 + "stroke": "#424242", 96 + "link": { 97 + "stroke": "#424242" 98 + } 75 99 } 76 100 } 77 101 } ··· 96 120 "backgroundOpacity": 30, 97 121 "color": "#FFF8E1", 98 122 "borderColor": "#FF8A65" 123 + }, 124 + "parentTooltips": { 125 + "color": "#FFF8E1", 126 + "backgroundColor": "#1E1E1E", 127 + "stroke": "#FFF8E1", 128 + "link": { 129 + "stroke": "#FFF8E1" 130 + } 99 131 } 100 132 } 101 133 } ··· 119 151 "backgroundColor": "#2A2F2A", 120 152 "color": "#AACCAA", 121 153 "borderColor": "#66CC66" 154 + }, 155 + "parentTooltips": { 156 + "color": "#99AA99", 157 + "backgroundColor": "#1A1A1A", 158 + "stroke": "#99AA99", 159 + "link": { 160 + "stroke": "#99AA99" 161 + } 122 162 } 123 163 } 124 164 } ··· 137 177 "tooltip": { 138 178 "backgroundColor": "#fbfafa", 139 179 "color": "#8A9892" 180 + }, 181 + "parentTooltips": { 182 + "color": "#8A9892", 183 + "backgroundColor": "#FBFAFA", 184 + "stroke": "#8A9892", 185 + "link": { 186 + "stroke": "#8A9892" 187 + } 140 188 } 141 189 } 142 190 } ··· 155 203 "tooltip": { 156 204 "backgroundColor": "#f6f6fb", 157 205 "color": "#50606C" 206 + }, 207 + "parentTooltips": { 208 + "color": "#50606C", 209 + "backgroundColor": "#f6f6fb", 210 + "stroke": "#50606C", 211 + "link": { 212 + "stroke": "#50606C" 213 + } 158 214 } 159 215 } 160 216 }
+27
src/useConfig.js
··· 6949 6949 stack: false, 6950 6950 annotator: true, 6951 6951 svg: true, 6952 + zoom: true, 6952 6953 }), 6953 6954 table: { 6954 6955 ...TABLE, ··· 6957 6958 td: TABLE_TD, 6958 6959 columnNames: { 6959 6960 datapoint: 'Datapoint', 6961 + parent: 'Parent', 6962 + depth: 'Depth', 6960 6963 value: 'Value', 6961 6964 }, 6962 6965 }, ··· 6977 6980 intensity: 40, 6978 6981 }, 6979 6982 labels: { 6983 + parents: { show: false }, 6984 + children: { show: true }, 6980 6985 name: { 6981 6986 fontSizeRatio: 1, 6982 6987 show: true, ··· 6997 7002 }, 6998 7003 }, 6999 7004 }, 7005 + parentTooltips: { 7006 + show: true, 7007 + fontSizeRatio: 1, 7008 + borderRadiusRatio: 1, 7009 + color: COLOR_BLACK, 7010 + filter: 'drop-shadow(0 2px 4px #2D353C30)', 7011 + backgroundColor: COLOR_WHITE, 7012 + strokeWidth: 1, 7013 + useSerieColor: true, 7014 + stroke: COLOR_BLACK, 7015 + link: { 7016 + strokeWidth: 1, 7017 + strokeDasharray: 0, 7018 + opacity: 0.6, 7019 + useSerieColor: false, 7020 + stroke: COLOR_BLACK, 7021 + }, 7022 + }, 7000 7023 tooltip: TOOLTIP, 7024 + zoom: { 7025 + show: true, 7026 + speed: 1, 7027 + }, 7001 7028 }, 7002 7029 }, 7003 7030 };
+22
ts-playground/src/components/charts/ts-vue-ui-circle-pack.vue
··· 35 35 { 36 36 name: 'A', 37 37 value: 384, 38 + children: [ 39 + { 40 + name: 'A', 41 + value: 384, 42 + }, 43 + { 44 + name: 'B', 45 + value: 192, 46 + }, 47 + { 48 + name: 'C', 49 + value: 96, 50 + }, 51 + { 52 + name: 'D', 53 + value: 48, 54 + }, 55 + { 56 + name: 'E', 57 + value: 24, 58 + }, 59 + ], 38 60 }, 39 61 { 40 62 name: 'B',
+77
types/vue-data-ui.d.ts
··· 13290 13290 color: string; 13291 13291 x: number; 13292 13292 y: number; 13293 + childCount: number; 13294 + children: VueUiCirclePackDatapoint[]; 13295 + depth: number; 13296 + hasChildren: boolean; 13297 + hierarchyPath: number[]; 13298 + leafCount: number; 13299 + parentId: string; 13300 + parentName: string; 13301 + rootId: string; 13293 13302 }; 13294 13303 13295 13304 export type VueUiCirclePackConfig = { ··· 13321 13330 td?: ChartTableCell; 13322 13331 columnNames?: { 13323 13332 datapoint?: string; 13333 + parent?: string; 13334 + depth?: string; 13324 13335 value?: string; 13325 13336 }; 13326 13337 }; ··· 13341 13352 intensity?: number; 13342 13353 }; 13343 13354 labels?: { 13355 + parents?: { 13356 + show?: boolean; 13357 + }; 13358 + children?: { 13359 + show?: boolean; 13360 + }; 13344 13361 name?: { 13345 13362 show?: boolean; 13346 13363 bold?: boolean; ··· 13361 13378 }; 13362 13379 }; 13363 13380 }; 13381 + parentTooltips?: { 13382 + show?: boolean; 13383 + fontSizeRatio?: number; 13384 + borderRadiusRatio?: number; 13385 + color?: string; 13386 + filter?: string; 13387 + backgroundColor?: string; 13388 + strokeWidth?: number; 13389 + useSerieColor?: boolean; 13390 + stroke?: string; 13391 + link?: { 13392 + strokeWidth?: number; 13393 + strokeDasharray?: number; 13394 + opacity?: number; 13395 + useSerieColor?: boolean; 13396 + stroke?: string; 13397 + }; 13398 + }; 13364 13399 tooltip?: ChartTooltip & { 13365 13400 customFormat?: 13366 13401 | null ··· 13371 13406 VueUiCirclePackConfig 13372 13407 >, 13373 13408 ) => string); 13409 + }; 13410 + zoom?: { 13411 + show?: boolean; 13412 + speed?: number; 13374 13413 }; 13375 13414 }; 13376 13415 }; ··· 13386 13425 toggleTable(): void; 13387 13426 toggleAnnotator(): void; 13388 13427 toggleFullscreen(): void; 13428 + copyAlt(): void; 13429 + toggleZoom(): void; 13430 + resetZoom(): void; 13389 13431 }; 13390 13432 13433 + export type VueUiCirclePackParentTooltip = { 13434 + datapoint: VueUiCirclePackDatapoint; 13435 + anchorX: number; 13436 + anchorY: number; 13437 + height: number; 13438 + lineX: number; 13439 + lineY: number; 13440 + width: number; 13441 + x: number; 13442 + y: number; 13443 + id: string; 13444 + color: string; 13445 + lines: string[]; 13446 + fontSize: number; 13447 + lineHeight: number; 13448 + paddingX: number; 13449 + paddingY: number; 13450 + }; 13451 + 13452 + export type VueUiCirclePackParentTooltipSlotProps = 13453 + VueUiCirclePackParentTooltip; 13454 + 13391 13455 export type VueUiCirclePackDataLabelSlotProps = VueUiCirclePackDatapoint & { 13392 13456 createTSpans: (args: CreateTSpansArgs) => string; 13393 13457 fontSize: { name: number; value: number }; ··· 13396 13460 13397 13461 export type VueUiCirclePackCircleSlotProps = VueUiCirclePackDatapoint & { 13398 13462 isSelected: boolean; 13463 + isDescendantOfSelected: boolean; 13399 13464 uid: string; 13465 + showLabel: boolean; 13400 13466 }; 13401 13467 13402 13468 export type VueUiCirclePackSvgSlotProps = { ··· 13416 13482 config: VueUiCirclePackConfig; 13417 13483 }; 13418 13484 13485 + export type VueUiCirclePackOptionZoomSlotProps = { 13486 + toggleZoom: () => void; 13487 + isZoomLocked: boolean; 13488 + }; 13489 + 13419 13490 export type VueUiCirclePackProps = { 13420 13491 config?: VueUiCirclePackConfig; 13421 13492 dataset: VueUiCirclePackDatasetItem[]; ··· 13433 13504 optionImg?: () => VNodeChild; 13434 13505 optionSvg?: () => VNodeChild; 13435 13506 optionTable?: () => VNodeChild; 13507 + optionZoom?: ( 13508 + props: VueUiCirclePackOptionZoomSlotProps, 13509 + ) => VNodeChild; 13436 13510 optionFullscreen?: ( 13437 13511 props: VueUiOptionFullscreenSlotProps, 13438 13512 ) => VNodeChild; ··· 13462 13536 props: VueUiCircleDatapointTooltipSlotProps, 13463 13537 ) => VNodeChild; 13464 13538 skeleton?: () => VNodeChild; 13539 + ['parent-tooltip']?: ( 13540 + props: VueUiCirclePackParentTooltipSlotProps, 13541 + ) => VNodeChild; 13465 13542 }; 13466 13543 }; 13467 13544 };
+12
types/vue-ui-circle-pack.d.ts
··· 23 23 VueUiWatermarkSlotProps, 24 24 VueUiCircleDatapointTooltipSlotProps, 25 25 CommonAnnotatorSlots, 26 + VueUiCirclePackParentTooltipSlotProps, 27 + VueUiCirclePackParentTooltip, 28 + VueUiCirclePackOptionZoomSlotProps, 26 29 } from 'vue-data-ui'; 27 30 28 31 export type { ··· 48 51 VueUiWatermarkSlotProps, 49 52 VueUiCircleDatapointTooltipSlotProps, 50 53 CommonAnnotatorSlots, 54 + VueUiCirclePackParentTooltipSlotProps, 55 + VueUiCirclePackParentTooltip, 56 + VueUiCirclePackOptionZoomSlotProps, 51 57 }; 52 58 53 59 declare const VueUiCirclePackBase: DefineComponent<VueUiCirclePackProps>; ··· 62 68 optionImg?: () => VNodeChild; 63 69 optionSvg?: () => VNodeChild; 64 70 optionTable?: () => VNodeChild; 71 + optionZoom?: ( 72 + props: VueUiCirclePackOptionZoomSlotProps, 73 + ) => VNodeChild; 65 74 optionFullscreen?: ( 66 75 props: VueUiOptionFullscreenSlotProps, 67 76 ) => VNodeChild; ··· 89 98 props: VueUiCircleDatapointTooltipSlotProps, 90 99 ) => VNodeChild; 91 100 skeleton?: () => VNodeChild; 101 + ['parent-tooltip']?: ( 102 + props: VueUiCirclePackParentTooltipSlotProps, 103 + ) => VNodeChild; 92 104 }; 93 105 }; 94 106 };