plot3 0.6.3__tar.gz → 0.6.5__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.3 → plot3-0.6.5}/PKG-INFO +5 -1
  2. {plot3-0.6.3 → plot3-0.6.5}/README.md +4 -0
  3. plot3-0.6.5/plot3/__version__.py +1 -0
  4. {plot3-0.6.3 → plot3-0.6.5}/plot3/viewer.py +122 -15
  5. {plot3-0.6.3 → plot3-0.6.5}/plot3.egg-info/PKG-INFO +5 -1
  6. plot3-0.6.3/plot3/__version__.py +0 -1
  7. {plot3-0.6.3 → plot3-0.6.5}/LICENSE +0 -0
  8. {plot3-0.6.3 → plot3-0.6.5}/plot3/__init__.py +0 -0
  9. {plot3-0.6.3 → plot3-0.6.5}/plot3/aesexpr.py +0 -0
  10. {plot3-0.6.3 → plot3-0.6.5}/plot3/build.py +0 -0
  11. {plot3-0.6.3 → plot3-0.6.5}/plot3/calculus.py +0 -0
  12. {plot3-0.6.3 → plot3-0.6.5}/plot3/compose.py +0 -0
  13. {plot3-0.6.3 → plot3-0.6.5}/plot3/contour.py +0 -0
  14. {plot3-0.6.3 → plot3-0.6.5}/plot3/craft.py +0 -0
  15. {plot3-0.6.3 → plot3-0.6.5}/plot3/encode.py +0 -0
  16. {plot3-0.6.3 → plot3-0.6.5}/plot3/expr.py +0 -0
  17. {plot3-0.6.3 → plot3-0.6.5}/plot3/flip.py +0 -0
  18. {plot3-0.6.3 → plot3-0.6.5}/plot3/function.py +0 -0
  19. {plot3-0.6.3 → plot3-0.6.5}/plot3/geoms.py +0 -0
  20. {plot3-0.6.3 → plot3-0.6.5}/plot3/ggplot.py +0 -0
  21. {plot3-0.6.3 → plot3-0.6.5}/plot3/io.py +0 -0
  22. {plot3-0.6.3 → plot3-0.6.5}/plot3/jupyter.py +0 -0
  23. {plot3-0.6.3 → plot3-0.6.5}/plot3/latexin.py +0 -0
  24. {plot3-0.6.3 → plot3-0.6.5}/plot3/masking.py +0 -0
  25. {plot3-0.6.3 → plot3-0.6.5}/plot3/mathtext.py +0 -0
  26. {plot3-0.6.3 → plot3-0.6.5}/plot3/payload.py +0 -0
  27. {plot3-0.6.3 → plot3-0.6.5}/plot3/remote.py +0 -0
  28. {plot3-0.6.3 → plot3-0.6.5}/plot3/scales.py +0 -0
  29. {plot3-0.6.3 → plot3-0.6.5}/plot3/scaling.py +0 -0
  30. {plot3-0.6.3 → plot3-0.6.5}/plot3/special.py +0 -0
  31. {plot3-0.6.3 → plot3-0.6.5}/plot3/stat2d.py +0 -0
  32. {plot3-0.6.3 → plot3-0.6.5}/plot3/static.py +0 -0
  33. {plot3-0.6.3 → plot3-0.6.5}/plot3/stats3d.py +0 -0
  34. {plot3-0.6.3 → plot3-0.6.5}/plot3/table.py +0 -0
  35. {plot3-0.6.3 → plot3-0.6.5}/plot3/themes.py +0 -0
  36. {plot3-0.6.3 → plot3-0.6.5}/plot3.egg-info/SOURCES.txt +0 -0
  37. {plot3-0.6.3 → plot3-0.6.5}/plot3.egg-info/dependency_links.txt +0 -0
  38. {plot3-0.6.3 → plot3-0.6.5}/plot3.egg-info/requires.txt +0 -0
  39. {plot3-0.6.3 → plot3-0.6.5}/plot3.egg-info/top_level.txt +0 -0
  40. {plot3-0.6.3 → plot3-0.6.5}/pyproject.toml +0 -0
  41. {plot3-0.6.3 → plot3-0.6.5}/setup.cfg +0 -0
  42. {plot3-0.6.3 → plot3-0.6.5}/tests/test_2d_density.py +0 -0
  43. {plot3-0.6.3 → plot3-0.6.5}/tests/test_3d.py +0 -0
  44. {plot3-0.6.3 → plot3-0.6.5}/tests/test_animation.py +0 -0
  45. {plot3-0.6.3 → plot3-0.6.5}/tests/test_annotate.py +0 -0
  46. {plot3-0.6.3 → plot3-0.6.5}/tests/test_api_public.py +0 -0
  47. {plot3-0.6.3 → plot3-0.6.5}/tests/test_array_backend.py +0 -0
  48. {plot3-0.6.3 → plot3-0.6.5}/tests/test_arrow.py +0 -0
  49. {plot3-0.6.3 → plot3-0.6.5}/tests/test_build_contract.py +0 -0
  50. {plot3-0.6.3 → plot3-0.6.5}/tests/test_contour.py +0 -0
  51. {plot3-0.6.3 → plot3-0.6.5}/tests/test_everyday_ggplot2.py +0 -0
  52. {plot3-0.6.3 → plot3-0.6.5}/tests/test_expr.py +0 -0
  53. {plot3-0.6.3 → plot3-0.6.5}/tests/test_facets_compose.py +0 -0
  54. {plot3-0.6.3 → plot3-0.6.5}/tests/test_geom_function.py +0 -0
  55. {plot3-0.6.3 → plot3-0.6.5}/tests/test_geoms_ds.py +0 -0
  56. {plot3-0.6.3 → plot3-0.6.5}/tests/test_ggplot2_extras2.py +0 -0
  57. {plot3-0.6.3 → plot3-0.6.5}/tests/test_ggplot2_parity.py +0 -0
  58. {plot3-0.6.3 → plot3-0.6.5}/tests/test_ggplot_extras.py +0 -0
  59. {plot3-0.6.3 → plot3-0.6.5}/tests/test_grammar.py +0 -0
  60. {plot3-0.6.3 → plot3-0.6.5}/tests/test_latex_input.py +0 -0
  61. {plot3-0.6.3 → plot3-0.6.5}/tests/test_lidar_scene.py +0 -0
  62. {plot3-0.6.3 → plot3-0.6.5}/tests/test_masking.py +0 -0
  63. {plot3-0.6.3 → plot3-0.6.5}/tests/test_math_layers.py +0 -0
  64. {plot3-0.6.3 → plot3-0.6.5}/tests/test_mathtext.py +0 -0
  65. {plot3-0.6.3 → plot3-0.6.5}/tests/test_payload.py +0 -0
  66. {plot3-0.6.3 → plot3-0.6.5}/tests/test_payload_display.py +0 -0
  67. {plot3-0.6.3 → plot3-0.6.5}/tests/test_point_cloud.py +0 -0
  68. {plot3-0.6.3 → plot3-0.6.5}/tests/test_r_oracle_parity.py +0 -0
  69. {plot3-0.6.3 → plot3-0.6.5}/tests/test_readme.py +0 -0
  70. {plot3-0.6.3 → plot3-0.6.5}/tests/test_remote_bridge.py +0 -0
  71. {plot3-0.6.3 → plot3-0.6.5}/tests/test_review_fixes.py +0 -0
  72. {plot3-0.6.3 → plot3-0.6.5}/tests/test_save_menu.py +0 -0
  73. {plot3-0.6.3 → plot3-0.6.5}/tests/test_scales_api.py +0 -0
  74. {plot3-0.6.3 → plot3-0.6.5}/tests/test_scales_themes.py +0 -0
  75. {plot3-0.6.3 → plot3-0.6.5}/tests/test_special.py +0 -0
  76. {plot3-0.6.3 → plot3-0.6.5}/tests/test_stat2d.py +0 -0
  77. {plot3-0.6.3 → plot3-0.6.5}/tests/test_static_save.py +0 -0
  78. {plot3-0.6.3 → plot3-0.6.5}/tests/test_stats_semantic.py +0 -0
  79. {plot3-0.6.3 → plot3-0.6.5}/tests/test_step3_geoms.py +0 -0
  80. {plot3-0.6.3 → plot3-0.6.5}/tests/test_table_backends.py +0 -0
  81. {plot3-0.6.3 → plot3-0.6.5}/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.3
3
+ Version: 0.6.5
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
@@ -130,6 +130,10 @@ gapminder = pd.read_csv("https://raw.githubusercontent.com/kirenz/datasets/maste
130
130
  `{frame_time}` in the title shows the current year as it plays.
131
131
  `transition_states("phase")` steps through categories instead of time.
132
132
 
133
+ Click a bubble to trace it: its path through every frame appears, with
134
+ the years along it and its name (from `group=`) riding beside it as it
135
+ plays. Click it again, or empty space, to clear it. This works in 3D too.
136
+
133
137
  <details>
134
138
  <summary>Run this without downloading anything (made-up gapminder-shaped data)</summary>
135
139
 
@@ -103,6 +103,10 @@ gapminder = pd.read_csv("https://raw.githubusercontent.com/kirenz/datasets/maste
103
103
  `{frame_time}` in the title shows the current year as it plays.
104
104
  `transition_states("phase")` steps through categories instead of time.
105
105
 
106
+ Click a bubble to trace it: its path through every frame appears, with
107
+ the years along it and its name (from `group=`) riding beside it as it
108
+ plays. Click it again, or empty space, to clear it. This works in 3D too.
109
+
106
110
  <details>
107
111
  <summary>Run this without downloading anything (made-up gapminder-shaped data)</summary>
108
112
 
@@ -0,0 +1 @@
1
+ __version__ = "0.6.5"
@@ -55,6 +55,9 @@ body{display:flex;flex-direction:column}
55
55
  #legend .sz-block{clear:both;padding-top:6px}
56
56
  #legend .bub{display:inline-block;border-radius:50%;box-sizing:border-box;flex:none;
57
57
  border:1px solid rgba(0,0,0,.35)}
58
+ #legend.docked{display:flex!important;flex-wrap:wrap;align-items:center;column-gap:14px;row-gap:0;line-height:1.5}
59
+ #legend.docked .sz-block{display:flex;flex-wrap:wrap;align-items:center;column-gap:10px;padding-top:0;clear:none}
60
+ #legend.docked .sz{margin:0}
58
61
  #player{display:none;flex:none;align-items:center;flex-wrap:wrap;gap:8px;padding:4px 10px 8px;max-width:100%;box-sizing:border-box}
59
62
  #player button{font:inherit;padding:3px 10px;border-radius:5px;cursor:pointer}
60
63
  #player #play-range{flex:1;min-width:80px}
@@ -345,19 +348,25 @@ function showLegendBox() {
345
348
  legEl.style.border = '1px solid ' + T.grid;
346
349
  legEl.style.color = T.ink2;
347
350
  }
351
+ // Height the docked legend takes at the bottom; the plot shrinks by it.
352
+ let legendReserve = 0;
348
353
  function placeLegend() {
349
- if (!legEl || legEl.style.display === 'none' || legEl.dataset.docked === '1') return;
354
+ legendReserve = 0;
355
+ if (!legEl || legEl.style.display === 'none') return;
350
356
  const figH = figEl.clientHeight || 0;
351
357
  if (figH < 40) return;
352
- // A key taller than the panel covers the curve. Sit it under the axes.
353
- if (legEl.offsetHeight > figH * 0.40) {
358
+ // A key taller than the panel covers the data (a short notebook output).
359
+ // Lay it out in rows under the axes, and keep that room free.
360
+ if (legEl.dataset.docked !== '1' && legEl.offsetHeight > figH * 0.40) {
354
361
  legEl.dataset.docked = '1';
362
+ legEl.classList.add('docked');
355
363
  legEl.style.top = 'auto';
356
- legEl.style.bottom = '8px';
364
+ legEl.style.bottom = '6px';
357
365
  legEl.style.left = '8px';
358
366
  legEl.style.right = '8px';
359
367
  legEl.style.maxWidth = 'none';
360
368
  }
369
+ if (legEl.dataset.docked === '1') legendReserve = legEl.offsetHeight + 8;
361
370
  }
362
371
  const LEGEND_GLYPH = { circle: '●', triangle: '▲', square: '■', diamond: '◆', plus: '+', cross: '×' };
363
372
  // Legend keys: a square, the point's symbol, or a short (dashed) line.
@@ -693,6 +702,26 @@ const stepEntries = [];
693
702
  let hoverHold = false;
694
703
  let trail = null;
695
704
  let trailId = null;
705
+ // The clicked bubble's path: points with their frame, its name and colour.
706
+ let trailInfo = null;
707
+ // A click toggles a bubble's path only on the bubble itself (a little
708
+ // slack for tiny dots), so a stray click near one draws nothing.
709
+ function clickRadius(L, i, minimum) {
710
+ return L._size ? Math.max(minimum, L._size[i] * 0.55) : minimum;
711
+ }
712
+ function trailKey(L, i) { return String(i) + '|' + (L.ids ? L.ids[i] : ''); }
713
+ function trailHint(L, i) {
714
+ return '<br><span style="opacity:.6;font-size:10px">click to ' +
715
+ (trailId === trailKey(L, i) ? 'hide' : 'show') + ' its path</span>';
716
+ }
717
+ // The frame's value (the year) for a point on the path, or '' if unknown.
718
+ function trailYear(f) {
719
+ const tr = S.transition;
720
+ if (!tr || !tr.times || !trailInfo || tr.times.length !== trailInfo.nF) return '';
721
+ const v = tr.times[f];
722
+ if (tr.integer) return String(Math.round(+v));
723
+ return typeof v === 'number' ? fmt(v) : String(v);
724
+ }
696
725
  function framePresent(F, i) {
697
726
  if (!F.x || !F.x.maskData || !F.x.maskData[i]) return false;
698
727
  if (!F.y || !F.y.maskData || !F.y.maskData[i]) return false;
@@ -1158,6 +1187,7 @@ function clearTrail() {
1158
1187
  trail = null;
1159
1188
  }
1160
1189
  trailId = null;
1190
+ trailInfo = null;
1161
1191
  }
1162
1192
  function setTrail(L, obj) {
1163
1193
  clearTrail();
@@ -1199,7 +1229,18 @@ function setTrail(L, obj) {
1199
1229
  }
1200
1230
  scene.add(group);
1201
1231
  trail = group;
1202
- trailId = String(obj) + '|' + (L.ids ? L.ids[obj] : '');
1232
+ trailId = trailKey(L, obj);
1233
+ const pts = [];
1234
+ for (let f = 0; f < nF; f++) {
1235
+ const i = obj * nF + f;
1236
+ if (!framePresent(F, i)) continue;
1237
+ pts.push({ x: F.x.data[i], y: F.y.data[i], z: F.z ? F.z.data[i] : 0, f: f });
1238
+ }
1239
+ trailInfo = {
1240
+ L: L, obj: obj, nF: nF, ext: ext, pts: pts,
1241
+ name: L.ids && L.ids[obj] != null ? String(L.ids[obj]) : '',
1242
+ color: 'rgb(' + rgb.map(c => Math.round(c * 255)).join(',') + ')',
1243
+ };
1203
1244
  }
1204
1245
  let frameLabel = '';
1205
1246
  const axisTitleSprites = [];
@@ -2029,14 +2070,52 @@ if (!S.is3d) {
2029
2070
  const AY = Math.min(Math.max(Y, M.t + 12), M.t + H - 10);
2030
2071
  s += '<text x="' + AX + '" y="' + AY + '" fill="' + T.ink + '" text-anchor="middle" dominant-baseline="middle" font-size="12" font-weight="600" paint-order="stroke" stroke="' + T.surface + '" stroke-width="4" stroke-linejoin="round">' + plot3Esc(ann.text) + '</text>';
2031
2072
  }
2073
+ if (trailInfo) s += trailLabels2D();
2032
2074
  svg.innerHTML = s;
2033
2075
  if (gridSvg) gridSvg.innerHTML = grid;
2034
2076
  }
2035
2077
 
2078
+ // The clicked bubble's name rides beside it; years mark its path,
2079
+ // spaced so they do not overlap (the first and last always show).
2080
+ function trailLabels2D() {
2081
+ const t = trailInfo, ext = t.ext;
2082
+ const inside = (X, Y) => X >= M.l && X <= M.l + W && Y >= M.t && Y <= M.t + H;
2083
+ const halo = ' paint-order="stroke" stroke="' + T.surface + '" stroke-width="3" stroke-linejoin="round"';
2084
+ // The name sits right of the bubble's current position; years keep clear.
2085
+ const L = t.L, i = t.obj;
2086
+ let nameBox = null, nameSvg = '';
2087
+ if (t.name && L._x && L._y) {
2088
+ const X = M.l + screenX(L._x[i] * ext[0]), Y = M.t + screenY(L._y[i] * ext[1]);
2089
+ if (inside(X, Y)) {
2090
+ const x0 = X + (L._size ? L._size[i] * 0.5 : 4) + 4;
2091
+ nameBox = [x0 - 4, Y - 9, x0 + 7.2 * t.name.length + 4, Y + 9];
2092
+ nameSvg = '<text x="' + x0 + '" y="' + Y + '" fill="' + t.color +
2093
+ '" font-size="12" font-weight="700" dominant-baseline="middle"' + halo + '>' +
2094
+ plot3Esc(t.name) + '</text>';
2095
+ }
2096
+ }
2097
+ const underName = (X, Y) => nameBox && X + 5 + 26 > nameBox[0] && X + 5 < nameBox[2] &&
2098
+ Y - 5 > nameBox[1] - 6 && Y - 5 - 10 < nameBox[3];
2099
+ const kept = [];
2100
+ t.pts.forEach((p, k) => {
2101
+ const X = M.l + screenX(p.x * ext[0]), Y = M.t + screenY(p.y * ext[1]);
2102
+ const label = trailYear(p.f);
2103
+ if (!label || !inside(X, Y) || underName(X, Y)) return;
2104
+ const prev = kept[kept.length - 1];
2105
+ const near = prev && Math.hypot(X - prev.X, Y - prev.Y) < 34;
2106
+ if (near && k !== t.pts.length - 1) return;
2107
+ if (near && kept.length > 1) kept.pop(); // the last year wins a crowded end
2108
+ else if (near) return;
2109
+ kept.push({ X, Y, label });
2110
+ });
2111
+ return kept.map(k => '<text x="' + (k.X + 5) + '" y="' + (k.Y - 5) + '" fill="' + T.muted +
2112
+ '" font-size="10"' + halo + '>' + plot3Esc(k.label) + '</text>').join('') + nameSvg;
2113
+ }
2114
+
2036
2115
  function layout() {
2037
2116
  placeLegend();
2038
2117
  W = Math.max(50, figEl.clientWidth - M.l - M.r);
2039
- H = Math.max(50, figEl.clientHeight - M.t - M.b);
2118
+ H = Math.max(50, figEl.clientHeight - M.t - M.b - legendReserve);
2040
2119
  if (equalAspect) applyEqual();
2041
2120
  host.style.left = M.l + 'px'; host.style.top = M.t + 'px';
2042
2121
  renderer.setSize(W, H);
@@ -2274,7 +2353,9 @@ if (!S.is3d) {
2274
2353
  : head
2275
2354
  + fmtSpan('x', xv, xSpan) + ', '
2276
2355
  + fmtSpan('y', yv, ySpan) + sizeBit;
2277
- consider(d, sx, sy, html, !!(L.kind === 'point' && L.frames && S.transition));
2356
+ const moving = L.kind === 'point' && L.frames && S.transition;
2357
+ const cr = clickRadius(L, i, 6);
2358
+ consider(d, sx, sy, html + (moving && d <= cr * cr ? trailHint(L, i) : ''), !!moving);
2278
2359
  }
2279
2360
  }
2280
2361
  }
@@ -2302,14 +2383,14 @@ if (!S.is3d) {
2302
2383
  for (let i = 0; i < L.n; i++) {
2303
2384
  if (L._alpha && L._alpha[i] <= 0.04) continue;
2304
2385
  const sx = screenX(L._x[i]), sy = screenY(L._y[i]);
2305
- const rad = L._size ? Math.max(10, L._size[i] * 0.55) : 12;
2386
+ const rad = clickRadius(L, i, 6);
2306
2387
  const d = (sx - mx) * (sx - mx) + (sy - my) * (sy - my);
2307
2388
  if (d <= rad * rad && d < hitD) { hitD = d; hit = { L, i }; }
2308
2389
  }
2309
2390
  }
2391
+ tip.style.display = 'none';
2310
2392
  if (!hit) { clearTrail(); draw(); return; }
2311
- const key = String(hit.i) + '|' + (hit.L.ids ? hit.L.ids[hit.i] : '');
2312
- if (trailId === key) { clearTrail(); draw(); return; }
2393
+ if (trailId === trailKey(hit.L, hit.i)) { clearTrail(); draw(); return; }
2313
2394
  setTrail(hit.L, hit.i);
2314
2395
  draw();
2315
2396
  });
@@ -2651,6 +2732,7 @@ if (!S.is3d) {
2651
2732
  }
2652
2733
  }
2653
2734
  hoverHold = !!(best && best[0].kind === 'point' && best[0].frames && S.transition);
2735
+ const hoverD = bestD;
2654
2736
  if (!best) { tip3.style.display = 'none'; return; }
2655
2737
  const [L, i, sx, sy, xn, yn, zn] = best;
2656
2738
  const xv = fromScale('x', xn);
@@ -2678,6 +2760,10 @@ if (!S.is3d) {
2678
2760
  + axisPair('y', fmt(yv)) + ', '
2679
2761
  + axisPair('z', fmt(zv)) + sizeBit
2680
2762
  : head + fmt(xv) + ', ' + fmt(yv) + ', ' + fmt(zv) + sizeBit;
2763
+ if (hoverHold) {
2764
+ const cr = sizeAtten ? 8 : clickRadius(L, i, 8);
2765
+ if (hoverD <= cr * cr) tip3.innerHTML += trailHint(L, i);
2766
+ }
2681
2767
  plot3Typeset(tip3);
2682
2768
  tip3.style.left = Math.min(w - 8, sx + 12) + 'px';
2683
2769
  tip3.style.top = Math.max(8, sy - 10) + 'px';
@@ -2728,20 +2814,41 @@ if (!S.is3d) {
2728
2814
  v.project(cam);
2729
2815
  const sx = (v.x * 0.5 + 0.5) * w;
2730
2816
  const sy = (-v.y * 0.5 + 0.5) * h;
2731
- const rad = (L._size && !sizeAtten) ? Math.max(16, L._size[i] * 0.55) : 16;
2817
+ const rad = sizeAtten ? 8 : clickRadius(L, i, 8);
2732
2818
  const d = (sx - mx) * (sx - mx) + (sy - my) * (sy - my);
2733
2819
  if (d <= rad * rad && d < hitD) { hitD = d; hit = { L, i }; }
2734
2820
  }
2735
2821
  }
2822
+ tip3.style.display = 'none';
2736
2823
  if (!hit) { clearTrail(); return; }
2737
- const key = String(hit.i) + '|' + (hit.L.ids ? hit.L.ids[hit.i] : '');
2738
- if (trailId === key) { clearTrail(); return; }
2824
+ if (trailId === trailKey(hit.L, hit.i)) { clearTrail(); return; }
2739
2825
  setTrail(hit.L, hit.i);
2826
+ // The scene cannot move text every frame cheaply: label the path's
2827
+ // ends, the first year, and the name with the last year.
2828
+ if (trail && trailInfo && trailInfo.pts.length) {
2829
+ const t = trailInfo;
2830
+ const at = p => new THREE.Vector3(p.x * t.ext[0], p.y * t.ext[1], p.z * t.ext[2]);
2831
+ const first = t.pts[0], last = t.pts[t.pts.length - 1];
2832
+ const startText = trailYear(first.f);
2833
+ if (startText) {
2834
+ const sp = sprite(startText, true);
2835
+ sp.center.set(-0.1, -0.2);
2836
+ sp.position.copy(at(first));
2837
+ trail.add(sp);
2838
+ }
2839
+ const endText = [t.name, trailYear(last.f)].filter(Boolean).join(' ');
2840
+ if (endText) {
2841
+ const sp = sprite(endText, false);
2842
+ sp.center.set(-0.1, -0.2);
2843
+ sp.position.copy(at(last));
2844
+ trail.add(sp);
2845
+ }
2846
+ }
2740
2847
  });
2741
2848
 
2742
2849
  function layout() {
2743
2850
  placeLegend();
2744
- const w = Math.max(figEl.clientWidth, 1), h = Math.max(figEl.clientHeight, 1);
2851
+ const w = Math.max(figEl.clientWidth, 1), h = Math.max(figEl.clientHeight - legendReserve, 1);
2745
2852
  renderer.setSize(w, h);
2746
2853
  cam.aspect = w / h;
2747
2854
  cam.updateProjectionMatrix();
@@ -2763,7 +2870,7 @@ if (!S.is3d) {
2763
2870
  const touches = (a, b, air) => a[0] - air < b[0] + b[2] && b[0] - air < a[0] + a[2] &&
2764
2871
  a[1] - air < b[1] + b[3] && b[1] - air < a[1] + a[3];
2765
2872
  function declutterTicks() {
2766
- const w = Math.max(figEl.clientWidth, 1), h = Math.max(figEl.clientHeight, 1);
2873
+ const w = Math.max(figEl.clientWidth, 1), h = Math.max(figEl.clientHeight - legendReserve, 1);
2767
2874
  const placed = [];
2768
2875
  for (const marks of tickSprites) {
2769
2876
  if (!marks.length) continue;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plot3
3
- Version: 0.6.3
3
+ Version: 0.6.5
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
@@ -130,6 +130,10 @@ gapminder = pd.read_csv("https://raw.githubusercontent.com/kirenz/datasets/maste
130
130
  `{frame_time}` in the title shows the current year as it plays.
131
131
  `transition_states("phase")` steps through categories instead of time.
132
132
 
133
+ Click a bubble to trace it: its path through every frame appears, with
134
+ the years along it and its name (from `group=`) riding beside it as it
135
+ plays. Click it again, or empty space, to clear it. This works in 3D too.
136
+
133
137
  <details>
134
138
  <summary>Run this without downloading anything (made-up gapminder-shaped data)</summary>
135
139
 
@@ -1 +0,0 @@
1
- __version__ = "0.6.3"
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes