CirclePack.html.svelte component
Generates an HTML circle pack chart using d3-hierarchy.
Param | Default | Required | Description |
---|---|---|---|
idKey string |
'id' |
The key on each object where the id value lives. | |
parentKey string |
None | Set this if you want to define one parent circle. This will give you a nested graphic versus a grouping of circles. | |
valueKey string |
'value' |
The key on each object where the data value lives. | |
labelVisibilityThreshold Function |
r => r > 25 |
By default, only show the text inside a circle if its radius exceeds a certain size. Provide your own function for different behavior. | |
fill string |
'#fff' |
The circle's fill color. | |
stroke string |
'#999' |
The circle's stroke color. | |
strokeWidth number |
1 |
The circle's stroke width, in pixels. | |
textColor string |
'#333' |
The label text color. | |
textStroke string |
'#000' |
The label text's stroke color. | |
textStrokeWidth number |
0 |
The label text's stroke width, in pixels. | |
sortBy (a: HierarchyNode, b: HierarchyNode) => number |
(a, b) => b.value - a.value |
The order in which circle's are drawn. Sorting on the depth key is also a popular choice. |
|
spacing number |
0 |
Whitespace padding between each circle, in pixels. |
Used in these examples:
<!--
@component
Generates an HTML circle pack chart using [d3-hierarchy](https://github.com/d3/d3-hierarchy).
-->
<script>
import { stratify, pack, hierarchy } from 'd3-hierarchy';
import { getContext } from 'svelte';
import { format } from 'd3-format';
const titleCase = d => d.replace(/^\w/, w => w.toUpperCase());
const commas = format(',');
const { width, height, data } = getContext('LayerCake');
/** @typedef {import('d3-hierarchy').HierarchyNode} HierarchyNode */
/**
* @typedef {Object} Props
* @property {string} [idKey='id'] - The key on each object where the id value lives.
* @property {string} [parentKey] - Set this if you want to define one parent circle. This will give you a [nested](https://layercake.graphics/example/CirclePackNested) graphic versus a [grouping of circles](https://layercake.graphics/example/CirclePack).
* @property {string} [valueKey='value'] - The key on each object where the data value lives.
* @property {Function} [labelVisibilityThreshold=r => r> 25] - By default, only show the text inside a circle if its radius exceeds a certain size. Provide your own function for different behavior.
* @property {string} [fill='#fff'] - The circle's fill color.
* @property {string} [stroke='#999'] - The circle's stroke color.
* @property {number} [strokeWidth=1] - The circle's stroke width, in pixels.
* @property {string} [textColor='#333'] - The label text color.
* @property {string} [textStroke='#000'] - The label text's stroke color.
* @property {number} [textStrokeWidth=0] - The label text's stroke width, in pixels.
* @property {(a: HierarchyNode, b: HierarchyNode) => number} [sortBy=(a, b) => b.value - a.value] - The order in which circle's are drawn. Sorting on the `depth` key is also a popular choice.
* @property {number} [spacing=0] - Whitespace padding between each circle, in pixels.
*/
/** @type {Props} */
let {
idKey = 'id',
parentKey,
valueKey = 'value',
labelVisibilityThreshold = r => r > 25,
fill = '#fff',
stroke = '#999',
strokeWidth = 1,
textColor = '#333',
textStroke = '#000',
textStrokeWidth = 0,
sortBy = (a, b) => b.value - a.value,
spacing = 0
} = $props();
/* --------------------------------------------
* This component will automatically group your data
* into one group if no `parentKey` was passed in.
* Stash $data here so we can add our own parent
* if there's no `parentKey`
*/
let parent = $derived(parentKey !== undefined ? {} : { [idKey]: 'all' });
let dataset = $derived(parentKey !== undefined ? $data : [...$data, parent]);
let stratifier = $derived(
stratify()
.id(d => d[idKey])
.parentId(d => {
if (d[idKey] === parent[idKey]) return '';
if (parentKey === undefined) return parent[idKey];
return d[parentKey];
})
);
let descendants = $derived(
pack()
.size([$width, $height])
.padding(spacing)(
hierarchy(stratifier(dataset))
.sum(d => {
return d.data[valueKey] || 1;
})
.sort(sortBy)
)
.descendants()
);
</script>
<div class="circle-pack" data-has-parent-key={parentKey !== undefined}>
{#each descendants as d}
<div class="circle-group" data-id={d.data.id} data-visible={labelVisibilityThreshold(d.r)}>
<div
class="circle"
style:left="{d.x}px"
style:top="{d.y}px"
style:width="{d.r * 2}px"
style:height="{d.r * 2}px"
style:background-color={fill}
style:border="{strokeWidth}px solid {stroke}"
></div>
<div
class="text-group"
style="
color:{textColor};
text-shadow:
-{textStrokeWidth}px -{textStrokeWidth}px 0 {textStroke},
{textStrokeWidth}px -{textStrokeWidth}px 0 {textStroke},
-{textStrokeWidth}px {textStrokeWidth}px 0 {textStroke},
{textStrokeWidth}px {textStrokeWidth}px 0 {textStroke};
left:{d.x}px;
top:{d.y - (labelVisibilityThreshold(d.r) ? 0 : d.r + 4)}px;
"
>
<div class="text">{titleCase(d.data.id)}</div>
{#if d.data.data[valueKey]}
<div class="text value">{commas(d.data.data[valueKey])}</div>
{/if}
</div>
</div>
{/each}
</div>
<style>
.circle-pack {
position: relative;
width: 100%;
height: 100%;
}
.circle,
.text-group {
position: absolute;
}
.circle {
transform: translate(-50%, -50%);
}
/* Hide the root node if we want, useful if we are creating our own root */
.circle-pack[data-has-parent-key='false'] .circle-group[data-id='all'] {
display: none;
}
/* .circle-group:hover {
z-index: 9999;
} */
.circle-group[data-visible='false'] .text-group {
display: none;
padding: 4px 7px;
background: #fff;
border: 1px solid #ccc;
transform: translate(-50%, -100%);
top: -4px;
}
.circle-group[data-visible='false']:hover .text-group {
z-index: 999;
display: block !important;
/* On hover, set the text color to black and eliminate the shadow */
text-shadow: none !important;
color: #000 !important;
}
.circle-group[data-visible='false']:hover .circle {
border-color: #000 !important;
}
.text-group {
width: auto;
top: 50%;
left: 50%;
text-align: center;
transform: translate(-50%, -50%);
white-space: nowrap;
pointer-events: none;
cursor: pointer;
line-height: 13px;
}
.text {
width: 100%;
font-size: 11px;
/* text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff; */
}
.text.value {
font-size: 11px;
}
.circle {
border-radius: 50%;
top: 0;
left: 0;
}
</style>