plot3 0.6.1__tar.gz → 0.6.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {plot3-0.6.1 → plot3-0.6.3}/PKG-INFO +36 -25
  2. {plot3-0.6.1 → plot3-0.6.3}/README.md +33 -16
  3. plot3-0.6.3/plot3/__version__.py +1 -0
  4. {plot3-0.6.1 → plot3-0.6.3}/plot3/build.py +45 -2
  5. {plot3-0.6.1 → plot3-0.6.3}/plot3/contour.py +2 -2
  6. {plot3-0.6.1 → plot3-0.6.3}/plot3/ggplot.py +3 -3
  7. {plot3-0.6.1 → plot3-0.6.3}/plot3/static.py +9 -7
  8. {plot3-0.6.1 → plot3-0.6.3}/plot3.egg-info/PKG-INFO +36 -25
  9. {plot3-0.6.1 → plot3-0.6.3}/plot3.egg-info/requires.txt +1 -10
  10. {plot3-0.6.1 → plot3-0.6.3}/pyproject.toml +1 -4
  11. {plot3-0.6.1 → plot3-0.6.3}/tests/test_contour.py +1 -1
  12. {plot3-0.6.1 → plot3-0.6.3}/tests/test_grammar.py +42 -0
  13. {plot3-0.6.1 → plot3-0.6.3}/tests/test_static_save.py +2 -2
  14. plot3-0.6.1/plot3/__version__.py +0 -1
  15. {plot3-0.6.1 → plot3-0.6.3}/LICENSE +0 -0
  16. {plot3-0.6.1 → plot3-0.6.3}/plot3/__init__.py +0 -0
  17. {plot3-0.6.1 → plot3-0.6.3}/plot3/aesexpr.py +0 -0
  18. {plot3-0.6.1 → plot3-0.6.3}/plot3/calculus.py +0 -0
  19. {plot3-0.6.1 → plot3-0.6.3}/plot3/compose.py +0 -0
  20. {plot3-0.6.1 → plot3-0.6.3}/plot3/craft.py +0 -0
  21. {plot3-0.6.1 → plot3-0.6.3}/plot3/encode.py +0 -0
  22. {plot3-0.6.1 → plot3-0.6.3}/plot3/expr.py +0 -0
  23. {plot3-0.6.1 → plot3-0.6.3}/plot3/flip.py +0 -0
  24. {plot3-0.6.1 → plot3-0.6.3}/plot3/function.py +0 -0
  25. {plot3-0.6.1 → plot3-0.6.3}/plot3/geoms.py +0 -0
  26. {plot3-0.6.1 → plot3-0.6.3}/plot3/io.py +0 -0
  27. {plot3-0.6.1 → plot3-0.6.3}/plot3/jupyter.py +0 -0
  28. {plot3-0.6.1 → plot3-0.6.3}/plot3/latexin.py +0 -0
  29. {plot3-0.6.1 → plot3-0.6.3}/plot3/masking.py +0 -0
  30. {plot3-0.6.1 → plot3-0.6.3}/plot3/mathtext.py +0 -0
  31. {plot3-0.6.1 → plot3-0.6.3}/plot3/payload.py +0 -0
  32. {plot3-0.6.1 → plot3-0.6.3}/plot3/remote.py +0 -0
  33. {plot3-0.6.1 → plot3-0.6.3}/plot3/scales.py +0 -0
  34. {plot3-0.6.1 → plot3-0.6.3}/plot3/scaling.py +0 -0
  35. {plot3-0.6.1 → plot3-0.6.3}/plot3/special.py +0 -0
  36. {plot3-0.6.1 → plot3-0.6.3}/plot3/stat2d.py +0 -0
  37. {plot3-0.6.1 → plot3-0.6.3}/plot3/stats3d.py +0 -0
  38. {plot3-0.6.1 → plot3-0.6.3}/plot3/table.py +0 -0
  39. {plot3-0.6.1 → plot3-0.6.3}/plot3/themes.py +0 -0
  40. {plot3-0.6.1 → plot3-0.6.3}/plot3/viewer.py +0 -0
  41. {plot3-0.6.1 → plot3-0.6.3}/plot3.egg-info/SOURCES.txt +0 -0
  42. {plot3-0.6.1 → plot3-0.6.3}/plot3.egg-info/dependency_links.txt +0 -0
  43. {plot3-0.6.1 → plot3-0.6.3}/plot3.egg-info/top_level.txt +0 -0
  44. {plot3-0.6.1 → plot3-0.6.3}/setup.cfg +0 -0
  45. {plot3-0.6.1 → plot3-0.6.3}/tests/test_2d_density.py +0 -0
  46. {plot3-0.6.1 → plot3-0.6.3}/tests/test_3d.py +0 -0
  47. {plot3-0.6.1 → plot3-0.6.3}/tests/test_animation.py +0 -0
  48. {plot3-0.6.1 → plot3-0.6.3}/tests/test_annotate.py +0 -0
  49. {plot3-0.6.1 → plot3-0.6.3}/tests/test_api_public.py +0 -0
  50. {plot3-0.6.1 → plot3-0.6.3}/tests/test_array_backend.py +0 -0
  51. {plot3-0.6.1 → plot3-0.6.3}/tests/test_arrow.py +0 -0
  52. {plot3-0.6.1 → plot3-0.6.3}/tests/test_build_contract.py +0 -0
  53. {plot3-0.6.1 → plot3-0.6.3}/tests/test_everyday_ggplot2.py +0 -0
  54. {plot3-0.6.1 → plot3-0.6.3}/tests/test_expr.py +0 -0
  55. {plot3-0.6.1 → plot3-0.6.3}/tests/test_facets_compose.py +0 -0
  56. {plot3-0.6.1 → plot3-0.6.3}/tests/test_geom_function.py +0 -0
  57. {plot3-0.6.1 → plot3-0.6.3}/tests/test_geoms_ds.py +0 -0
  58. {plot3-0.6.1 → plot3-0.6.3}/tests/test_ggplot2_extras2.py +0 -0
  59. {plot3-0.6.1 → plot3-0.6.3}/tests/test_ggplot2_parity.py +0 -0
  60. {plot3-0.6.1 → plot3-0.6.3}/tests/test_ggplot_extras.py +0 -0
  61. {plot3-0.6.1 → plot3-0.6.3}/tests/test_latex_input.py +0 -0
  62. {plot3-0.6.1 → plot3-0.6.3}/tests/test_lidar_scene.py +0 -0
  63. {plot3-0.6.1 → plot3-0.6.3}/tests/test_masking.py +0 -0
  64. {plot3-0.6.1 → plot3-0.6.3}/tests/test_math_layers.py +0 -0
  65. {plot3-0.6.1 → plot3-0.6.3}/tests/test_mathtext.py +0 -0
  66. {plot3-0.6.1 → plot3-0.6.3}/tests/test_payload.py +0 -0
  67. {plot3-0.6.1 → plot3-0.6.3}/tests/test_payload_display.py +0 -0
  68. {plot3-0.6.1 → plot3-0.6.3}/tests/test_point_cloud.py +0 -0
  69. {plot3-0.6.1 → plot3-0.6.3}/tests/test_r_oracle_parity.py +0 -0
  70. {plot3-0.6.1 → plot3-0.6.3}/tests/test_readme.py +0 -0
  71. {plot3-0.6.1 → plot3-0.6.3}/tests/test_remote_bridge.py +0 -0
  72. {plot3-0.6.1 → plot3-0.6.3}/tests/test_review_fixes.py +0 -0
  73. {plot3-0.6.1 → plot3-0.6.3}/tests/test_save_menu.py +0 -0
  74. {plot3-0.6.1 → plot3-0.6.3}/tests/test_scales_api.py +0 -0
  75. {plot3-0.6.1 → plot3-0.6.3}/tests/test_scales_themes.py +0 -0
  76. {plot3-0.6.1 → plot3-0.6.3}/tests/test_special.py +0 -0
  77. {plot3-0.6.1 → plot3-0.6.3}/tests/test_stat2d.py +0 -0
  78. {plot3-0.6.1 → plot3-0.6.3}/tests/test_stats_semantic.py +0 -0
  79. {plot3-0.6.1 → plot3-0.6.3}/tests/test_step3_geoms.py +0 -0
  80. {plot3-0.6.1 → plot3-0.6.3}/tests/test_table_backends.py +0 -0
  81. {plot3-0.6.1 → plot3-0.6.3}/tests/test_titles_theme.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plot3
3
- Version: 0.6.1
3
+ Version: 0.6.3
4
4
  Summary: ggplot2 grammar of graphics for Python: interactive WebGL figures and journal-ready PNG, SVG, and PDF
5
5
  Author: Rigoberto Leyva Salmeron
6
6
  License-Expression: MIT
@@ -17,18 +17,12 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: numpy>=1.24
19
19
  Requires-Dist: pandas>=2.0
20
- Provides-Extra: fast
21
- Requires-Dist: contourpy>=1.0; extra == "fast"
20
+ Provides-Extra: pdf
21
+ Requires-Dist: cairosvg>=2.7; extra == "pdf"
22
22
  Provides-Extra: dev
23
23
  Requires-Dist: pytest>=7.0; extra == "dev"
24
24
  Requires-Dist: polars>=1.0; extra == "dev"
25
25
  Requires-Dist: contourpy>=1.0; extra == "dev"
26
- Provides-Extra: jupyter
27
- Requires-Dist: ipython>=8.0; extra == "jupyter"
28
- Provides-Extra: polars
29
- Requires-Dist: polars>=1.0; extra == "polars"
30
- Provides-Extra: export
31
- Requires-Dist: cairosvg>=2.7; extra == "export"
32
26
  Dynamic: license-file
33
27
 
34
28
  # plot3
@@ -57,7 +51,7 @@ in the notebook and save as journal-ready PNG, SVG, or PDF.
57
51
  </table>
58
52
 
59
53
  ```bash
60
- pip install "plot3[jupyter,export]"
54
+ pip install plot3
61
55
  ```
62
56
 
63
57
  Contents: [Plot an equation](#plot-an-equation) ·
@@ -224,18 +218,27 @@ ggplot2's names and defaults.
224
218
 
225
219
  ```python
226
220
  rng = np.random.default_rng(1)
227
- trial = pd.DataFrame({"arm": rng.choice(["placebo", "low", "high"], 150),
221
+ arms = ["placebo", "low", "high"]
222
+ trial = pd.DataFrame({"arm": pd.Categorical(rng.choice(arms, 150), categories=arms),
228
223
  "dose": rng.uniform(0, 10, 150)})
229
- trial["response"] = 2 + 0.6 * trial.dose + 1.5 * (trial.arm == "high") + rng.normal(0, 1.2, 150)
224
+ slope = trial.arm.map({"placebo": 0.05, "low": 0.35, "high": 0.7}).astype(float)
225
+ trial["response"] = 2 + slope * trial.dose + rng.normal(0, 0.9, 150)
230
226
 
231
227
  a = (ggplot(trial, aes(x="dose", y="response", colour="arm"))
232
- + geom_point(alpha=0.7) + geom_smooth(method="lm"))
228
+ + geom_point(alpha=0.7) + geom_smooth(method="lm", se=False)
229
+ + scale_colour_hue() + theme_grey() + theme(legend_position="bottom"))
233
230
  b = (ggplot(trial, aes(x="arm", y="response", fill="arm"))
234
- + geom_violin() + geom_jitter(width=0.1, height=0) + theme(legend_position="none"))
235
- fig = (a | b) + plot_annotation(title="Response by dose and arm", tag_levels="A")
231
+ + geom_violin() + geom_jitter(width=0.1, height=0)
232
+ + scale_fill_hue() + theme_grey() + theme(legend_position="none"))
233
+ fig = ((a | b) + plot_layout(widths=[3, 2])
234
+ + plot_annotation(title="Response by dose and arm", tag_levels="A"))
236
235
  ```
237
236
 
238
- <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/ggplot2_compose.png" alt="Two-panel figure: scatter with linear fits, and violins by arm">
237
+ `scale_colour_hue()` and `theme_grey()` are ggplot2's own defaults, so the
238
+ figure looks as it would from R. plot3's defaults are `theme_bw()` for saved
239
+ files and `theme_dark()` in the viewer.
240
+
241
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/ggplot2_look.png" alt="Two-panel figure in ggplot2's grey theme: response against dose with one fitted line per arm, and violins of response by arm">
239
242
 
240
243
  Everything else you'd expect is there, including histograms, densities,
241
244
  boxplots, bar positions, `stat_summary`, error bars, heatmaps, hexbins, 2D
@@ -270,27 +273,35 @@ In notebooks, bare column names work as in R: `aes(x=dose, y=response)`.
270
273
  ## Install
271
274
 
272
275
  ```bash
273
- pip install "plot3[jupyter,export]"
276
+ pip install plot3
274
277
  ```
275
278
 
276
279
  Python 3.10 or newer; NumPy and pandas are the only required packages.
280
+ Notebooks, pandas, and Polars tables work with no extras.
281
+
282
+ For PDF, and PNG with real fonts, add cairosvg and the Cairo library. SVG
283
+ needs neither.
284
+
285
+ ```bash
286
+ pip install cairosvg
287
+ ```
288
+
289
+ ```bash
290
+ brew install cairo
291
+ ```
277
292
 
278
- | Extra | Adds |
279
- |---|---|
280
- | `export` | `cairosvg`: PNG with real fonts and PDF. Needs the Cairo C library (`brew install cairo`, `apt install libcairo2`). SVG needs nothing. |
281
- | `fast` | `contourpy`: faster implicit-curve contours (matplotlib users have it) |
282
- | `jupyter` | IPython integration (bare column names, `%plot3`) |
283
- | `polars` | Polars tables |
293
+ (`apt install libcairo2` on Debian and Ubuntu.) `pip install "plot3[pdf]"`
294
+ installs plot3 and cairosvg together.
284
295
 
285
296
  The latest unreleased code installs from GitHub with
286
- `pip install "plot3[jupyter,export] @ git+https://github.com/rleyvasal/plot3"`.
297
+ `pip install "plot3 @ git+https://github.com/rleyvasal/plot3"`.
287
298
 
288
299
  ## Development
289
300
 
290
301
  ```bash
291
302
  git clone https://github.com/rleyvasal/plot3 && cd plot3
292
303
  python3 -m venv .venv && source .venv/bin/activate
293
- pip install -e ".[dev,jupyter,export]"
304
+ pip install -e ".[dev]"
294
305
  pytest -q # includes every example in this README and docs/reference.md
295
306
  python examples/showcase_2d.py # 2D gallery in the browser
296
307
  python examples/showcase_3d.py # 3D gallery
@@ -24,7 +24,7 @@ in the notebook and save as journal-ready PNG, SVG, or PDF.
24
24
  </table>
25
25
 
26
26
  ```bash
27
- pip install "plot3[jupyter,export]"
27
+ pip install plot3
28
28
  ```
29
29
 
30
30
  Contents: [Plot an equation](#plot-an-equation) ·
@@ -191,18 +191,27 @@ ggplot2's names and defaults.
191
191
 
192
192
  ```python
193
193
  rng = np.random.default_rng(1)
194
- trial = pd.DataFrame({"arm": rng.choice(["placebo", "low", "high"], 150),
194
+ arms = ["placebo", "low", "high"]
195
+ trial = pd.DataFrame({"arm": pd.Categorical(rng.choice(arms, 150), categories=arms),
195
196
  "dose": rng.uniform(0, 10, 150)})
196
- trial["response"] = 2 + 0.6 * trial.dose + 1.5 * (trial.arm == "high") + rng.normal(0, 1.2, 150)
197
+ slope = trial.arm.map({"placebo": 0.05, "low": 0.35, "high": 0.7}).astype(float)
198
+ trial["response"] = 2 + slope * trial.dose + rng.normal(0, 0.9, 150)
197
199
 
198
200
  a = (ggplot(trial, aes(x="dose", y="response", colour="arm"))
199
- + geom_point(alpha=0.7) + geom_smooth(method="lm"))
201
+ + geom_point(alpha=0.7) + geom_smooth(method="lm", se=False)
202
+ + scale_colour_hue() + theme_grey() + theme(legend_position="bottom"))
200
203
  b = (ggplot(trial, aes(x="arm", y="response", fill="arm"))
201
- + geom_violin() + geom_jitter(width=0.1, height=0) + theme(legend_position="none"))
202
- fig = (a | b) + plot_annotation(title="Response by dose and arm", tag_levels="A")
204
+ + geom_violin() + geom_jitter(width=0.1, height=0)
205
+ + scale_fill_hue() + theme_grey() + theme(legend_position="none"))
206
+ fig = ((a | b) + plot_layout(widths=[3, 2])
207
+ + plot_annotation(title="Response by dose and arm", tag_levels="A"))
203
208
  ```
204
209
 
205
- <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/ggplot2_compose.png" alt="Two-panel figure: scatter with linear fits, and violins by arm">
210
+ `scale_colour_hue()` and `theme_grey()` are ggplot2's own defaults, so the
211
+ figure looks as it would from R. plot3's defaults are `theme_bw()` for saved
212
+ files and `theme_dark()` in the viewer.
213
+
214
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/ggplot2_look.png" alt="Two-panel figure in ggplot2's grey theme: response against dose with one fitted line per arm, and violins of response by arm">
206
215
 
207
216
  Everything else you'd expect is there, including histograms, densities,
208
217
  boxplots, bar positions, `stat_summary`, error bars, heatmaps, hexbins, 2D
@@ -237,27 +246,35 @@ In notebooks, bare column names work as in R: `aes(x=dose, y=response)`.
237
246
  ## Install
238
247
 
239
248
  ```bash
240
- pip install "plot3[jupyter,export]"
249
+ pip install plot3
241
250
  ```
242
251
 
243
252
  Python 3.10 or newer; NumPy and pandas are the only required packages.
253
+ Notebooks, pandas, and Polars tables work with no extras.
254
+
255
+ For PDF, and PNG with real fonts, add cairosvg and the Cairo library. SVG
256
+ needs neither.
257
+
258
+ ```bash
259
+ pip install cairosvg
260
+ ```
261
+
262
+ ```bash
263
+ brew install cairo
264
+ ```
244
265
 
245
- | Extra | Adds |
246
- |---|---|
247
- | `export` | `cairosvg`: PNG with real fonts and PDF. Needs the Cairo C library (`brew install cairo`, `apt install libcairo2`). SVG needs nothing. |
248
- | `fast` | `contourpy`: faster implicit-curve contours (matplotlib users have it) |
249
- | `jupyter` | IPython integration (bare column names, `%plot3`) |
250
- | `polars` | Polars tables |
266
+ (`apt install libcairo2` on Debian and Ubuntu.) `pip install "plot3[pdf]"`
267
+ installs plot3 and cairosvg together.
251
268
 
252
269
  The latest unreleased code installs from GitHub with
253
- `pip install "plot3[jupyter,export] @ git+https://github.com/rleyvasal/plot3"`.
270
+ `pip install "plot3 @ git+https://github.com/rleyvasal/plot3"`.
254
271
 
255
272
  ## Development
256
273
 
257
274
  ```bash
258
275
  git clone https://github.com/rleyvasal/plot3 && cd plot3
259
276
  python3 -m venv .venv && source .venv/bin/activate
260
- pip install -e ".[dev,jupyter,export]"
277
+ pip install -e ".[dev]"
261
278
  pytest -q # includes every example in this README and docs/reference.md
262
279
  python examples/showcase_2d.py # 2D gallery in the browser
263
280
  python examples/showcase_3d.py # 3D gallery
@@ -0,0 +1 @@
1
+ __version__ = "0.6.3"
@@ -762,7 +762,7 @@ def expand_stat_geom(
762
762
  if len(group_cols) > 1:
763
763
  out_row[colour_col] = key_tuple[1]
764
764
  outlier_rows.append(out_row)
765
- frame = pd.DataFrame(rows)
765
+ frame = _keep_categories(pd.DataFrame(rows), data, [colour_col])
766
766
  box_width = float(getattr(geom, "width", 0.75))
767
767
  dodge_levels = None
768
768
  if len(group_cols) > 1 and not frame.empty:
@@ -920,7 +920,7 @@ def expand_stat_geom(
920
920
  elif colour_col == xcol:
921
921
  row[colour_col] = x_key
922
922
  rows.append(row)
923
- frame = pd.DataFrame(rows)
923
+ frame = _keep_categories(pd.DataFrame(rows), data, [colour_col])
924
924
  if frame.empty:
925
925
  frame = pd.DataFrame(columns=["x", "y", "group"])
926
926
  # Force categorical x scale labels via a helper frame for domains:
@@ -1423,6 +1423,48 @@ def _legend_position_spec(value):
1423
1423
 
1424
1424
 
1425
1425
 
1426
+ def _categorical_order(g, resolved, levels) -> list:
1427
+ """Colour levels in the order of a categorical colour column, as ggplot2.
1428
+
1429
+ Stats (density, histogram, smooth, summaries) rebuild their data, so
1430
+ their levels arrive sorted as text. When the plot's colour column is a
1431
+ ``pd.Categorical``, its categories set the order (and so the colours);
1432
+ levels it does not list keep their place after them.
1433
+ """
1434
+ data = getattr(g, "data", None)
1435
+ if data is None:
1436
+ return list(levels)
1437
+ columns = dict.fromkeys(m.get("color") for _geom, m in resolved if isinstance(m.get("color"), str))
1438
+ for column in columns:
1439
+ if not has_column(data, column):
1440
+ continue
1441
+ source = materialize_columns(data, [column])[column]
1442
+ if not isinstance(source.dtype, pd.CategoricalDtype):
1443
+ continue
1444
+ wanted = [str(c) for c in source.cat.categories]
1445
+ present = [str(level) for level in levels]
1446
+ first = [c for c in wanted if c in present]
1447
+ return first + [level for level in present if level not in first]
1448
+ return list(levels)
1449
+
1450
+
1451
+ def _keep_categories(frame: pd.DataFrame, data, columns) -> pd.DataFrame:
1452
+ """Give a stat's output columns the category order of the source column.
1453
+
1454
+ Box and violin stats build their output from plain rows, which turns
1455
+ ``pd.Categorical(..., categories=["placebo", "low", "high"])`` into
1456
+ text, sorted alphabetically: the fill colours then disagree with the
1457
+ points of the same column in another panel or layer.
1458
+ """
1459
+ for column in dict.fromkeys(c for c in columns if c):
1460
+ if column not in frame.columns or not has_column(data, column):
1461
+ continue
1462
+ source = materialize_columns(data, [column])[column]
1463
+ if isinstance(source.dtype, pd.CategoricalDtype):
1464
+ frame[column] = pd.Categorical(frame[column], categories=list(source.cat.categories))
1465
+ return frame
1466
+
1467
+
1426
1468
  def _forced_levels(g, ccats) -> list:
1427
1469
  """Colour levels shared by every facet panel, so a group keeps its colour."""
1428
1470
  forced = getattr(g, "_force_color_levels", None)
@@ -2826,6 +2868,7 @@ def build_spec(g: ggplot) -> tuple[dict, list[tuple[str, str]]]:
2826
2868
  # viridis_d scale, else the theme's colours extended past eight groups.
2827
2869
  cat_colours: list[str] = []
2828
2870
  if color_scale is not None and color_scale[0] == "cat":
2871
+ color_scale[1] = _categorical_order(g, resolved, color_scale[1])
2829
2872
  user_scale = getattr(g, "colour_scale", None)
2830
2873
  if user_scale is not None and user_scale.kind == "continuous":
2831
2874
  raise ValueError(
@@ -115,7 +115,7 @@ def _backend_name() -> str:
115
115
  if _load_contourpy() is None:
116
116
  raise ValueError(
117
117
  "PLOT3_CONTOUR_BACKEND=contourpy but contourpy>=1.0 is not "
118
- "installed. Install it with: pip install plot3[fast]"
118
+ "installed. Install it with: pip install contourpy"
119
119
  )
120
120
  return "contourpy"
121
121
  if _load_contourpy() is None:
@@ -346,7 +346,7 @@ def _contour_contourpy(
346
346
  if contourpy is None:
347
347
  raise ValueError(
348
348
  "PLOT3_CONTOUR_BACKEND=contourpy but contourpy>=1.0 is not "
349
- "installed. Install it with: pip install plot3[fast]"
349
+ "installed. Install it with: pip install contourpy"
350
350
  )
351
351
  masked = np.ma.masked_invalid(np.asarray(field, dtype=np.float64))
352
352
  generator = contourpy.contour_generator(
@@ -517,7 +517,7 @@ class ggplot:
517
517
  suffix writes the standalone HTML viewer. ``width`` and ``height``
518
518
  are pixels unless ``units`` is ``"in"``, ``"cm"``, or ``"mm"``.
519
519
  ``dpi`` is the PNG resolution for a physical size (default 300).
520
- PDF needs ``plot3[export]`` and the Cairo C library. ``.svg``
520
+ PDF needs cairosvg and the Cairo C library. ``.svg``
521
521
  needs neither.
522
522
  """
523
523
  suffix = Path(path).suffix.lower()
@@ -683,8 +683,8 @@ def ggsave(
683
683
  """Save ``plot``.
684
684
 
685
685
  ``ggsave("fig.png", p)`` and ``ggsave("fig.svg", p)`` write a static
686
- image. ``ggsave("fig.pdf", p)`` writes a PDF when ``plot3[export]`` is
687
- installed. That extra also needs the Cairo C library; ``.svg`` writes
686
+ image. ``ggsave("fig.pdf", p)`` writes a PDF when cairosvg is
687
+ installed (``plot3[pdf]``). It also needs the Cairo C library; ``.svg`` writes
688
688
  the same drawing with no extra dependencies. Any other suffix,
689
689
  including ``.html``, writes the standalone viewer. The plot and the
690
690
  filename can be passed in either order.
@@ -2,7 +2,7 @@
2
2
 
3
3
  PNG, SVG, and PDF replay one list of drawing commands. SVG is text.
4
4
  PNG and PDF are that same SVG rendered by cairosvg when the optional
5
- ``plot3[export]`` extra is installed, so the three files share one
5
+ ``plot3[pdf]`` extra (cairosvg) is installed, so the three files share one
6
6
  drawing and one font. That extra needs the Cairo C library as well as
7
7
  the Python package (``libcairo2``, or the GTK runtime on Windows).
8
8
  Without cairosvg, PNG falls back to a zlib RGB file and a built-in
@@ -42,7 +42,7 @@ _UNIT_INCH = {
42
42
  }
43
43
  _EXPORT_HINT = (
44
44
  "ggsave() needs the Cairo C library to write PDF with a journal font. "
45
- "Install the Python extra with: pip install 'plot3[export]'. "
45
+ "Install it with: pip install cairosvg. "
46
46
  "Windows and minimal Linux images also need the Cairo library itself "
47
47
  "(for example the libcairo2 package, or the GTK runtime). "
48
48
  "ggsave('fig.svg', plot) writes the same drawing with no extra dependencies."
@@ -196,8 +196,8 @@ def save_static(
196
196
  ``"cm"`` and ``"mm"``) with ``dpi`` (default 300) sets a physical
197
197
  page. Layout stays in CSS pixels (96 per inch) so type keeps its
198
198
  size, and cairosvg rasterizes that SVG at ``dpi``. PDF and a
199
- journal-font PNG need the Cairo C library (``pip install
200
- 'plot3[export]'``). ``.svg`` needs nothing extra.
199
+ journal-font PNG need cairosvg and the Cairo C library (``pip install
200
+ cairosvg``). ``.svg`` needs nothing extra.
201
201
  """
202
202
  path = Path(path)
203
203
  suffix = path.suffix.lower()
@@ -251,7 +251,7 @@ def save_static(
251
251
  if fallback:
252
252
  print(
253
253
  "plot3: PNG used the built-in font. "
254
- "pip install 'plot3[export]' for Helvetica."
254
+ "pip install cairosvg for Helvetica."
255
255
  )
256
256
  return str(path)
257
257
 
@@ -2196,13 +2196,15 @@ def _paint_legend(commands, origin, metrics, theme, fonts) -> None:
2196
2196
  surface = theme.get("surface") or "#0b1020"
2197
2197
  grid = theme.get("grid") or "#1c2742"
2198
2198
  if _rgb(grid) == _rgb(surface):
2199
- grid = theme.get("muted") or "#898781"
2199
+ # theme_grey / theme_classic: white grid on a white page. ggplot2
2200
+ # draws no legend box, so neither do we (not a dark one instead).
2201
+ grid = None
2200
2202
  tick = fonts[0]
2201
2203
  row_h = metrics["row_h"]
2202
2204
  lx, ly = origin
2203
2205
  # Opaque, so a legend inside the panel does not fade the marks under it.
2204
2206
  commands.append((
2205
- "rect", lx, ly, metrics["w"], metrics["h"], surface, grid, 1, 1.0,
2207
+ "rect", lx, ly, metrics["w"], metrics["h"], surface, grid, 1 if grid else 0, 1.0,
2206
2208
  ))
2207
2209
  cursor = ly + 6
2208
2210
  for row in metrics["rows"]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plot3
3
- Version: 0.6.1
3
+ Version: 0.6.3
4
4
  Summary: ggplot2 grammar of graphics for Python: interactive WebGL figures and journal-ready PNG, SVG, and PDF
5
5
  Author: Rigoberto Leyva Salmeron
6
6
  License-Expression: MIT
@@ -17,18 +17,12 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: numpy>=1.24
19
19
  Requires-Dist: pandas>=2.0
20
- Provides-Extra: fast
21
- Requires-Dist: contourpy>=1.0; extra == "fast"
20
+ Provides-Extra: pdf
21
+ Requires-Dist: cairosvg>=2.7; extra == "pdf"
22
22
  Provides-Extra: dev
23
23
  Requires-Dist: pytest>=7.0; extra == "dev"
24
24
  Requires-Dist: polars>=1.0; extra == "dev"
25
25
  Requires-Dist: contourpy>=1.0; extra == "dev"
26
- Provides-Extra: jupyter
27
- Requires-Dist: ipython>=8.0; extra == "jupyter"
28
- Provides-Extra: polars
29
- Requires-Dist: polars>=1.0; extra == "polars"
30
- Provides-Extra: export
31
- Requires-Dist: cairosvg>=2.7; extra == "export"
32
26
  Dynamic: license-file
33
27
 
34
28
  # plot3
@@ -57,7 +51,7 @@ in the notebook and save as journal-ready PNG, SVG, or PDF.
57
51
  </table>
58
52
 
59
53
  ```bash
60
- pip install "plot3[jupyter,export]"
54
+ pip install plot3
61
55
  ```
62
56
 
63
57
  Contents: [Plot an equation](#plot-an-equation) ·
@@ -224,18 +218,27 @@ ggplot2's names and defaults.
224
218
 
225
219
  ```python
226
220
  rng = np.random.default_rng(1)
227
- trial = pd.DataFrame({"arm": rng.choice(["placebo", "low", "high"], 150),
221
+ arms = ["placebo", "low", "high"]
222
+ trial = pd.DataFrame({"arm": pd.Categorical(rng.choice(arms, 150), categories=arms),
228
223
  "dose": rng.uniform(0, 10, 150)})
229
- trial["response"] = 2 + 0.6 * trial.dose + 1.5 * (trial.arm == "high") + rng.normal(0, 1.2, 150)
224
+ slope = trial.arm.map({"placebo": 0.05, "low": 0.35, "high": 0.7}).astype(float)
225
+ trial["response"] = 2 + slope * trial.dose + rng.normal(0, 0.9, 150)
230
226
 
231
227
  a = (ggplot(trial, aes(x="dose", y="response", colour="arm"))
232
- + geom_point(alpha=0.7) + geom_smooth(method="lm"))
228
+ + geom_point(alpha=0.7) + geom_smooth(method="lm", se=False)
229
+ + scale_colour_hue() + theme_grey() + theme(legend_position="bottom"))
233
230
  b = (ggplot(trial, aes(x="arm", y="response", fill="arm"))
234
- + geom_violin() + geom_jitter(width=0.1, height=0) + theme(legend_position="none"))
235
- fig = (a | b) + plot_annotation(title="Response by dose and arm", tag_levels="A")
231
+ + geom_violin() + geom_jitter(width=0.1, height=0)
232
+ + scale_fill_hue() + theme_grey() + theme(legend_position="none"))
233
+ fig = ((a | b) + plot_layout(widths=[3, 2])
234
+ + plot_annotation(title="Response by dose and arm", tag_levels="A"))
236
235
  ```
237
236
 
238
- <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/ggplot2_compose.png" alt="Two-panel figure: scatter with linear fits, and violins by arm">
237
+ `scale_colour_hue()` and `theme_grey()` are ggplot2's own defaults, so the
238
+ figure looks as it would from R. plot3's defaults are `theme_bw()` for saved
239
+ files and `theme_dark()` in the viewer.
240
+
241
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/ggplot2_look.png" alt="Two-panel figure in ggplot2's grey theme: response against dose with one fitted line per arm, and violins of response by arm">
239
242
 
240
243
  Everything else you'd expect is there, including histograms, densities,
241
244
  boxplots, bar positions, `stat_summary`, error bars, heatmaps, hexbins, 2D
@@ -270,27 +273,35 @@ In notebooks, bare column names work as in R: `aes(x=dose, y=response)`.
270
273
  ## Install
271
274
 
272
275
  ```bash
273
- pip install "plot3[jupyter,export]"
276
+ pip install plot3
274
277
  ```
275
278
 
276
279
  Python 3.10 or newer; NumPy and pandas are the only required packages.
280
+ Notebooks, pandas, and Polars tables work with no extras.
281
+
282
+ For PDF, and PNG with real fonts, add cairosvg and the Cairo library. SVG
283
+ needs neither.
284
+
285
+ ```bash
286
+ pip install cairosvg
287
+ ```
288
+
289
+ ```bash
290
+ brew install cairo
291
+ ```
277
292
 
278
- | Extra | Adds |
279
- |---|---|
280
- | `export` | `cairosvg`: PNG with real fonts and PDF. Needs the Cairo C library (`brew install cairo`, `apt install libcairo2`). SVG needs nothing. |
281
- | `fast` | `contourpy`: faster implicit-curve contours (matplotlib users have it) |
282
- | `jupyter` | IPython integration (bare column names, `%plot3`) |
283
- | `polars` | Polars tables |
293
+ (`apt install libcairo2` on Debian and Ubuntu.) `pip install "plot3[pdf]"`
294
+ installs plot3 and cairosvg together.
284
295
 
285
296
  The latest unreleased code installs from GitHub with
286
- `pip install "plot3[jupyter,export] @ git+https://github.com/rleyvasal/plot3"`.
297
+ `pip install "plot3 @ git+https://github.com/rleyvasal/plot3"`.
287
298
 
288
299
  ## Development
289
300
 
290
301
  ```bash
291
302
  git clone https://github.com/rleyvasal/plot3 && cd plot3
292
303
  python3 -m venv .venv && source .venv/bin/activate
293
- pip install -e ".[dev,jupyter,export]"
304
+ pip install -e ".[dev]"
294
305
  pytest -q # includes every example in this README and docs/reference.md
295
306
  python examples/showcase_2d.py # 2D gallery in the browser
296
307
  python examples/showcase_3d.py # 3D gallery
@@ -6,14 +6,5 @@ pytest>=7.0
6
6
  polars>=1.0
7
7
  contourpy>=1.0
8
8
 
9
- [export]
9
+ [pdf]
10
10
  cairosvg>=2.7
11
-
12
- [fast]
13
- contourpy>=1.0
14
-
15
- [jupyter]
16
- ipython>=8.0
17
-
18
- [polars]
19
- polars>=1.0
@@ -25,11 +25,8 @@ dependencies = [
25
25
  ]
26
26
 
27
27
  [project.optional-dependencies]
28
- fast = ["contourpy>=1.0"]
28
+ pdf = ["cairosvg>=2.7"]
29
29
  dev = ["pytest>=7.0", "polars>=1.0", "contourpy>=1.0"]
30
- jupyter = ["ipython>=8.0"]
31
- polars = ["polars>=1.0"]
32
- export = ["cairosvg>=2.7"]
33
30
 
34
31
  [project.urls]
35
32
  Homepage = "https://github.com/rleyvasal/plot3"
@@ -265,7 +265,7 @@ def test_version_floor_and_backend_selection(monkeypatch):
265
265
  assert _backend_name() == "numpy"
266
266
  monkeypatch.setenv("PLOT3_CONTOUR_BACKEND", "contourpy")
267
267
  _reset_contour_backend()
268
- with pytest.raises(ValueError, match=r"plot3\[fast\]"):
268
+ with pytest.raises(ValueError, match=r"pip install contourpy"):
269
269
  _contour_lines(np.linspace(-1, 1, 4), np.linspace(-1, 1, 4), np.zeros((4, 4)), 0.0)
270
270
 
271
271
  fake = types.ModuleType("contourpy")
@@ -135,3 +135,45 @@ def test_geom_smooth_takes_a_linetype():
135
135
  assert [layer.get("dash") for layer in spec["layers"] if layer["kind"] == "line"] == [[4.0, 4.0]]
136
136
  with pytest.raises(ValueError):
137
137
  geom_smooth(linetype="wavy")
138
+
139
+
140
+ @pytest.mark.parametrize("kind", ["violin", "boxplot", "density", "histogram", "bar", "smooth", "summary"])
141
+ def test_categorical_colour_order_survives_stats(kind):
142
+ # pd.Categorical(categories=["placebo", "low", "high"]) sets the colour
143
+ # order for every geom, as in ggplot2; stats used to sort it as text, so
144
+ # a violin and a scatter of the same column disagreed on colours.
145
+ import numpy as np
146
+ import pandas as pd
147
+
148
+ from plot3 import (aes, geom_bar, geom_boxplot, geom_density, geom_histogram,
149
+ geom_smooth, geom_violin, ggplot, stat_summary)
150
+ from plot3.build import build_spec
151
+
152
+ rng = np.random.default_rng(0)
153
+ arms = ["placebo", "low", "high"]
154
+ df = pd.DataFrame({"arm": pd.Categorical(rng.choice(arms, 90), categories=arms),
155
+ "x": rng.uniform(0, 10, 90), "y": rng.normal(size=90),
156
+ "sex": rng.choice(["F", "M"], 90)})
157
+ layer = {
158
+ "violin": (aes(x="arm", y="y", fill="arm"), geom_violin()),
159
+ "boxplot": (aes(x="sex", y="y", fill="arm"), geom_boxplot()),
160
+ "density": (aes(x="y", colour="arm"), geom_density()),
161
+ "histogram": (aes(x="y", fill="arm"), geom_histogram(bins=10)),
162
+ "bar": (aes(x="sex", fill="arm"), geom_bar()),
163
+ "smooth": (aes(x="x", y="y", colour="arm"), geom_smooth(method="lm")),
164
+ "summary": (aes(x="sex", y="y", colour="arm"), stat_summary()),
165
+ }[kind]
166
+ spec, _ = build_spec(ggplot(df, layer[0]) + layer[1])
167
+ assert spec["color"]["cats"] == arms
168
+ assert [entry["label"] for entry in spec["legend"]] == arms
169
+
170
+
171
+ def test_theme_grey_legend_has_no_dark_frame(tmp_path):
172
+ import pandas as pd
173
+
174
+ from plot3 import aes, geom_point, ggplot, ggsave, theme_grey
175
+
176
+ df = pd.DataFrame({"x": [1, 2], "y": [1, 2], "g": ["a", "b"]})
177
+ out = tmp_path / "g.svg"
178
+ ggsave(str(out), ggplot(df, aes(x="x", y="y", colour="g")) + geom_point() + theme_grey())
179
+ assert 'stroke="#4d4d4d"' not in out.read_text()
@@ -438,7 +438,7 @@ def test_inches_and_dpi_set_the_png_page(tmp_path, monkeypatch, capsys):
438
438
  rgb = _read_png(path)
439
439
  assert rgb.shape == (1200, 2100, 3)
440
440
  assert tuple(rgb[0, 0]) == (255, 255, 255)
441
- assert "plot3[export]" in capsys.readouterr().out
441
+ assert "pip install cairosvg" in capsys.readouterr().out
442
442
 
443
443
  cm = tmp_path / "cm.png"
444
444
  ggsave(cm, fig, width=2.54, height=2.54, units="cm", dpi=100)
@@ -466,7 +466,7 @@ def test_pdf_without_cairosvg_names_the_extra(tmp_path, monkeypatch):
466
466
  from plot3 import static
467
467
 
468
468
  monkeypatch.setattr(static, "_load_cairosvg", lambda: None)
469
- with pytest.raises(RuntimeError, match=r"plot3\[export\]") as caught:
469
+ with pytest.raises(RuntimeError, match=r"pip install cairosvg") as caught:
470
470
  ggsave(tmp_path / "fig.pdf", _curve())
471
471
  message = str(caught.value)
472
472
  assert "Cairo" in message
@@ -1 +0,0 @@
1
- __version__ = "0.6.1"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes