# Packed Bubble Chart > Packed Bubble Chart shows category magnitudes as circles sized by value, packed toward the center with a physics layout. A packed bubble chart shows category magnitudes as circles sized by value, where bubbles gravitate toward the center with a physics simulation and settle into place. Labels sit inside the larger bubbles, the layout adapts continuously to the container, and cluster tightness is tunable via the gravity options. Hovering a bubble highlights it and fades the others. - **Good for:** comparing magnitudes across a single set of categories, showing relative size at a glance (revenue by product, headcount by team, traffic by source). - **Not great for:** precise value comparison (use a bar chart), part-to-whole hierarchy (use a sunburst or treemap), or data that needs x/y coordinates (use a bubble plot). ## Syntax Use the following AML definition to add the Packed Bubble Chart to your custom chart library. ```aml CustomChartDef packed_bubble_force { label: 'Packed Bubble (Force)' description: 'To show category magnitudes as gravity-clustered bubbles, with labels inside the larger bubbles.' fields { field dimension { label: 'Dimension' type: 'dimension' sort { apply_order: 1 direction: 'asc' } } field value { label: 'Value' type: 'measure' sort { apply_order: 2 direction: 'desc' } } } options { option gravity_x { label: 'Horizontal gravity' type: 'select' options: [0.05, 0.1, 0.2, 0.3, 0.5] default_value: 0.1 } option gravity_y { label: 'Vertical gravity' type: 'select' options: [0.05, 0.1, 0.2, 0.3, 0.5] default_value: 0.2 } option show_labels { label: 'Show labels in bubbles' type: 'toggle' default_value: true } option color_scheme { label: 'Color scheme' type: 'select' options: ['tableau10', 'category10', 'accent', 'dark2', 'paired', 'set2'] default_value: 'tableau10' } } template: @vg { "$schema": "https://vega.github.io/schema/vega/v5.json", "signals": [ { "name": "width", "init": "containerSize()[0] - 10", "on": [{ "events": "window:resize", "update": "containerSize()[0] - 10" }] }, { "name": "height", "init": "containerSize()[1] - 10", "on": [{ "events": "window:resize", "update": "containerSize()[1] - 10" }] }, {"name": "cx", "update": "width / 2"}, {"name": "cy", "update": "height / 2"}, {"name": "gravityX", "update": "@{options.gravity_x.value}"}, {"name": "gravityY", "update": "@{options.gravity_y.value}"}, {"name": "maxSize", "update": "(width * height * 0.55) / max(1, length(data('table')))"}, { "name": "hovered", "value": null, "on": [ {"events": "@nodes:mouseover", "update": "datum.category"}, {"events": "@nodes:mouseout", "update": "null"} ] }, { "name": "normalPointSelection", "value": null, "on": [ {"events": "@nodes:click", "update": "{'@{fields.dimension.name}': [datum['category']]}"}, {"events": "click[!event.item]", "update": "null"} ] }, { "name": "hoverPointSelection", "value": null, "on": [ {"events": "@nodes:mouseover", "update": "{'@{fields.dimension.name}': [datum['category']]}"}, {"events": "@nodes:mouseout", "update": "null"} ] } ], "holisticsConfig": { "crossFilterSignals": ["normalPointSelection"], "contextMenuSignals": ["hoverPointSelection"] }, "data": [ { "name": "table", "values": @{values}, "transform": [ {"type": "formula", "expr": "datum['@{fields.dimension.name}']", "as": "category"}, {"type": "formula", "expr": "datum['@{fields.value.name}']", "as": "amount"}, {"type": "filter", "expr": "datum.amount != null && datum.amount > 0"}, { "type": "aggregate", "groupby": ["category"], "fields": ["amount"], "ops": ["sum"], "as": ["amount"] } ] } ], "scales": [ { "name": "size", "type": "linear", "zero": true, "domain": {"data": "table", "field": "amount"}, "range": [16, {"signal": "maxSize"}] }, { "name": "color", "type": "ordinal", "domain": {"data": "table", "field": "category", "sort": true}, "range": {"scheme": @{options.color_scheme.value}} } ], "marks": [ { "name": "nodes", "type": "symbol", "from": {"data": "table"}, "encode": { "enter": { "xfocus": {"signal": "cx"}, "yfocus": {"signal": "cy"} }, "update": { "size": {"scale": "size", "field": "amount"}, "fill": {"scale": "color", "field": "category"}, "fillOpacity": { "signal": "hovered === null || hovered === datum.category ? 1 : 0.3" }, "stroke": {"value": "white"}, "strokeWidth": {"value": 1.5}, "tooltip": { "signal": "{'Category': datum.category, 'Value': format(datum.amount, ',')}" } } }, "transform": [ { "type": "force", "iterations": 100, "static": false, "forces": [ { "force": "collide", "iterations": 2, "radius": {"expr": "sqrt(datum.size) / 2 + 1"} }, {"force": "center", "x": {"signal": "cx"}, "y": {"signal": "cy"}}, {"force": "x", "x": "xfocus", "strength": {"signal": "gravityX"}}, {"force": "y", "y": "yfocus", "strength": {"signal": "gravityY"}} ] } ] }, { "type": "text", "interactive": false, "from": {"data": "nodes"}, "encode": { "update": { "x": {"field": "x"}, "y": {"field": "y"}, "align": {"value": "center"}, "baseline": {"value": "middle"}, "text": {"field": "datum.category"}, "fontSize": {"signal": "clamp(sqrt(datum.size) / 5.5, 8, 14)"}, "fontWeight": {"value": 500}, "fill": {"value": "white"}, "fillOpacity": { "signal": "hovered === null || hovered === datum.datum.category ? 1 : 0.3" }, "limit": {"signal": "sqrt(datum.size) * 0.95"}, "opacity": { "signal": "@{options.show_labels.value} && sqrt(datum.size) / 2 > 16 ? 1 : 0" } } } } ] };; } ``` ## Required fields A Packed Bubble Chart expects exactly two fields. Each row of input is one category whose value sets the bubble size. | Field | Label | Type | Role | |-------------|-----------|-------------|------| | `dimension` | Dimension | `dimension` | Category each bubble represents; shown as the bubble label. Sorted ascending (`apply_order: 1`). | | `value` | Value | `measure` | Magnitude that sets the bubble area. Sorted descending (`apply_order: 2`). | **Data requirements:** The template sums duplicate categories and filters out null and non-positive values, so you don't need to pre-aggregate. Use positive values, since the template drops zero and negative rows before rendering. **Sample data:** | dimension | value | |-----------|-------| | Search | 4200 | | Direct | 3100 | | Social | 2400 | | Email | 1500 | | Referral | 900 | ## Options Set these options to adjust the layout without editing the Vega template. The `CustomChartDef` block above declares each option's type and allowed values. | Option | Default | Effect | |----------------|-------------|--------| | `gravity_x` | `0.1` | Horizontal pull toward the center. Higher values pack bubbles tighter left-to-right. | | `gravity_y` | `0.2` | Vertical pull toward the center. Higher values pack bubbles tighter top-to-bottom. | | `show_labels` | `true` | Whether category labels appear inside bubbles. Labels still only show on bubbles large enough to fit them. | | `color_scheme` | `tableau10` | Ordinal color palette applied across categories. | ## Known limitations - **Positions are not quantitative.** Bubbles settle by physics simulation, so their x/y location carries no meaning. Use a bubble plot when you need two numeric axes. - **Labels show only on larger bubbles.** Small bubbles omit their label even with `show_labels` on, since the text would not fit inside the circle. - **Magnitudes are hard to compare precisely.** Area-based sizing reads relative scale well but not exact differences. Use a bar chart when precise comparison matters.