lessPython 0.2.0__py3-none-any.whl → 0.3.1__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.
Files changed (59) hide show
  1. lessPy/ANOVA.py +74 -15
  2. lessPy/Chart.py +431 -88
  3. lessPy/Correlation.py +40 -11
  4. lessPy/Flows.py +3 -3
  5. lessPy/Logit.py +15 -11
  6. lessPy/Regression.py +27 -21
  7. lessPy/X.py +280 -42
  8. lessPy/XY.py +1332 -239
  9. lessPy/__init__.py +12 -2
  10. lessPy/_themes.py +5007 -0
  11. lessPy/bc_items_plotly.py +33 -20
  12. lessPy/bc_plotly.py +27 -10
  13. lessPy/bubble_plotly.py +33 -17
  14. lessPy/corCFA.py +9 -5
  15. lessPy/corProp.py +8 -4
  16. lessPy/corReflect.py +8 -4
  17. lessPy/corReorder.py +8 -4
  18. lessPy/corScree.py +12 -8
  19. lessPy/data/StockPrice.csv +1401 -1386
  20. lessPy/details.py +17 -4
  21. lessPy/dn_plotly.py +74 -54
  22. lessPy/dot_plotly.py +9 -8
  23. lessPy/freq_poly_plotly.py +18 -11
  24. lessPy/getColors.py +247 -20
  25. lessPy/hier_plotly.py +41 -14
  26. lessPy/hs_plotly.py +23 -11
  27. lessPy/pie_plotly.py +1 -1
  28. lessPy/pivot.py +39 -9
  29. lessPy/plotly_utils.py +162 -29
  30. lessPy/plt_add.py +2 -7
  31. lessPy/plt_contour.py +6 -6
  32. lessPy/plt_contour_facet.py +3 -3
  33. lessPy/plt_mat_plotly.py +85 -12
  34. lessPy/plt_plotly.py +34 -18
  35. lessPy/plt_smooth.py +4 -4
  36. lessPy/plt_time.py +10 -3
  37. lessPy/prob_norm.py +7 -3
  38. lessPy/prob_tcut.py +7 -3
  39. lessPy/prob_znorm.py +7 -1
  40. lessPy/profile_plotly.py +5 -4
  41. lessPy/radar_plotly.py +4 -3
  42. lessPy/rcolors.py +461 -0
  43. lessPy/simCImean.py +7 -3
  44. lessPy/simCLT.py +7 -3
  45. lessPy/simFlips.py +7 -3
  46. lessPy/simMeans.py +7 -3
  47. lessPy/stats_out.py +257 -11
  48. lessPy/style.py +167 -0
  49. lessPy/sunflower_plotly.py +141 -0
  50. lessPy/ttest.py +17 -13
  51. lessPy/utils.py +146 -6
  52. lessPy/vbs_plotly.py +39 -27
  53. lessPy/x_console.py +763 -0
  54. {lesspython-0.2.0.dist-info → lesspython-0.3.1.dist-info}/METADATA +13 -2
  55. lesspython-0.3.1.dist-info/RECORD +89 -0
  56. {lesspython-0.2.0.dist-info → lesspython-0.3.1.dist-info}/WHEEL +1 -1
  57. lesspython-0.2.0.dist-info/RECORD +0 -84
  58. {lesspython-0.2.0.dist-info → lesspython-0.3.1.dist-info}/licenses/LICENSE +0 -0
  59. {lesspython-0.2.0.dist-info → lesspython-0.3.1.dist-info}/top_level.txt +0 -0
lessPy/plotly_utils.py CHANGED
@@ -17,6 +17,43 @@ BASE_COLORS = [
17
17
  "#00A3BA", "#D26FAF", "#00A76F", "#BD76CB",
18
18
  ]
19
19
 
20
+
21
+ def theme_palette(theme=None):
22
+ """The palette a color theme gives to groups. R analog: .get_fill()
23
+ (not divergent): the qualitative hues for the default theme, else a
24
+ sequential palette in the theme's hue."""
25
+ from .utils import get_option
26
+ if theme is None:
27
+ theme = get_option("theme", "colors")
28
+ if theme == "colors":
29
+ return "hues"
30
+ for pal, members in (
31
+ ("grays", ("gray", "white")),
32
+ ("blues", ("lightbronze", "dodgerblue", "blue")),
33
+ ("browns", ("gold", "brown", "sienna")),
34
+ ("rusts", ("orange",)),
35
+ ("reds", ("darkred", "red", "rose", "slatered")),
36
+ ("greens", ("darkgreen", "green")),
37
+ ("violets", ("purple",))):
38
+ if theme in members:
39
+ return pal
40
+ return "blues"
41
+
42
+
43
+ def by_colors(n, theme=None):
44
+ """One color per group of a by variable under the current theme,
45
+ as X() and XY() draw them. R analog: .color_range(.get_fill(theme),
46
+ n), two grays set apart as gray60 and gray30."""
47
+ pal = theme_palette(theme)
48
+ if pal == "hues":
49
+ return [BASE_COLORS[i % len(BASE_COLORS)] for i in range(n)]
50
+ if pal == "grays" and n == 2:
51
+ return ["#999999", "#4D4D4D"]
52
+ from .getColors import getColors
53
+ cols = getColors(pal, n=max(1, n), quiet=True)
54
+ return [c[:7] if len(c) == 9 else c for c in cols]
55
+
56
+
20
57
  # minimal named-color table; R's col2rgb() knows all R names, but
21
58
  # plotly accepts CSS names directly so unknowns pass through as-is
22
59
  _NAMED = {
@@ -80,6 +117,10 @@ def to_hex(col):
80
117
  gray = _gray_level(lower)
81
118
  if gray is not None:
82
119
  return gray
120
+ # any of R's named colors, as col2rgb() resolves them
121
+ from .rcolors import R_COLORS
122
+ if lower in R_COLORS:
123
+ return R_COLORS[lower]
83
124
  return c1 # CSS name unknown to us; plotly handles it
84
125
 
85
126
 
@@ -222,11 +263,15 @@ def legend_style(by_name, style_opts, title=None, position=None,
222
263
  leg = dict(
223
264
  title=dict(text=abbrev(text, abbrev_n) or "",
224
265
  font=dict(size=round(
225
- 15 * get_option("lab_size", 1) * mult))),
266
+ 15 * get_option("lab_size", 1) * mult),
267
+ color=as_plotly_color(style_opts["lab_color"]))),
268
+ # the legend reads in the theme's label color, light on the
269
+ # black sub_theme as the axis labels are
226
270
  font=dict(size=round(
227
- 16 * get_option("axis_size", 0.9) * mult)),
228
- bgcolor=to_hex(style_opts["window_fill"]),
229
- bordercolor=to_hex(style_opts["legend_border"]),
271
+ 16 * get_option("axis_size", 0.9) * mult),
272
+ color=as_plotly_color(style_opts["lab_color"])),
273
+ bgcolor=as_plotly_color(style_opts["window_fill"]),
274
+ bordercolor=as_plotly_color(style_opts["legend_border"]),
230
275
  borderwidth=1,
231
276
  )
232
277
  # placement stays plotly's own unless asked for, so the callers
@@ -315,12 +360,27 @@ def axis_format(vals, digits_d=2, axis_fmt="K", prefix=""):
315
360
  return base
316
361
 
317
362
 
363
+ _R_DASH = {"dashed": "dash", "dotted": "dot",
364
+ "dotdash": "dashdot", "longdash": "longdash",
365
+ "twodash": "longdashdot"}
366
+
367
+
368
+ def r_dash(lty):
369
+ """An R line type name as plotly's dash, such as "dotted" to
370
+ "dot"; plotly's own names and None pass through. R analog: lty"""
371
+ return _R_DASH.get(lty, lty) if isinstance(lty, str) else lty
372
+
373
+
318
374
  def grid_style():
319
- """R analog: .grid_style()"""
375
+ """R analog: .grid_style(). A sub_theme sets the grid through
376
+ grid_x_color / grid_y_color (white on the gray panel of "default"),
377
+ which take precedence over the grid color itself"""
378
+ clr = (get_option("grid_y_color") or get_option("grid_x_color")
379
+ or get_option("grid_col", "gray85"))
320
380
  return {
321
- "color": to_hex(get_option("grid_col", "gray85")),
381
+ "color": as_plotly_color(clr),
322
382
  "width": get_option("grid_lwd", 0.5),
323
- "dash": get_option("grid_lty", None),
383
+ "dash": r_dash(get_option("grid_lty", None)),
324
384
  }
325
385
 
326
386
 
@@ -355,27 +415,36 @@ def y_grid(y_vals):
355
415
 
356
416
 
357
417
  def axis_base():
358
- """R analog: axis_base()"""
418
+ """R analog: axis_base(). A sub_theme sets the axes through their
419
+ own settings, so on sub_theme="black" the axis line and its labels
420
+ are light where axis_color stays dark; the labels are text, so they
421
+ take axis_text_color, as lessR's base R axes do"""
422
+ ax_line = (get_option("axis_x_color")
423
+ or get_option("axis_color", "black"))
424
+ ax_text = (get_option("axis_x_text_color")
425
+ or get_option("axis_text_color")
426
+ or get_option("axis_color", "black"))
359
427
  return {
360
428
  "zeroline": False,
361
429
  "showline": True,
362
- "linecolor": to_hex(get_option("axis_color", "black")),
430
+ "linecolor": as_plotly_color(ax_line),
363
431
  "linewidth": get_option("axis_lwd", 1),
364
432
  "ticks": "outside",
365
433
  "ticklen": 4,
434
+ "tickcolor": as_plotly_color(ax_line),
366
435
  "automargin": True,
367
436
  "tickfont": {
368
- "color": to_hex(get_option("axis_color", "black")),
437
+ "color": as_plotly_color(ax_text),
369
438
  "size": 16 * get_option("axis_size", 0.9),
370
439
  },
371
440
  "title": {
372
441
  "font": {
373
- "color": to_hex(get_option("lab_color", "black")),
442
+ "color": as_plotly_color(get_option("lab_color", "black")),
374
443
  "size": 15 * get_option("lab_size", 1),
375
444
  },
376
445
  "standoff": 10,
377
446
  },
378
- "gridcolor": to_hex(get_option("grid_color", "gray90")),
447
+ "gridcolor": as_plotly_color(get_option("grid_color", "gray90")),
379
448
  "gridwidth": get_option("grid_lwd", 1),
380
449
  }
381
450
 
@@ -445,7 +514,7 @@ def add_subtitle(fig, sub):
445
514
  title_size = round(16 * get_option("main_size", 1))
446
515
  size = round(title_size * get_option("sub_size", 0.75) / 0.75
447
516
  * 0.75)
448
- color = to_hex(get_option("sub_color",
517
+ color = as_plotly_color(get_option("sub_color",
449
518
  get_option("main_color",
450
519
  get_option("lab_color",
451
520
  "black"))))
@@ -486,6 +555,10 @@ def font_scaled(func):
486
555
  fig = func(*args, **kwargs)
487
556
  if sub and hasattr(fig, "layout"):
488
557
  add_subtitle(fig, sub)
558
+ # style(font_family=) sets the family of all plotted text
559
+ family = get_option("font_family")
560
+ if family and hasattr(fig, "layout"):
561
+ fig.update_layout(font=dict(family=family))
489
562
  try:
490
563
  if font_size and font_size != 1 and hasattr(fig, "layout"):
491
564
  apply_font_size(fig, font_size)
@@ -496,13 +569,66 @@ def font_scaled(func):
496
569
  return wrapper
497
570
 
498
571
 
499
- def sym_at(shape, i):
500
- """Per-group marker symbol. A scalar shape is used for every
501
- group (by= differentiates by color only, the default); a list/
502
- tuple gives one symbol per group, recycled if short."""
572
+ # R's pch codes 0-25 as plotly symbols; 21-25 are lessR's named
573
+ # shapes. R analog: .plt.shapes(), and points() for the open codes
574
+ _R_PCH = {
575
+ 0: "square-open", 1: "circle-open", 2: "triangle-up-open",
576
+ 3: "cross-thin-open", 4: "x-thin-open", 5: "diamond-open",
577
+ 6: "triangle-down-open", 7: "square-x-open",
578
+ 8: "asterisk-open", 9: "diamond-cross-open",
579
+ 10: "circle-cross-open", 11: "hexagram-open",
580
+ 12: "square-cross-open", 13: "circle-x-open",
581
+ 14: "square-open", 15: "square", 16: "circle",
582
+ 17: "triangle-up", 18: "diamond", 19: "circle", 20: "circle",
583
+ 21: "circle", 22: "square", 23: "diamond", 24: "triangle-up",
584
+ 25: "triangle-down"}
585
+ _LESSR_SHAPES = {"triup": "triangle-up", "tridown": "triangle-down"}
586
+ # pt_shape="vary": one shape per by group, in lessR's order
587
+ _VARY = (["triup", "tridown", "circle", "square", "diamond",
588
+ 8, 7, 9, 10, 12, 13, 14, 11]
589
+ + list("abcdefghijklmnopqrstuvwxyz")
590
+ + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
591
+ + list("+*#%!=-&$?|><@./;:"))
592
+
593
+
594
+ def shape_at(shape, i):
595
+ """Per-group shape as (plotly symbol, None), or (None, char) for
596
+ a single character, drawn as text, as R's pch="$". A scalar
597
+ shape is used for every group (by= differentiates by color
598
+ only, the default); a list gives one shape per group, recycled;
599
+ "vary" a different shape per group. Takes plotly's names,
600
+ lessR's triup/tridown, and R's pch codes 0-25.
601
+ R analog: .plt.shapes()"""
602
+ if isinstance(shape, str) and shape == "vary":
603
+ shape = _VARY
503
604
  if isinstance(shape, (list, tuple)):
504
- return shape[i % len(shape)]
505
- return shape
605
+ shape = shape[i % len(shape)]
606
+ if isinstance(shape, (int, np.integer)) and not isinstance(
607
+ shape, bool):
608
+ if int(shape) not in _R_PCH:
609
+ raise ValueError(
610
+ f"pt_shape={shape}: R's numbered shapes are 0 to 25")
611
+ return _R_PCH[int(shape)], None
612
+ s = str(shape)
613
+ if len(s) == 1:
614
+ return None, s
615
+ return _LESSR_SHAPES.get(s, s), None
616
+
617
+
618
+ def marks(shape, i, mode, marker):
619
+ """go.Scatter keywords that draw the points of group i: the
620
+ marker with its symbol, or for a character shape the
621
+ character as text in the marker's border color, sized to the
622
+ marker. mode is "markers" or "lines+markers"."""
623
+ sym, ch = shape_at(shape, i)
624
+ if ch is None:
625
+ return dict(mode=mode, marker=dict(marker, symbol=sym))
626
+ color = (marker.get("line") or {}).get("color", marker.get("color"))
627
+ return dict(mode=mode.replace("markers", "text"), text=ch,
628
+ textposition="middle center",
629
+ textfont=dict(color=color,
630
+ size=max(8, round(float(np.median(
631
+ marker.get("size", 8))) * 1.4))))
506
632
 
507
633
 
508
634
  def square_layout(main=False, side=460):
@@ -603,7 +729,7 @@ def facet_panels(facet, facet_order, facet2=None,
603
729
 
604
730
  def finish_facet(fig, levels, ax, x_lab, y_lab, gridT1,
605
731
  style_opts=None, y_cat=None, n_col=1, pos=None,
606
- height=None, width=None):
732
+ height=None, width=None, x_cat=None):
607
733
  """Facet styling shared by the faceted renderers: per-panel y
608
734
  axes on a common scale (y label on the middle panel of the
609
735
  first column), x ticks on the bottom row only, vertical grid
@@ -631,8 +757,8 @@ def finish_facet(fig, levels, ax, x_lab, y_lab, gridT1,
631
757
  anns = list(fig.layout.annotations or [])
632
758
  s_fill = as_plotly_color(get_option("strip_fill",
633
759
  "#7F7F7F37"))
634
- s_line = to_hex(get_option("strip_color", "gray40"))
635
- s_text = to_hex(get_option("strip_text_color", "gray15"))
760
+ s_line = as_plotly_color(get_option("strip_color", "gray40"))
761
+ s_text = as_plotly_color(get_option("strip_text_color", "gray15"))
636
762
  strip_h = max(0.10, min(0.30, 0.05 * n_row))
637
763
  # strip font: shrink so the longest label fits its panel
638
764
  # (the two-facet Var = "level" cell labels run long)
@@ -654,7 +780,7 @@ def finish_facet(fig, levels, ax, x_lab, y_lab, gridT1,
654
780
  if y_cat is None:
655
781
  ay = axis_num(show_ylab, ax["axT2"], ax["axL2"])
656
782
  ay.update(showgrid=True,
657
- gridcolor=to_hex(style_opts["grid_col"]),
783
+ gridcolor=as_plotly_color(style_opts["grid_col"]),
658
784
  gridwidth=1, griddash="dot",
659
785
  showticklabels=c == 1,
660
786
  range=[0, float(ax["axT2"][-1]) * 1.04])
@@ -670,7 +796,14 @@ def finish_facet(fig, levels, ax, x_lab, y_lab, gridT1,
670
796
  showticklabels=c == 1)
671
797
  fig.update_yaxes(row=r, col=c, **ay)
672
798
  lowest = r == n_row or (r + 1, c) not in occupied
673
- if lowest:
799
+ if lowest and x_cat is not None:
800
+ # categories along x, the vertical bars of a panel
801
+ ax_x = axis_cat(x_lab if c == mid_c else "")
802
+ ax_x.update(categoryorder="array",
803
+ categoryarray=list(x_cat), tickmode="array",
804
+ tickvals=list(x_cat), ticktext=list(x_cat))
805
+ fig.update_xaxes(row=r, col=c, showticklabels=True, **ax_x)
806
+ elif lowest:
674
807
  # a shared x axis hides the labels above the bottom row,
675
808
  # so a ragged grid's lowest panel turns its own back on
676
809
  fig.update_xaxes(
@@ -680,7 +813,7 @@ def finish_facet(fig, levels, ax, x_lab, y_lab, gridT1,
680
813
  else:
681
814
  fig.update_xaxes(
682
815
  row=r, col=c, showline=True, ticks="",
683
- linecolor=to_hex(get_option("axis_color",
816
+ linecolor=as_plotly_color(get_option("axis_color",
684
817
  "black")))
685
818
  for v in (gridT1 or []): # vertical panel grid
686
819
  shapes.append({
@@ -693,7 +826,7 @@ def finish_facet(fig, levels, ax, x_lab, y_lab, gridT1,
693
826
  "type": "rect",
694
827
  "xref": f"x{suf} domain", "yref": f"y{suf} domain",
695
828
  "x0": 0, "x1": 1, "y0": 0, "y1": 1,
696
- "line": {"color": to_hex("gray75"), "width": 1},
829
+ "line": {"color": as_plotly_color("gray75"), "width": 1},
697
830
  "fillcolor": "rgba(0,0,0,0)"})
698
831
  shapes.append({ # strip label band
699
832
  "type": "rect",
@@ -718,8 +851,8 @@ def finish_facet(fig, levels, ax, x_lab, y_lab, gridT1,
718
851
 
719
852
  layout_kw = dict(
720
853
  shapes=shapes, annotations=anns, template=None,
721
- plot_bgcolor=to_hex(style_opts["panel_fill"]),
722
- paper_bgcolor=to_hex(style_opts["window_fill"]),
854
+ plot_bgcolor=as_plotly_color(style_opts["panel_fill"]),
855
+ paper_bgcolor=as_plotly_color(style_opts["window_fill"]),
723
856
  height=height if height is not None else 140 + 160 * n_row,
724
857
  )
725
858
  if width is not None:
@@ -814,7 +947,7 @@ def plot_border(top=True, right=True, color=None, width=None):
814
947
  if isinstance(color, str) and re.fullmatch(r"gray\d+", color):
815
948
  color = "gray75" # make a gray a little lighter for plotly
816
949
 
817
- line = {"color": to_hex(color), "width": width}
950
+ line = {"color": as_plotly_color(color), "width": width}
818
951
  out = []
819
952
  if top:
820
953
  out.append({"type": "line", "xref": "paper", "yref": "paper",
lessPy/plt_add.py CHANGED
@@ -13,14 +13,9 @@
13
13
  import numpy as np
14
14
  import plotly.graph_objects as go
15
15
 
16
- from .plotly_utils import as_plotly_color, make_trans, to_hex
16
+ from .plotly_utils import as_plotly_color, make_trans, r_dash, to_hex
17
17
  from .utils import get_option
18
18
 
19
- _DASH = {"dashed": "dash", "dotted": "dot",
20
- "dotdash": "dashdot", "longdash": "longdash",
21
- "twodash": "longdashdot"}
22
-
23
-
24
19
  def _as_list(v):
25
20
  if v is None:
26
21
  return []
@@ -86,7 +81,7 @@ def plt_add(fig, add, x1=None, x2=None, y1=None, y2=None):
86
81
  else as_plotly_color(fll))
87
82
  line = dict(color=col, width=max(0.5, lwd * 2))
88
83
  if lty != "solid":
89
- line["dash"] = _DASH.get(lty, lty)
84
+ line["dash"] = r_dash(lty)
90
85
 
91
86
  for xx1, yy1, xx2, yy2 in locs(obj):
92
87
  if obj == "v_line":
lessPy/plt_contour.py CHANGED
@@ -14,7 +14,7 @@ import plotly.graph_objects as go
14
14
  from scipy import stats as sps
15
15
 
16
16
  from .plotly_utils import (
17
- axis_format, axis_num, get_tick_fmt, plot_border,
17
+ as_plotly_color, axis_format, axis_num, get_tick_fmt, plot_border,
18
18
  plotly_style, to_hex)
19
19
  from .utils import get_option, pretty
20
20
 
@@ -134,7 +134,7 @@ def plt_contour(xv, yv, contour_n, contour_nbins, contour_points,
134
134
  showscale=bool(legend),
135
135
  colorbar=dict(
136
136
  tickfont=dict(
137
- color=to_hex(get_option("axis_color", "black")),
137
+ color=as_plotly_color(get_option("axis_color", "black")),
138
138
  size=16 * get_option("axis_size", 0.9)),
139
139
  outlinewidth=0),
140
140
  hovertemplate=(f"{x_lab}: {xpart}<br>{y_lab}: {ypart}"
@@ -158,7 +158,7 @@ def plt_contour(xv, yv, contour_n, contour_nbins, contour_points,
158
158
  for ex_l, ey_l in ellipses or []:
159
159
  fig.add_trace(go.Scatter(
160
160
  x=ex_l, y=ey_l, mode="lines",
161
- line=dict(color=to_hex(ellipse_color),
161
+ line=dict(color=as_plotly_color(ellipse_color),
162
162
  width=ellipse_lwd),
163
163
  hoverinfo="skip", showlegend=False))
164
164
 
@@ -167,7 +167,7 @@ def plt_contour(xv, yv, contour_n, contour_nbins, contour_points,
167
167
  continue
168
168
  fig.add_trace(go.Scatter(
169
169
  x=fl["x"], y=fl["y"], mode="lines",
170
- line=dict(color=to_hex(fit_color), width=fit_lwd),
170
+ line=dict(color=as_plotly_color(fit_color), width=fit_lwd),
171
171
  hoverinfo="skip", showlegend=False))
172
172
 
173
173
  ax_x = axis_num(x_lab, axT1, axL1)
@@ -178,8 +178,8 @@ def plt_contour(xv, yv, contour_n, contour_nbins, contour_points,
178
178
  xaxis=ax_x, yaxis=ax_y,
179
179
  shapes=plot_border(),
180
180
  template=None,
181
- plot_bgcolor=to_hex(style_opts["panel_fill"]),
182
- paper_bgcolor=to_hex(style_opts["window_fill"]),
181
+ plot_bgcolor=as_plotly_color(style_opts["panel_fill"]),
182
+ paper_bgcolor=as_plotly_color(style_opts["window_fill"]),
183
183
  )
184
184
 
185
185
  if main:
@@ -17,7 +17,7 @@ import numpy as np
17
17
  import plotly.graph_objects as go
18
18
 
19
19
  from .plotly_utils import (
20
- axis_format, facet_fig, facet_panels, finish_facet,
20
+ as_plotly_color, axis_format, facet_fig, facet_panels, finish_facet,
21
21
  plotly_style, to_hex)
22
22
  from .plt_contour import _COLORSCALE, _bw_nrd, _kde2d
23
23
  from .plt_smooth import _RAMP as _SMOOTH_RAMP
@@ -159,7 +159,7 @@ def plt_contour_facet(xv, yv, facet_arr, facet_order, facet_name,
159
159
  ex, ey = ellipse_fn(xi, yi, float(lv))
160
160
  fig.add_trace(go.Scatter(
161
161
  x=ex, y=ey, mode="lines",
162
- line=dict(color=to_hex(ellipse_color),
162
+ line=dict(color=as_plotly_color(ellipse_color),
163
163
  width=ellipse_lwd),
164
164
  hoverinfo="skip", showlegend=False,
165
165
  ), row=row, col=col)
@@ -169,7 +169,7 @@ def plt_contour_facet(xv, yv, facet_arr, facet_order, facet_name,
169
169
  if okf.sum() >= 2:
170
170
  fig.add_trace(go.Scatter(
171
171
  x=xs[okf], y=f[okf], mode="lines",
172
- line=dict(color=to_hex(fit_color),
172
+ line=dict(color=as_plotly_color(fit_color),
173
173
  width=fit_lwd),
174
174
  hoverinfo="skip", showlegend=False,
175
175
  ), row=row, col=col)
lessPy/plt_mat_plotly.py CHANGED
@@ -29,14 +29,28 @@ def _axis_ref(k, kind):
29
29
 
30
30
 
31
31
  def scatter_matrix(df, fit="lm", digits_d=2, cor_coef=True,
32
- main=None, band=True):
32
+ main=None, band=True, form="scatter",
33
+ contour_n=None, contour_nbins=50,
34
+ contour_points=False, smooth_bins=128,
35
+ smooth_power=0.25):
33
36
  """Scatterplot matrix of the columns of df, in column order.
34
37
  fit: "off" (no line, XY default), "lm" (Regression) or
35
38
  "loess". cor_coef=True puts the correlation in the upper
36
39
  triangle and the scatter below (~ .plt.mat, Regression);
37
40
  cor_coef=False draws the scatter in every off-diagonal cell,
38
41
  the symmetric matrix R's Logit uses (logit.4Pred). band adds
39
- the fit's 95% confidence band. Returns a plotly Figure."""
42
+ the fit's 95% confidence band. form="contour" or "smooth"
43
+ draws each cell as its own kernel density over the cell's
44
+ range, 8 contour bands unless contour_n is given (a cell cannot
45
+ resolve the 20 of a single plot), any fit drawn without its
46
+ band. R analog: .plt.mat() panel.dens. Returns a plotly
47
+ Figure."""
48
+ if form not in ("scatter", "contour", "smooth"):
49
+ raise ValueError(
50
+ 'A matrix renders its cells as form="scatter", "contour", '
51
+ f'or "smooth", so form="{form}" is not\navailable for a '
52
+ "matrix.")
53
+ contour_n = 8 if contour_n is None else int(contour_n)
40
54
  cols = list(df.columns)
41
55
  n = len(cols)
42
56
  if n < 2:
@@ -54,12 +68,12 @@ def scatter_matrix(df, fit="lm", digits_d=2, cor_coef=True,
54
68
  bg = window_fill if panel_fill == "transparent" else panel_fill
55
69
  diag_fill = get_option("se_fill", "#1A1A1A19")
56
70
  se_fill = get_option("se_fill", "#1A1A1A19")
57
- border = to_hex(style["panel_border"])
71
+ border = as_plotly_color(style["panel_border"])
58
72
  pt_fill = get_option("pt_fill", get_option("pt_color",
59
73
  "#324E5C"))
60
74
  pt_color = get_option("pt_color", "#324E5C")
61
- lab_color = to_hex(style["lab_color"])
62
- fit_color = to_hex(get_option("fit_color", "#5C4032"))
75
+ lab_color = as_plotly_color(style["lab_color"])
76
+ fit_color = as_plotly_color(get_option("fit_color", "#5C4032"))
63
77
  fit_lwd = get_option("fit_lwd", 2)
64
78
 
65
79
  # sizes shrink with the variable count, as R's cex.adj
@@ -78,6 +92,17 @@ def scatter_matrix(df, fit="lm", digits_d=2, cor_coef=True,
78
92
 
79
93
  # scatter in the lower triangle, and also in the
80
94
  # upper triangle when no correlations are requested
95
+ if r != c and (r > c or not cor_coef) \
96
+ and form != "scatter":
97
+ _cell_bg(fig, xr, yr, bg, border)
98
+ _density_cell(fig, r, c, vals[cols[c - 1]],
99
+ vals[cols[r - 1]], rng[cols[c - 1]],
100
+ rng[cols[r - 1]], form, contour_n,
101
+ contour_nbins, contour_points,
102
+ smooth_bins, smooth_power,
103
+ fit if do_fit else "off", fit_color,
104
+ fit_lwd)
105
+ continue
81
106
  if r != c and (r > c or not cor_coef):
82
107
  _cell_bg(fig, xr, yr, bg, border)
83
108
  _scatter_cell(fig, r, c, vals[cols[c - 1]],
@@ -135,8 +160,8 @@ def scatter_matrix(df, fit="lm", digits_d=2, cor_coef=True,
135
160
 
136
161
  fig.update_layout(
137
162
  template=None, showlegend=False,
138
- plot_bgcolor=to_hex(bg),
139
- paper_bgcolor=to_hex(window_fill))
163
+ plot_bgcolor=as_plotly_color(bg),
164
+ paper_bgcolor=as_plotly_color(window_fill))
140
165
  if main:
141
166
  fig.update_layout(title=dict(
142
167
  text=main, x=0.5, xanchor="center",
@@ -182,16 +207,17 @@ def _scatter_cell(fig, r, c, xv, yv, fit, do_fit, px,
182
207
  (f - tq * se_f)[::-1]]))
183
208
  else:
184
209
  poly = _se_band(xs, ys_s, f, 0.95)
185
- fig.add_trace(go.Scatter(
186
- x=poly[0], y=poly[1], mode="none", fill="toself",
187
- fillcolor=as_plotly_color(se_fill),
188
- hoverinfo="skip", showlegend=False), row=r, col=c)
210
+ if poly is not None:
211
+ fig.add_trace(go.Scatter(
212
+ x=poly[0], y=poly[1], mode="none", fill="toself",
213
+ fillcolor=as_plotly_color(se_fill),
214
+ hoverinfo="skip", showlegend=False), row=r, col=c)
189
215
 
190
216
  fig.add_trace(go.Scatter(
191
217
  x=xv, y=yv, mode="markers",
192
218
  marker=dict(symbol="circle", size=px, sizemode="diameter",
193
219
  color=make_trans(pt_fill, 0.9), opacity=1,
194
- line=dict(color=to_hex(pt_color), width=0.5)),
220
+ line=dict(color=as_plotly_color(pt_color), width=0.5)),
195
221
  hoverinfo="x+y", showlegend=False), row=r, col=c)
196
222
 
197
223
  if do_fit and len(xv) >= 2:
@@ -199,3 +225,50 @@ def _scatter_cell(fig, r, c, xv, yv, fit, do_fit, px,
199
225
  x=xs, y=f, mode="lines",
200
226
  line=dict(color=fit_color, width=fit_lwd),
201
227
  hoverinfo="skip", showlegend=False), row=r, col=c)
228
+
229
+
230
+ def _density_cell(fig, r, c, xv, yv, xr, yr, form, contour_n,
231
+ contour_nbins, contour_points, smooth_bins,
232
+ smooth_power, fit, fit_color, fit_lwd):
233
+ """One cell as a kernel density over the cell's range: filled
234
+ contour bands or the smooth shading, then the fit, unbanded.
235
+ R analog: panel.dens of .plt.mat()"""
236
+ from .plt_contour import _COLORSCALE, _bw_nrd, _kde2d
237
+ from .plt_smooth import _RAMP
238
+ from .XY import _loess, _plt_fit
239
+
240
+ ok = np.isfinite(xv) & np.isfinite(yv)
241
+ xo, yo = xv[ok], yv[ok]
242
+ if len(xo) < 3 or _bw_nrd(xo) <= 0 or _bw_nrd(yo) <= 0:
243
+ return
244
+ lims = (float(xr[0]), float(xr[-1]), float(yr[0]), float(yr[-1]))
245
+ n_grid = smooth_bins if form == "smooth" else contour_nbins
246
+ gx, gy, z = _kde2d(xo, yo, n_grid, lims)
247
+ if form == "smooth":
248
+ fig.add_trace(go.Heatmap(
249
+ x=gx, y=gy, z=(z ** smooth_power).T, colorscale=_RAMP,
250
+ zsmooth="best", showscale=False, hoverinfo="skip"),
251
+ row=r, col=c)
252
+ else:
253
+ lv0, lv1 = float(z.min()), float(z.max())
254
+ step = (lv1 - lv0) / contour_n
255
+ fig.add_trace(go.Contour(
256
+ x=gx, y=gy, z=z.T, colorscale=_COLORSCALE,
257
+ contours=dict(start=lv0 + step, end=lv1, size=step),
258
+ line=dict(width=0), showscale=False, hoverinfo="skip"),
259
+ row=r, col=c)
260
+ if contour_points:
261
+ fig.add_trace(go.Scatter(
262
+ x=xo, y=yo, mode="markers",
263
+ marker=dict(size=3, color="rgba(0,0,0,0.27)",
264
+ line=dict(color="white", width=0.2)),
265
+ hoverinfo="skip", showlegend=False), row=r, col=c)
266
+ if fit in ("lm", "loess"):
267
+ if fit == "loess":
268
+ xs, _, f, _ = _loess(xo, yo, 2 / 3)
269
+ else:
270
+ xs, _, f = _plt_fit(xo, yo, "lm", 1)
271
+ fig.add_trace(go.Scatter(
272
+ x=xs, y=f, mode="lines",
273
+ line=dict(color=fit_color, width=fit_lwd),
274
+ hoverinfo="skip", showlegend=False), row=r, col=c)