Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 297 additions & 0 deletions docs/examples/fast_bar.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "0",
"metadata": {},
"source": [
"# Fast Bar Charts\n",
"\n",
"The `fast_bar()` method creates bar-like visualizations using stacked areas. This renders much faster than actual bar charts for large datasets because it uses a single polygon per trace instead of individual rectangles."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import xarray as xr\n",
"\n",
"from xarray_plotly import config, xpx\n",
"\n",
"config.notebook()"
]
},
{
"cell_type": "markdown",
"id": "2",
"metadata": {},
"source": [
"## Basic Example"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3",
"metadata": {},
"outputs": [],
"source": [
"# Quarterly revenue data by product and region\n",
"np.random.seed(42)\n",
"da = xr.DataArray(\n",
" np.random.rand(4, 3, 2) * 100 + 50,\n",
" dims=[\"quarter\", \"product\", \"region\"],\n",
" coords={\n",
" \"quarter\": [\"Q1\", \"Q2\", \"Q3\", \"Q4\"],\n",
" \"product\": [\"Widgets\", \"Gadgets\", \"Gizmos\"],\n",
" \"region\": [\"North\", \"South\"],\n",
" },\n",
" name=\"revenue\",\n",
")\n",
"\n",
"xpx(da).fast_bar()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4",
"metadata": {},
"outputs": [],
"source": [
"# Comparison with regular bar()\n",
"xpx(da).bar()"
]
},
{
"cell_type": "markdown",
"id": "5",
"metadata": {},
"source": [
"## With Faceting"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6",
"metadata": {},
"outputs": [],
"source": [
"xpx(da).fast_bar(facet_col=\"region\")"
]
},
{
"cell_type": "markdown",
"id": "7",
"metadata": {},
"source": [
"## With Animation"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8",
"metadata": {},
"outputs": [],
"source": [
"# Multi-year data for animation\n",
"np.random.seed(123)\n",
"da_anim = xr.DataArray(\n",
" np.random.rand(4, 3, 5) * 100 + 20,\n",
" dims=[\"quarter\", \"product\", \"year\"],\n",
" coords={\n",
" \"quarter\": [\"Q1\", \"Q2\", \"Q3\", \"Q4\"],\n",
" \"product\": [\"Widgets\", \"Gadgets\", \"Gizmos\"],\n",
" \"year\": [2020, 2021, 2022, 2023, 2024],\n",
" },\n",
" name=\"revenue\",\n",
")\n",
"\n",
"xpx(da_anim).fast_bar(animation_frame=\"year\")"
]
},
{
"cell_type": "markdown",
"id": "9",
"metadata": {},
"source": [
"## Faceting + Animation"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "10",
"metadata": {},
"outputs": [],
"source": [
"# 4D data: quarter x product x region x year\n",
"np.random.seed(456)\n",
"da_4d = xr.DataArray(\n",
" np.random.rand(4, 3, 2, 4) * 80 + 30,\n",
" dims=[\"quarter\", \"product\", \"region\", \"year\"],\n",
" coords={\n",
" \"quarter\": [\"Q1\", \"Q2\", \"Q3\", \"Q4\"],\n",
" \"product\": [\"Widgets\", \"Gadgets\", \"Gizmos\"],\n",
" \"region\": [\"North\", \"South\"],\n",
" \"year\": [2021, 2022, 2023, 2024],\n",
" },\n",
" name=\"revenue\",\n",
")\n",
"\n",
"xpx(da_4d).fast_bar(facet_col=\"region\", animation_frame=\"year\")"
]
},
{
"cell_type": "markdown",
"id": "11",
"metadata": {},
"source": [
"## Positive and Negative Values\n",
"\n",
"`fast_bar()` classifies each trace by its values:\n",
"- **Purely positive** → stacks upward\n",
"- **Purely negative** → stacks downward\n",
"- **Mixed signs** → warning + dashed line (use `bar()` instead)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "12",
"metadata": {},
"outputs": [],
"source": [
"# Profit (positive) and Loss (negative) - stacks correctly\n",
"np.random.seed(789)\n",
"da_split = xr.DataArray(\n",
" np.column_stack(\n",
" [\n",
" np.random.rand(6) * 80 + 20, # Revenue: positive\n",
" -np.random.rand(6) * 50 - 10, # Costs: negative\n",
" ]\n",
" ),\n",
" dims=[\"month\", \"category\"],\n",
" coords={\n",
" \"month\": [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\"],\n",
" \"category\": [\"Revenue\", \"Costs\"],\n",
" },\n",
" name=\"financials\",\n",
")\n",
"\n",
"xpx(da_split).fast_bar()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "13",
"metadata": {},
"outputs": [],
"source": [
"# With animation - sign classification is consistent across frames\n",
"np.random.seed(321)\n",
"da_split_anim = xr.DataArray(\n",
" np.stack(\n",
" [\n",
" np.column_stack([np.random.rand(6) * 80 + 20, -np.random.rand(6) * 50 - 10])\n",
" for _ in range(4)\n",
" ],\n",
" axis=-1,\n",
" ),\n",
" dims=[\"month\", \"category\", \"year\"],\n",
" coords={\n",
" \"month\": [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\"],\n",
" \"category\": [\"Revenue\", \"Costs\"],\n",
" \"year\": [2021, 2022, 2023, 2024],\n",
" },\n",
" name=\"financials\",\n",
")\n",
"\n",
"xpx(da_split_anim).fast_bar(animation_frame=\"year\")"
]
},
{
"cell_type": "markdown",
"id": "14",
"metadata": {},
"source": [
"## Mixed Sign Warning\n",
"\n",
"When a trace has both positive and negative values, `fast_bar()` shows a warning and displays it as a dashed line:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "15",
"metadata": {},
"outputs": [],
"source": [
"# Both columns have mixed signs - triggers warning\n",
"da_mixed = xr.DataArray(\n",
" np.array(\n",
" [\n",
" [50, -30],\n",
" [-40, 60],\n",
" [30, -50],\n",
" [-20, 40],\n",
" ]\n",
" ),\n",
" dims=[\"month\", \"category\"],\n",
" coords={\n",
" \"month\": [\"Jan\", \"Feb\", \"Mar\", \"Apr\"],\n",
" \"category\": [\"A\", \"B\"],\n",
" },\n",
")\n",
"\n",
"# This will show a warning\n",
"xpx(da_mixed).fast_bar()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "16",
"metadata": {},
"outputs": [],
"source": [
"# For mixed data, use bar() instead\n",
"xpx(da_mixed).bar()"
]
},
{
"cell_type": "markdown",
"id": "17",
"metadata": {},
"source": [
"## When to Use\n",
"\n",
"| Method | Use when... |\n",
"|--------|-------------|\n",
"| `fast_bar()` | Large datasets, animations, performance matters, data is same-sign per trace |\n",
"| `bar()` | Need grouped bars, pattern fills, or have mixed +/- values per trace |\n",
"| `area()` | Want smooth continuous fills |"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.12.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
3 changes: 3 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,7 @@ nav:
- Dimensions & Facets: examples/dimensions.ipynb
- Plotly Express Options: examples/kwargs.ipynb
- Figure Customization: examples/figure.ipynb
- Combining Figures: examples/combining.ipynb
- Figure Manipulation: examples/manipulation.ipynb
- Fast Bar Charts: examples/fast_bar.ipynb
- API Reference: api.md
60 changes: 60 additions & 0 deletions tests/test_accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,66 @@ def test_area_returns_figure(self) -> None:
fig = self.da_2d.plotly.area()
assert isinstance(fig, go.Figure)

def test_fast_bar_returns_figure(self) -> None:
"""Test that fast_bar() returns a Plotly Figure."""
fig = self.da_2d.plotly.fast_bar()
assert isinstance(fig, go.Figure)

def test_fast_bar_trace_styling(self) -> None:
"""Test that fast_bar applies correct trace styling."""
fig = self.da_2d.plotly.fast_bar()
for trace in fig.data:
assert trace.line.width == 0
assert trace.line.shape == "hv"
assert trace.fillcolor is not None

def test_fast_bar_animation_frames(self) -> None:
"""Test that fast_bar styling applies to animation frames."""
da = xr.DataArray(
np.random.rand(5, 3, 4),
dims=["time", "city", "year"],
)
fig = da.plotly.fast_bar(animation_frame="year")
assert len(fig.frames) > 0
for frame in fig.frames:
for trace in frame.data:
assert trace.line.width == 0
assert trace.line.shape == "hv"
assert trace.fillcolor is not None

def test_fast_bar_mixed_signs_dashed(self) -> None:
"""Test that fast_bar shows mixed-sign traces as dashed lines."""
da = xr.DataArray(
np.array([[50, -30], [-40, 60]]), # Both columns have mixed signs
dims=["time", "category"],
)
fig = da.plotly.fast_bar()
# Mixed traces should have no stacking and dashed lines
for trace in fig.data:
assert trace.stackgroup is None
assert trace.line.dash == "dash"

def test_fast_bar_separate_sign_columns(self) -> None:
"""Test that fast_bar uses separate stackgroups when columns have different signs."""
da = xr.DataArray(
np.array([[50, -30], [60, -40]]), # Column 0 positive, column 1 negative
dims=["time", "category"],
)
fig = da.plotly.fast_bar()
stackgroups = {trace.stackgroup for trace in fig.data}
assert "positive" in stackgroups
assert "negative" in stackgroups

def test_fast_bar_same_sign_stacks(self) -> None:
"""Test that fast_bar uses stacking for same-sign data."""
da = xr.DataArray(
np.random.rand(5, 3) * 100,
dims=["time", "category"],
)
fig = da.plotly.fast_bar()
for trace in fig.data:
assert trace.stackgroup is not None

def test_scatter_returns_figure(self) -> None:
"""Test that scatter() returns a Plotly Figure."""
fig = self.da_2d.plotly.scatter()
Expand Down
Loading