Skip to content

Commit daec3fd

Browse files
committed
Enhance bar chart functionality: support grouped bars, log scale, and update API for matplotlib alignment
1 parent 16b423f commit daec3fd

8 files changed

Lines changed: 998 additions & 295 deletions

File tree

Examples/plot_bar.py

Lines changed: 146 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,159 @@
22
Bar Chart
33
=========
44
5-
Demonstrate :meth:`~anyplotlib.figure_plots.Axes.bar` with vertical and
6-
horizontal orientations, per-bar colours, category labels, and live data
7-
updates via :meth:`~anyplotlib.figure_plots.PlotBar.update`.
5+
Demonstrate :meth:`~anyplotlib.figure_plots.Axes.bar` with:
86
9-
Three separate figures are shown:
10-
11-
1. **Vertical bar chart** – monthly sales data with a uniform colour.
12-
2. **Horizontal bar chart** – ranked items with per-bar colours and value
13-
labels.
14-
3. **Side-by-side comparison** – two panels sharing the same figure; one
15-
panel updates its data to show a different quarter.
7+
* **Matplotlib-aligned API** — ``ax.bar(x, height, width, bottom, …)``
8+
* Vertical and horizontal orientations, per-bar colours, category labels
9+
* **Grouped bars** — pass a 2-D *height* array ``(N, G)``
10+
* **Log-scale value axis** — ``log_scale=True``
11+
* Live data updates via :meth:`~anyplotlib.figure_plots.PlotBar.update`
1612
"""
1713
import numpy as np
1814
import anyplotlib as vw
1915

2016
rng = np.random.default_rng(7)
2117

18+
# ── 1. Vertical bar chart — monthly sales ────────────────────────────────────
19+
# The first positional argument is now *x* (positions or labels), matching
20+
# ``matplotlib.pyplot.bar(x, height, width=0.8, bottom=0.0, ...)``.
21+
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
22+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
23+
sales = np.array([42, 55, 48, 63, 71, 68, 74, 81, 66, 59, 52, 78],
24+
dtype=float)
25+
26+
fig1, ax1 = vw.subplots(1, 1, figsize=(640, 340))
27+
bar1 = ax1.bar(
28+
months, # x — category strings become x_labels automatically
29+
sales, # height
30+
width=0.6,
31+
color="#4fc3f7",
32+
show_values=True,
33+
units="Month",
34+
y_units="Units sold",
35+
)
36+
fig1
37+
38+
# %%
39+
# Horizontal bar chart — ranked items
40+
# -------------------------------------
41+
# Set ``orient="h"`` for a horizontal layout. Pass a list of CSS colours
42+
# to ``colors`` to give each bar its own colour.
43+
44+
categories = ["NumPy", "SciPy", "Matplotlib", "Pandas", "Scikit-learn",
45+
"PyTorch", "TensorFlow", "JAX", "Polars", "Dask"]
46+
scores = np.array([95, 88, 91, 87, 83, 79, 76, 72, 68, 65], dtype=float)
47+
48+
palette = [
49+
"#ef5350", "#ec407a", "#ab47bc", "#7e57c2", "#42a5f5",
50+
"#26c6da", "#26a69a", "#66bb6a", "#d4e157", "#ffa726",
51+
]
52+
53+
fig2, ax2 = vw.subplots(1, 1, figsize=(540, 400))
54+
bar2 = ax2.bar(
55+
categories,
56+
scores,
57+
orient="h",
58+
colors=palette,
59+
width=0.65,
60+
show_values=True,
61+
y_units="Popularity score",
62+
)
63+
fig2
64+
65+
# %%
66+
# Grouped bar chart — quarterly comparison
67+
# -----------------------------------------
68+
# Pass a 2-D *height* array of shape ``(N, G)`` to draw *G* bars side by
69+
# side for each category. Provide ``group_labels`` to show a legend and
70+
# ``group_colors`` to customise each group's colour.
71+
72+
quarters = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
73+
q_data = np.array([
74+
[42, 58, 51], # Jan — Q1, Q2, Q3
75+
[55, 61, 59], # Feb
76+
[48, 70, 65], # Mar
77+
[63, 75, 71], # Apr
78+
[71, 69, 80], # May
79+
[68, 83, 77], # Jun
80+
], dtype=float) # shape (6, 3) → 6 categories, 3 groups
81+
82+
fig3, ax3 = vw.subplots(1, 1, figsize=(680, 340))
83+
bar3 = ax3.bar(
84+
quarters,
85+
q_data,
86+
width=0.8,
87+
group_labels=["Q1", "Q2", "Q3"],
88+
group_colors=["#4fc3f7", "#ff7043", "#66bb6a"],
89+
show_values=False,
90+
y_units="Sales",
91+
)
92+
fig3
93+
94+
# %%
95+
# Log-scale value axis
96+
# ---------------------
97+
# Set ``log_scale=True`` for a logarithmic value axis. Non-positive values
98+
# are clamped to ``1e-10`` — no error is raised. Tick marks are placed at
99+
# each decade (10⁰, 10¹, 10², …) with faint minor gridlines at 2×, 3×, 5×
100+
# multiples.
101+
102+
log_labels = ["A", "B", "C", "D", "E"]
103+
log_vals = np.array([1, 10, 100, 1_000, 10_000], dtype=float)
104+
105+
fig4, ax4 = vw.subplots(1, 1, figsize=(500, 340))
106+
bar4 = ax4.bar(
107+
log_labels,
108+
log_vals,
109+
log_scale=True,
110+
color="#ab47bc",
111+
show_values=True,
112+
y_units="Count (log scale)",
113+
)
114+
fig4
115+
116+
# %%
117+
# Side-by-side comparison — update data live
118+
# -------------------------------------------
119+
# Place two :class:`~anyplotlib.figure_plots.PlotBar` panels in one figure.
120+
# Call :meth:`~anyplotlib.figure_plots.PlotBar.update` to swap in Q2 data —
121+
# the value-axis range recalculates automatically.
122+
123+
q1 = np.array([42, 55, 48, 63, 71, 68, 74, 81, 66, 59, 52, 78], dtype=float)
124+
q2 = np.array([58, 61, 70, 75, 69, 83, 90, 88, 77, 64, 71, 95], dtype=float)
125+
all_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
126+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
127+
128+
fig5, (ax_left, ax_right) = vw.subplots(1, 2, figsize=(820, 320))
129+
bar_left = ax_left.bar(
130+
all_months, q1, width=0.6,
131+
color="#4fc3f7", show_values=False, y_units="Q1 sales",
132+
)
133+
bar_right = ax_right.bar(
134+
all_months, q1, width=0.6,
135+
color="#ff7043", show_values=False, y_units="Q2 sales",
136+
)
137+
bar_right.update(q2) # swap in Q2 — axis range recalculates automatically
138+
139+
fig5
140+
141+
# %%
142+
# Mutate colours, annotations, and scale at runtime
143+
# --------------------------------------------------
144+
# :meth:`~anyplotlib.figure_plots.PlotBar.set_color` repaints all bars,
145+
# :meth:`~anyplotlib.figure_plots.PlotBar.set_show_values` toggles labels,
146+
# :meth:`~anyplotlib.figure_plots.PlotBar.set_log_scale` switches the
147+
# value-axis between linear and logarithmic.
148+
149+
bar1.set_color("#ff7043")
150+
bar1.set_show_values(False)
151+
fig1
152+
153+
import numpy as np
154+
import anyplotlib as vw
155+
156+
rng = np.random.default_rng(7)
157+
22158
# ── 1. Vertical bar chart — monthly sales ────────────────────────────────────
23159
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
24160
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

anyplotlib/FIGURE_ESM.md

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -294,30 +294,64 @@ Writes to `model.event_json` + `save_changes()`.
294294

295295
---
296296

297-
### Bar chart (lines 2343–2697)
297+
### Bar chart (lines 2803–2970)
298298

299299
State fields:
300300
```
301-
st.values, st.x_centers, st.x_labels
302-
st.bar_color, st.bar_colors, st.bar_width
301+
st.values [[g0,g1,...], ...] always 2-D (N×G) list; G=1 for ungrouped
302+
st.groups int — number of bar groups per category slot (≥1)
303+
st.x_centers, st.x_labels
304+
st.bar_color, st.bar_colors (ungrouped: per-bar colours)
305+
st.group_colors list[str], length G — colour per group; overrides bar_color
306+
st.group_labels list[str], length G — legend labels (shown when groups > 1)
307+
st.bar_width fraction of slot occupied by all bars in the slot (0–1)
303308
st.orient 'v' (default) | 'h'
304-
st.baseline value axis zero line
305-
st.data_min/max current visible value-axis range — modified by zoom/pan
309+
st.baseline value-axis root; skipped for log scale
310+
st.log_scale bool — logarithmic value axis; non-positive values clamped to 1e-10
311+
st.data_min/max current visible value-axis range
306312
st.x_axis, st.view_x0/x1 widget coordinate system (category axis)
307313
st.overlay_widgets
308314
```
309315

310316
| Function | Lines | Purpose |
311317
|----------|-------|---------|
312-
| `_barGeom(st,r)` | 2347 | Per-bar geometry: slot/bar px, `xToPx`/`yToPx`, baseline px |
313-
| `drawBar(p)` | 2376 | **Main bar render**: grid, bars (clipped), value labels, axis, ticks; calls `drawOverlay1d` |
314-
| `_attachEventsBar(p)` | 2581 | **Full interaction**: wheel zoom on `data_min/max`, left-drag pan on value axis, widget drag via `_ovHitTest1d`/`_doDrag1d`, 'r' reset, per-widget cursors, status bar, bar hover + tooltip, `on_click` |
315-
316-
#### Bar zoom/pan model
317-
Unlike 1D (which zooms `view_x0/x1`), bar zooms and pans the **value axis** by
318-
modifying `st.data_min`/`st.data_max` directly. `view_x0/x1` stays fixed at
319-
0/1 so overlay widgets (vlines, hlines) keep correct positions throughout.
320-
`origDataMin/Max` are captured on first interaction (JS closure) for 'r' reset.
318+
| `_barGeom(st,r)` | ~2808 | Per-bar geometry: slot/group pixel sizes, `xToPx`/`yToPx`, `groupOffsetPx(g)`, `getVal(i,g)`, log-scale coordinate mappers, `basePx` |
319+
| `drawBar(p)` | ~2870 | **Main bar render**: log/linear grid, grouped bars (clipped), value labels, axis borders, log/linear ticks, group legend |
320+
| `_attachEventsBar(p)` | ~2977 | **Full interaction**: widget drag, hover/tooltip (shows group label), `on_click` (emits `bar_index`, `group_index`, `value`, `group_value`), keyboard |
321+
322+
#### `_barGeom` — grouped geometry
323+
324+
For *G* groups per category and bar-width fraction *w*:
325+
```
326+
slotPx = (r.w or r.h) / n — pixel width of one category slot
327+
barPx = slotPx * w / G — pixel width of a single bar
328+
groupOffsetPx(g) = (g - (G-1)/2) * barPx — centre offset for group g
329+
```
330+
`getVal(i, g)` reads from `st.values[i][g]` (2-D) or legacy `st.values[i]`
331+
(scalar) so old 1-D state still renders correctly.
332+
333+
#### Log scale
334+
335+
When `st.log_scale` is true `yToPx`/`xToPx` use `Math.log10` internally:
336+
```js
337+
lv = Math.log10(Math.max(1e-10, v))
338+
py = r.y + r.h - ((lv - lMin) / (lMax - lMin)) * r.h
339+
```
340+
Grid lines: faint minor lines at 2×, 3×, 5× per decade; full-opacity major
341+
lines at each power of 10. Tick labels use superscript notation (`10^N`).
342+
343+
#### Bar zoom/pan model (unchanged)
344+
Unlike 1D (which zooms `view_x0/x1`), bar zooms/pans the **value axis** by
345+
modifying `st.data_min`/`st.data_max` directly. `view_x0/x1` stays fixed
346+
at 0/1 so overlay widgets keep correct positions throughout.
347+
348+
#### `on_click` event payload
349+
```js
350+
{ bar_index, group_index, value, group_value, x_center, x_label }
351+
```
352+
`group_index` is always 0 for ungrouped charts. `group_value` equals
353+
`value` (alias for convenience).
354+
321355

322356
---
323357

anyplotlib/_repr_utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,12 @@ def _widget_px(widget) -> tuple[int, int]:
136136
const _anyCbs = [];
137137
return {{
138138
get(key) {{ return _data[key]; }},
139-
set(key, val) {{ _data[key] = val; }},
139+
set(key, val) {{
140+
_data[key] = val;
141+
const ev = 'change:' + key;
142+
if (_cbs[ev]) for (const cb of [..._cbs[ev]]) try {{ cb({{ new: val }}); }} catch(_) {{}}
143+
for (const cb of [..._anyCbs]) try {{ cb(); }} catch(_) {{}}
144+
}},
140145
save_changes() {{
141146
for (const [ev, cbs] of Object.entries(_cbs))
142147
for (const cb of cbs) try {{ cb({{ new: _data[ev.slice(7)] }}); }} catch(_) {{}}

0 commit comments

Comments
 (0)