plotto 0.4.0 → 0.4.1

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.
package/README.md CHANGED
@@ -197,7 +197,8 @@ One-time setup:
197
197
  Each release (`main` takes changes only through a reviewed pull request):
198
198
 
199
199
  ```bash
200
- # 1. Merge your changes into main through a pull request
200
+ # 1. Merge your changes into main through a pull request,
201
+ # with a line for each change under "## Unreleased" in CHANGELOG.md
201
202
 
202
203
  # 2. Prepare the release
203
204
  git switch main && git pull && source .venv/bin/activate
@@ -211,7 +212,7 @@ scripts/release.sh tag # answer y
211
212
  npm view plotto version && pip index versions python-plotto
212
213
  ```
213
214
 
214
- - `scripts/release.sh X.Y.Z` checks that `main` is current and the version is new. It runs every test and lint. Then, on the branch `release/vX.Y.Z`, it sets the version in `package.json`, `package-lock.json` and `python/plotto/__init__.py`, builds `docs/`, commits and pushes.
215
+ - `scripts/release.sh X.Y.Z` checks that `main` is current, that the version is new and that `CHANGELOG.md` has changes under "Unreleased". It runs every test and lint. Then, on the branch `release/vX.Y.Z`, it sets the version in `package.json`, `package-lock.json` and `python/plotto/__init__.py`, moves the "Unreleased" changes to a dated `X.Y.Z` section, builds `docs/`, commits and pushes.
215
216
  - `scripts/release.sh tag` tags the merged `main` as `vX.Y.Z`, asks before it pushes the tag, and deletes the release branch.
216
217
  - The pushed tag runs `.github/workflows/release.yml`, which publishes to PyPI and npm. Follow it in the Actions tab.
217
218
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plotto",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Grammar-of-graphics plots for notebooks, web pages and slides with one renderer.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://theoradusz.github.io/plotto/",
package/src/figure.js CHANGED
@@ -16,6 +16,8 @@ import { layerEdges, makeView } from "./views.js";
16
16
  const NARROW = 560; // px: below this width, a right legend moves to the bottom
17
17
  const MIN_HEIGHT = 240; // px: the smallest height of a reflowed figure
18
18
  const ASPECT_GROWTH = 1.2; // a theme aspect makes a reflowed figure at most this much taller
19
+ const RIGHT_SHARE = 0.25; // a format with "shrink" keeps the right column (legend, info card) to this share of the width
20
+ const SHRINK_STEP = 0.05; // and makes their text smaller by this share of its size at each step
19
21
 
20
22
  // options: document (the browser or jsdom), width (lay out at this width), domains and hidden (keep a view after a resize).
21
23
  export function figure(spec, { document: doc = globalThis.document, width, domains, hidden = new Set() } = {}) {
@@ -25,7 +27,6 @@ export function figure(spec, { document: doc = globalThis.document, width, domai
25
27
  const reflowed = width && theme.reflow && !theme.fixedHeight;
26
28
  let H = reflowed ? Math.max(MIN_HEIGHT, Math.round((W * theme.height) / theme.width)) : theme.height;
27
29
  if (theme.legend === "right" && W < NARROW) theme.legend = "bottom";
28
- const fonts = fontSizes(theme);
29
30
  const grid = facetPanels(ns);
30
31
  const kind = { x: axisKind(ns, "x"), y: axisKind(ns, "y") };
31
32
  const color = colorScale(ns);
@@ -33,19 +34,32 @@ export function figure(spec, { document: doc = globalThis.document, width, domai
33
34
  for (const p of grid.panels) p.views = ns.layers.map((layer, li) => makeView(ctx, layer, p.rows[li], new Set()));
34
35
  const home = homeDomains(ns, grid, kind);
35
36
  const horizontal = theme.legend === "bottom";
37
+ // The legend and the info card fit: a right column of at most RIGHT_SHARE of the width, one legend column,
38
+ // no cut legend label, no cut info key and no "+N more" row. A format with "shrink" (slide) makes their text smaller until they
39
+ // fit, down to that share of the size; the other formats keep the size.
40
+ const fits = (info, legend) => {
41
+ const right = Math.max(legend && !legend.horizontal ? legend.width : 0, info?.position === "right" ? info.width : 0);
42
+ const cut = legend?.items?.some((it) => it.lines.at(-1).endsWith("…") || (!legend.horizontal && it.x > 0));
43
+ return right <= W * RIGHT_SHARE && !cut && !info?.cut;
44
+ };
36
45
  const arrange = (H) => {
37
- const info = infoModel(ns, fonts, { W, H, narrow: W < NARROW });
38
- const above = info?.position === "right" ? info.height + fonts.base : 0; // the info card sits above a right legend
39
- const legend = legendModel(ns, color, fonts, { horizontal, maxWidth: W * 0.9, maxHeight: H * 0.8 - above });
40
- return { info, legend, layout: place({ ns, grid, kind, home, legend, info, fonts, W, H }) };
46
+ const base = fontSizes(theme);
47
+ for (let k = 1; ; k -= SHRINK_STEP) {
48
+ const f = { ...base, legend: Math.round(base.legend * k * 10) / 10 };
49
+ const info = infoModel(ns, f, { W, H, narrow: W < NARROW });
50
+ const above = info?.position === "right" ? info.height + f.base : 0; // the info card sits above a right legend
51
+ const legend = legendModel(ns, color, f, { horizontal, maxWidth: W * 0.9, maxHeight: H * 0.8 - above });
52
+ if (fits(info, legend) || k - SHRINK_STEP < (theme.shrink ?? 1) - 1e-9) return { fonts: f, info, legend, layout: place({ ns, grid, kind, home, legend, info, fonts: f, W, H }) };
53
+ }
41
54
  };
42
- let { info, legend, layout } = arrange(H);
55
+ let fonts, info, legend, layout;
56
+ ({ fonts, info, legend, layout } = arrange(H));
43
57
  // aspect: when the height follows the width, it changes so that the panels get their shape, up to 1.2 times the
44
58
  // usual height; beyond that, the panels get narrower.
45
59
  if (theme.aspect && theme.fitHeight) {
46
60
  const fit = H + grid.nrow * (layout.room.w * theme.aspect - layout.room.h);
47
61
  H = Math.round(Math.min(H * ASPECT_GROWTH, Math.max(MIN_HEIGHT, fit)));
48
- ({ info, legend, layout } = arrange(H));
62
+ ({ fonts, info, legend, layout } = arrange(H));
49
63
  }
50
64
 
51
65
  const svg = svgElement(doc, "svg", {
package/src/info.js CHANGED
@@ -62,7 +62,9 @@ function model(ns, fonts, entries, position, W, maxHeight) {
62
62
  kept--;
63
63
  t = table([...all.slice(0, kept), { key: `+${all.length - kept} more`, value: "" }]);
64
64
  }
65
- return { position, size, pad, lineH, keyGap, ...t };
65
+ // cut: the entries that do not show, plus the keys that end with an ellipsis.
66
+ const keys = t.columns.flatMap((c) => c.rows).filter((r) => r.key !== r.full).length;
67
+ return { position, size, pad, lineH, keyGap, cut: all.length - kept + keys, ...t };
66
68
  }
67
69
 
68
70
  // The table as an SVG group at (x, y): keys in the muted color, values in the ink color, on a strip-colored card.
package/theme.json CHANGED
@@ -142,6 +142,7 @@
142
142
  "width": 960,
143
143
  "height": 600,
144
144
  "size": 22,
145
+ "shrink": 0.7,
145
146
  "line": 2.5,
146
147
  "point": 4,
147
148
  "reflow": false