plot3 0.6.0__py3-none-any.whl → 0.6.2__py3-none-any.whl

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.
plot3/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.6.0"
1
+ __version__ = "0.6.2"
plot3/build.py CHANGED
@@ -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(
@@ -3953,8 +3996,9 @@ html,body{{margin:0;height:100%;background:{theme["surface"]};color:{theme["ink"
3953
3996
  border:1px solid {theme["axis"]};border-radius:6px;overflow:hidden;
3954
3997
  background:{theme["surface"]}}}
3955
3998
  .plab{{padding:4px 8px;font-size:11px;color:{theme["ink2"]};
3999
+ background:{theme.get("strip") or "transparent"};
3956
4000
  border-bottom:1px solid {theme["grid"]}}}
3957
- .cstrip,.rstrip{{background:{theme["grid"]};color:{theme["ink2"]};font-size:11px;
4001
+ .cstrip,.rstrip{{background:{theme.get("strip") or theme["grid"]};color:{theme["ink2"]};font-size:11px;
3958
4002
  font-weight:600;display:flex;align-items:center;justify-content:center;border-radius:4px}}
3959
4003
  .rstrip span{{writing-mode:vertical-rl}}
3960
4004
  .panel iframe{{flex:1;width:100%;border:0;background:{theme["surface"]}}}
plot3/static.py CHANGED
@@ -650,7 +650,9 @@ def _draw_facets(panels, layout, first_spec, theme, width, height, base_pt, comm
650
650
  0, 0, width, height, commands,
651
651
  )
652
652
  pad = 8.0
653
- strip_bg = theme.get("grid") or "#1c2742"
653
+ # ggplot2's facet strips are grey85 in theme_grey and theme_bw; other
654
+ # themes fall back to the grid colour.
655
+ strip_bg = theme.get("strip") or theme.get("grid") or "#1c2742"
654
656
  ink2 = theme.get("ink2") or theme.get("ink") or "#ffffff"
655
657
  raw_labs = first_spec.get("labs") or {}
656
658
  x_title = _with_frame(raw_labs.get("x") or "", label)
@@ -2194,13 +2196,15 @@ def _paint_legend(commands, origin, metrics, theme, fonts) -> None:
2194
2196
  surface = theme.get("surface") or "#0b1020"
2195
2197
  grid = theme.get("grid") or "#1c2742"
2196
2198
  if _rgb(grid) == _rgb(surface):
2197
- 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
2198
2202
  tick = fonts[0]
2199
2203
  row_h = metrics["row_h"]
2200
2204
  lx, ly = origin
2201
2205
  # Opaque, so a legend inside the panel does not fade the marks under it.
2202
2206
  commands.append((
2203
- "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,
2204
2208
  ))
2205
2209
  cursor = ly + 6
2206
2210
  for row in metrics["rows"]:
plot3/themes.py CHANGED
@@ -53,7 +53,7 @@ _THEMES = {
53
53
  "bw": dict(
54
54
  surface="#ffffff", ink="#000000", ink2="#222222", muted="#4d4d4d",
55
55
  grid="#ebebeb", axis="#333333", cat=_CAT_LIGHT, seq=_SEQ,
56
- frame="box",
56
+ frame="box", strip="#d9d9d9",
57
57
  ),
58
58
  # ggplot2 theme_classic: white page, no grid, black axis lines.
59
59
  "classic": dict(
@@ -78,7 +78,7 @@ _THEMES = {
78
78
  "grey": dict(
79
79
  surface="#ffffff", panel="#ebebeb", ink="#000000", ink2="#000000",
80
80
  muted="#4d4d4d", grid="#ffffff", axis="#ebebeb", cat=_CAT_LIGHT, seq=_SEQ,
81
- frame="none",
81
+ frame="none", strip="#d9d9d9",
82
82
  ),
83
83
  # ggplot2 theme_linedraw: white panel, thin dark grid, black border.
84
84
  "linedraw": dict(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plot3
3
- Version: 0.6.0
3
+ Version: 0.6.2
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
@@ -224,18 +224,27 @@ ggplot2's names and defaults.
224
224
 
225
225
  ```python
226
226
  rng = np.random.default_rng(1)
227
- trial = pd.DataFrame({"arm": rng.choice(["placebo", "low", "high"], 150),
227
+ arms = ["placebo", "low", "high"]
228
+ trial = pd.DataFrame({"arm": pd.Categorical(rng.choice(arms, 150), categories=arms),
228
229
  "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)
230
+ slope = trial.arm.map({"placebo": 0.05, "low": 0.35, "high": 0.7}).astype(float)
231
+ trial["response"] = 2 + slope * trial.dose + rng.normal(0, 0.9, 150)
230
232
 
231
233
  a = (ggplot(trial, aes(x="dose", y="response", colour="arm"))
232
- + geom_point(alpha=0.7) + geom_smooth(method="lm"))
234
+ + geom_point(alpha=0.7) + geom_smooth(method="lm", se=False)
235
+ + scale_colour_hue() + theme_grey() + theme(legend_position="bottom"))
233
236
  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")
237
+ + geom_violin() + geom_jitter(width=0.1, height=0)
238
+ + scale_fill_hue() + theme_grey() + theme(legend_position="none"))
239
+ fig = ((a | b) + plot_layout(widths=[3, 2])
240
+ + plot_annotation(title="Response by dose and arm", tag_levels="A"))
236
241
  ```
237
242
 
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">
243
+ `scale_colour_hue()` and `theme_grey()` are ggplot2's own defaults, so the
244
+ figure looks as it would from R. plot3's defaults are `theme_bw()` for saved
245
+ files and `theme_dark()` in the viewer.
246
+
247
+ <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
248
 
240
249
  Everything else you'd expect is there, including histograms, densities,
241
250
  boxplots, bar positions, `stat_summary`, error bars, heatmaps, hexbins, 2D
@@ -1,7 +1,7 @@
1
1
  plot3/__init__.py,sha256=-jbV46XmzHZJzv7jrfi50on5Xxs1PJ8U3aV7bbSm1VA,6016
2
- plot3/__version__.py,sha256=cID1jLnC_vj48GgMN6Yb1FA3JsQ95zNmCHmRYE8TFhY,22
2
+ plot3/__version__.py,sha256=jFlbxEJFS0G44LE-yXXVSwXACA1J_NyYDk5E20_2zpc,22
3
3
  plot3/aesexpr.py,sha256=5GbKRX0FZFpDXCDzsZzZW32MKK3uI24Y9nabNJO5RRc,10449
4
- plot3/build.py,sha256=EuIqW3tg0tH2Gbr904eJtY_ejuwhcR0QpD54axpZiEc,168960
4
+ plot3/build.py,sha256=cEiC9x34uplWWVCuNyAX-vpKbGiF3RIHhCyLHqqJ6EI,171107
5
5
  plot3/calculus.py,sha256=yBnZYVHKAehNIdZjYj7fWHadOTQBgSx5Ys8tNQaw11s,45442
6
6
  plot3/compose.py,sha256=BICOe1QMnI17k83YqJe8NBKs6lWnjuDox71Sv-erkM0,11513
7
7
  plot3/contour.py,sha256=YUrN7rtjjXR8if9L70EMJ6TswWbg9pY6-JSAqADAQEo,20401
@@ -23,13 +23,13 @@ plot3/scales.py,sha256=rBZbM7Z9YcJNjP7Vr3WwAukF2cDqAyGXWPx3Mo8Nko8,14197
23
23
  plot3/scaling.py,sha256=3z-m5sGh0BQcQQRIwlsX6tVaGGyMIF2DVcIEGHmY00Y,28316
24
24
  plot3/special.py,sha256=ARUav8LGcVe_cPbHoLy73O2U_2wuxUVjvoSwVYfm1-Y,12584
25
25
  plot3/stat2d.py,sha256=4a4C3HxBEOeRuMCQXHgLClxFIkPT2-Fz6DJZDev5kdg,66429
26
- plot3/static.py,sha256=0rcgEb52somhdcZNKL0cUyC04n6IFnVxJ3l2RroEGF4,154271
26
+ plot3/static.py,sha256=erJR9Hsb_pv_WYYG7xRE-moNbMhON1ce1pu3Rajzv-E,154546
27
27
  plot3/stats3d.py,sha256=B6WCKen7eAHSMlkeo3tnfUGcA1tpdH4OVKOl_5vkbmo,15714
28
28
  plot3/table.py,sha256=v4G6NgbEPKv6RakSHLnvfdkWNAaApEgel9be0PzPUMA,26033
29
- plot3/themes.py,sha256=mK9zWm4nRLSDbH2xDJqdrRnnkb9bGUWByyI-x9ZupdU,5145
29
+ plot3/themes.py,sha256=pQnsBApRRlwQxXPrBFJVYuo6kYan8bgdT3Nlm0T9XFg,5179
30
30
  plot3/viewer.py,sha256=tKhBkO7IJpDVs2_NdM87F1O2EVtuAGst1P7bI8gtr_A,141046
31
- plot3-0.6.0.dist-info/licenses/LICENSE,sha256=OPphTrXUbMt4GNrsK8juVty13iThKFPIPnu_nh4UlAk,1081
32
- plot3-0.6.0.dist-info/METADATA,sha256=2vsBMW0gt-otu5kR9V3eil-7pH4NreG3n7cvlpRIMsM,13806
33
- plot3-0.6.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
34
- plot3-0.6.0.dist-info/top_level.txt,sha256=wFHSJTzHoaJCrffpONuI8hayEblnTzRMi2y3qCcKzD0,6
35
- plot3-0.6.0.dist-info/RECORD,,
31
+ plot3-0.6.2.dist-info/licenses/LICENSE,sha256=OPphTrXUbMt4GNrsK8juVty13iThKFPIPnu_nh4UlAk,1081
32
+ plot3-0.6.2.dist-info/METADATA,sha256=RFUINTip5JKVd8IWARh6jP0av8LFt8aSvDhq9dBqcN0,14318
33
+ plot3-0.6.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
34
+ plot3-0.6.2.dist-info/top_level.txt,sha256=wFHSJTzHoaJCrffpONuI8hayEblnTzRMi2y3qCcKzD0,6
35
+ plot3-0.6.2.dist-info/RECORD,,
File without changes