Bubble pack chart
A tree as nested circles, each leaf's area its value, packed tightly inside its parent. Click a circle with children to zoom in; the breadcrumb or outer circle zooms out.
| Node | Value |
|---|---|
| repo | 1786 MB |
| repo > apps | 954 MB |
| repo > apps > web | 459 MB |
| repo > apps > web > node_modules | 412 MB |
| repo > apps > web > dist | 38 MB |
| repo > apps > web > src | 9 MB |
| repo > apps > docs | 264 MB |
| repo > apps > docs > node_modules | 188 MB |
| repo > apps > docs > public | 64 MB |
| repo > apps > docs > content | 12 MB |
| repo > apps > mobile | 231 MB |
| repo > apps > mobile > ios | 96 MB |
| repo > apps > mobile > android | 121 MB |
| repo > apps > mobile > src | 14 MB |
| repo > packages | 53 MB |
| repo > packages > ui | 22 MB |
| repo > packages > charts | 17 MB |
| repo > packages > auth | 6 MB |
| repo > packages > billing | 8 MB |
| repo > assets | 337 MB |
| repo > assets > video | 240 MB |
| repo > assets > images | 86 MB |
| repo > assets > fonts | 11 MB |
| repo > .git | 442 MB |
| repo > .git > objects | 302 MB |
| repo > .git > lfs | 140 MB |
Usage
use hydroxy_leptos::bubble_pack_chart::*;
use leptos::prelude::*;
fn folder(name: &str, files: &[(&str, f64)]) -> Tree {
Tree::branch(name, files.iter().map(|(n, v)| Tree::leaf(*n, *v)).collect())
}
pub fn demo() -> AnyView {
// Disk usage of a monorepo checkout, in MB.
let repo = Tree::branch(
"repo",
vec![
Tree::branch(
"apps",
vec![
folder("web", &[("node_modules", 412.0), ("dist", 38.0), ("src", 9.0)]),
folder("docs", &[("node_modules", 188.0), ("public", 64.0), ("content", 12.0)]),
folder("mobile", &[("ios", 96.0), ("android", 121.0), ("src", 14.0)]),
],
),
folder("packages", &[("ui", 22.0), ("charts", 17.0), ("auth", 6.0), ("billing", 8.0)]),
folder("assets", &[("video", 240.0), ("images", 86.0), ("fonts", 11.0)]),
folder(".git", &[("objects", 302.0), ("lfs", 140.0)]),
],
);
view! {
<BubblePackChart
root=repo
label="Disk usage by folder"
format=|v: f64| format!("{v:.0} MB")
/>
}
.into_any()
}use dioxus::prelude::*;
use hydroxy_dioxus::bubble_pack_chart::{BubblePackChart, Tree};
fn folder(name: &str, files: &[(&str, f64)]) -> Tree {
Tree::branch(name, files.iter().map(|(n, v)| Tree::leaf(*n, *v)).collect())
}
#[component]
pub fn BubblePackChartDemo() -> Element {
// Disk usage of a monorepo checkout, in MB.
let repo = Tree::branch(
"repo",
vec![
Tree::branch(
"apps",
vec![
folder("web", &[("node_modules", 412.0), ("dist", 38.0), ("src", 9.0)]),
folder("docs", &[("node_modules", 188.0), ("public", 64.0), ("content", 12.0)]),
folder("mobile", &[("ios", 96.0), ("android", 121.0), ("src", 14.0)]),
],
),
folder("packages", &[("ui", 22.0), ("charts", 17.0), ("auth", 6.0), ("billing", 8.0)]),
folder("assets", &[("video", 240.0), ("images", 86.0), ("fonts", 11.0)]),
folder(".git", &[("objects", 302.0), ("lfs", 140.0)]),
],
);
rsx! {
BubblePackChart {
root: repo,
label: "Disk usage by folder",
format: |v: f64| format!("{v:.0} MB"),
}
}
}
Anatomy
| Component | data-part | Renders |
|---|---|---|
BubblePackChart | root | figure with a screen-reader table of every node |
| breadcrumb | Path from the root to the current zoom; earlier steps are buttons |
| marks | Focusable SVG; arrow keys move between circles |
| bubble | One circle; `data-depth`, `data-leaf`, `data-active` |
| tooltip | Name, value and share of the zoomed node |
| legend | Top-level branches when there are two or more |
Keyboard
| Key | Does |
|---|---|
Arrow keys | Move between circles in tree order |
Home / End | Jump to the first or last circle |
Enter / Space | Zoom into the focused circle, or out from the outer one |
Escape | Clear the focused circle, then zoom out |
Props
| Prop | Type | Notes |
|---|---|---|
root | Tree | Required. Branch values come from their children |
label | String | Required. What the chart shows; the accessible name |
size | u16 | Diameter in px; scales down to fit. Default 480 |
format | Fn(f64) -> String | Formats values in the tooltip and table |
Events
| Prop | Type | Notes |
|---|---|---|
on_hover | Callback<Option<Datum>> | The mark under the pointer or keyboard focus changes, or is left. Fires only on a change |
on_select | Callback<Datum> | Click, tap or Enter on a mark |
view | Signal<View> | Controlled focused node. Uncontrolled when left off |
on_view_change | Callback<View> | Called when the focused node changes; not for changes that came from `view` |