lessPython 0.1.0__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 (82) hide show
  1. lessPy/ANOVA.py +680 -0
  2. lessPy/Chart.py +1055 -0
  3. lessPy/Correlation.py +236 -0
  4. lessPy/Flows.py +116 -0
  5. lessPy/Logit.py +615 -0
  6. lessPy/Prop_test.py +267 -0
  7. lessPy/Regression.py +1491 -0
  8. lessPy/VariableLabels.py +119 -0
  9. lessPy/X.py +426 -0
  10. lessPy/XY.py +2007 -0
  11. lessPy/__init__.py +60 -0
  12. lessPy/anova_rmd.py +227 -0
  13. lessPy/bc_plotly.py +575 -0
  14. lessPy/bubble_plotly.py +470 -0
  15. lessPy/corCFA.py +316 -0
  16. lessPy/corEFA.py +220 -0
  17. lessPy/corPrint.py +45 -0
  18. lessPy/corProp.py +73 -0
  19. lessPy/corRead.py +48 -0
  20. lessPy/corReflect.py +72 -0
  21. lessPy/corReorder.py +161 -0
  22. lessPy/corScree.py +87 -0
  23. lessPy/data/Anova_1way.csv +25 -0
  24. lessPy/data/Anova_2way.csv +49 -0
  25. lessPy/data/Anova_rb.csv +8 -0
  26. lessPy/data/Anova_rbf.csv +49 -0
  27. lessPy/data/Anova_sp.csv +57 -0
  28. lessPy/data/BodyMeas.csv +341 -0
  29. lessPy/data/Cars93.csv +94 -0
  30. lessPy/data/Employee.csv +38 -0
  31. lessPy/data/Employee_lbl.csv +9 -0
  32. lessPy/data/FreqTable99.csv +5 -0
  33. lessPy/data/Jackets.csv +1026 -0
  34. lessPy/data/Learn.csv +35 -0
  35. lessPy/data/Mach4.csv +352 -0
  36. lessPy/data/Mach4_lbl.csv +21 -0
  37. lessPy/data/Reading.csv +101 -0
  38. lessPy/data/StockPrice.csv +1489 -0
  39. lessPy/data/WeightLoss.csv +11 -0
  40. lessPy/datasets.py +46 -0
  41. lessPy/date_infer.py +112 -0
  42. lessPy/details.py +314 -0
  43. lessPy/dn_plotly.py +495 -0
  44. lessPy/dot_plotly.py +385 -0
  45. lessPy/freq_poly_plotly.py +324 -0
  46. lessPy/getColors.py +399 -0
  47. lessPy/hier_plotly.py +352 -0
  48. lessPy/hs_plotly.py +395 -0
  49. lessPy/logit_rmd.py +410 -0
  50. lessPy/order_by.py +94 -0
  51. lessPy/pie_plotly.py +292 -0
  52. lessPy/pivot.py +158 -0
  53. lessPy/plotly_utils.py +787 -0
  54. lessPy/plt_add.py +129 -0
  55. lessPy/plt_contour.py +192 -0
  56. lessPy/plt_contour_facet.py +194 -0
  57. lessPy/plt_forecast.py +677 -0
  58. lessPy/plt_mat_plotly.py +201 -0
  59. lessPy/plt_plotly.py +216 -0
  60. lessPy/plt_smooth.py +170 -0
  61. lessPy/plt_time.py +143 -0
  62. lessPy/prob_norm.py +111 -0
  63. lessPy/prob_tcut.py +131 -0
  64. lessPy/prob_znorm.py +110 -0
  65. lessPy/radar_plotly.py +201 -0
  66. lessPy/reg_rmd.py +754 -0
  67. lessPy/rename.py +33 -0
  68. lessPy/reshape.py +95 -0
  69. lessPy/showColors.py +130 -0
  70. lessPy/simCImean.py +165 -0
  71. lessPy/simCLT.py +265 -0
  72. lessPy/simFlips.py +104 -0
  73. lessPy/simMeans.py +146 -0
  74. lessPy/stats_out.py +189 -0
  75. lessPy/ttest.py +641 -0
  76. lessPy/utils.py +235 -0
  77. lessPy/vbs_plotly.py +545 -0
  78. lesspython-0.1.0.dist-info/METADATA +93 -0
  79. lesspython-0.1.0.dist-info/RECORD +82 -0
  80. lesspython-0.1.0.dist-info/WHEEL +5 -0
  81. lesspython-0.1.0.dist-info/licenses/LICENSE +338 -0
  82. lesspython-0.1.0.dist-info/top_level.txt +1 -0
lessPy/plt_add.py ADDED
@@ -0,0 +1,129 @@
1
+ # plt_add.py — analog of plt.add.R
2
+ #
3
+ # The add= annotation vocabulary shared by Chart(), X(), and
4
+ # XY(): "v_line", "h_line", "line", "rect", "arrow", "point",
5
+ # and any other string draws as text at (x1, y1). Style comes
6
+ # from the add_ options (set_option): add_color (gray10),
7
+ # add_fill (#D9D9D920), add_lwd (0.5), add_lty (solid),
8
+ # add_size (1), add_trans (0) — each may be a list, recycled
9
+ # per object as in R. One object with vector coordinates
10
+ # repeats at every location; several objects consume one
11
+ # coordinate per object, in order.
12
+
13
+ import numpy as np
14
+ import plotly.graph_objects as go
15
+
16
+ from .plotly_utils import as_plotly_color, make_trans, to_hex
17
+ from .utils import get_option
18
+
19
+ _DASH = {"dashed": "dash", "dotted": "dot",
20
+ "dotdash": "dashdot", "longdash": "longdash",
21
+ "twodash": "longdashdot"}
22
+
23
+
24
+ def _as_list(v):
25
+ if v is None:
26
+ return []
27
+ if isinstance(v, (list, tuple, np.ndarray)):
28
+ return list(v)
29
+ return [v]
30
+
31
+
32
+ def plt_add(fig, add, x1=None, x2=None, y1=None, y2=None):
33
+ """Draw the add= annotation objects onto a built figure.
34
+ R analog: .plt.add()"""
35
+ add = _as_list(add)
36
+ x1v, x2v = _as_list(x1), _as_list(x2)
37
+ y1v, y2v = _as_list(y1), _as_list(y2)
38
+ n_obj = len(add)
39
+
40
+ colors = _as_list(get_option("add_color", "gray10"))
41
+ fills = _as_list(get_option("add_fill", "#D9D9D920"))
42
+ lwds = _as_list(get_option("add_lwd", 0.5))
43
+ ltys = _as_list(get_option("add_lty", "solid"))
44
+ sizes = _as_list(get_option("add_size", 1.0))
45
+ transs = _as_list(get_option("add_trans", 0.0))
46
+
47
+ idx = {"x1": 0, "x2": 0, "y1": 0, "y2": 0}
48
+
49
+ def locs(obj):
50
+ """Coordinate tuples for one object: all locations when
51
+ there is a single object, else the next one per vector."""
52
+ need_x = obj != "h_line"
53
+ need_y = obj != "v_line"
54
+ two = obj in ("line", "rect", "arrow")
55
+ if n_obj == 1:
56
+ n_loc = max(len(x1v) if need_x else 0,
57
+ len(y1v) if need_y else 0, 1)
58
+ out = []
59
+ for k in range(n_loc):
60
+ out.append((
61
+ x1v[k % len(x1v)] if need_x and x1v else None,
62
+ y1v[k % len(y1v)] if need_y and y1v else None,
63
+ x2v[k % len(x2v)] if two and x2v else None,
64
+ y2v[k % len(y2v)] if two and y2v else None))
65
+ return out
66
+ vals = []
67
+ for name, vec, need in (("x1", x1v, need_x),
68
+ ("y1", y1v, need_y),
69
+ ("x2", x2v, two),
70
+ ("y2", y2v, two)):
71
+ if need and vec:
72
+ vals.append(vec[idx[name] % len(vec)])
73
+ idx[name] += 1
74
+ else:
75
+ vals.append(None)
76
+ return [tuple(vals)]
77
+
78
+ for i, obj in enumerate(add):
79
+ col = to_hex(colors[i % len(colors)])
80
+ lwd = float(lwds[i % len(lwds)])
81
+ lty = str(ltys[i % len(ltys)])
82
+ cex = float(sizes[i % len(sizes)])
83
+ trn = float(transs[i % len(transs)])
84
+ fll = fills[i % len(fills)]
85
+ fill_c = (make_trans(fll, 1 - trn) if trn > 0
86
+ else as_plotly_color(fll))
87
+ line = dict(color=col, width=max(0.5, lwd * 2))
88
+ if lty != "solid":
89
+ line["dash"] = _DASH.get(lty, lty)
90
+
91
+ for xx1, yy1, xx2, yy2 in locs(obj):
92
+ if obj == "v_line":
93
+ fig.add_shape(type="line", xref="x",
94
+ yref="paper", x0=xx1, x1=xx1,
95
+ y0=0, y1=1, line=line)
96
+ elif obj == "h_line":
97
+ fig.add_shape(type="line", xref="paper",
98
+ yref="y", x0=0, x1=1,
99
+ y0=yy1, y1=yy1, line=line)
100
+ elif obj == "line":
101
+ fig.add_shape(type="line", xref="x", yref="y",
102
+ x0=xx1, y0=yy1, x1=xx2, y1=yy2,
103
+ line=line)
104
+ elif obj == "rect":
105
+ fig.add_shape(type="rect", xref="x", yref="y",
106
+ x0=xx1, y0=yy1, x1=xx2, y1=yy2,
107
+ line=line, fillcolor=fill_c)
108
+ elif obj == "arrow":
109
+ fig.add_annotation(
110
+ x=xx2, y=yy2, ax=xx1, ay=yy1,
111
+ xref="x", yref="y", axref="x", ayref="y",
112
+ showarrow=True, arrowhead=2,
113
+ arrowcolor=col,
114
+ arrowwidth=max(0.5, lwd * 2), text="")
115
+ elif obj == "point":
116
+ fig.add_trace(go.Scatter(
117
+ x=[xx1], y=[yy1], mode="markers",
118
+ marker=dict(symbol="circle",
119
+ size=max(3, cex * 8),
120
+ color=fill_c,
121
+ line=dict(color=col, width=1)),
122
+ hoverinfo="skip", showlegend=False))
123
+ else: # any other string: text
124
+ fig.add_annotation(
125
+ x=xx1, y=yy1, text=str(obj),
126
+ xref="x", yref="y", showarrow=False,
127
+ font=dict(color=col,
128
+ size=max(6, round(13 * cex))))
129
+ return fig
lessPy/plt_contour.py ADDED
@@ -0,0 +1,192 @@
1
+ # plt_contour.py — analog of plt.contour.R
2
+ #
3
+ # form="contour" for XY(): filled contours of the 2-D kernel
4
+ # density of <x, y>. The density grid ports MASS::kde2d() directly
5
+ # (product normal kernel, bandwidth.nrd) so the contours match R.
6
+ # Renderer conventions follow plt_plotly.py: the fit line and the
7
+ # data ellipses arrive as prepared coordinates computed by XY().
8
+ # Deviation: the fill ramp is fixed at the default-theme blues,
9
+ # colorRampPalette(c("white", getColors("blues"))) — lessPy does
10
+ # not port themes.
11
+
12
+ import numpy as np
13
+ import plotly.graph_objects as go
14
+ from scipy import stats as sps
15
+
16
+ from .plotly_utils import (
17
+ axis_format, axis_num, get_tick_fmt, plot_border,
18
+ plotly_style, to_hex)
19
+ from .utils import get_option, pretty
20
+
21
+ # getColors("blues") preceded by white: the filled-contour ramp
22
+ # of the default lessR theme
23
+ _RAMP = [
24
+ "#FFFFFF", "#CCECFF", "#B4D8FC", "#9DC5EB", "#84B2DB",
25
+ "#6B9FCC", "#4F8DBC", "#2D7CAE", "#006BA0", "#005B93",
26
+ "#004C8A", "#004087", "#0040A9",
27
+ ]
28
+ _COLORSCALE = [[i / (len(_RAMP) - 1), c]
29
+ for i, c in enumerate(_RAMP)]
30
+
31
+
32
+ def _bw_nrd(v):
33
+ """Normal reference bandwidth.
34
+ R analog: MASS::bandwidth.nrd()"""
35
+ q1, q3 = np.percentile(v, [25, 75])
36
+ return (4 * 1.06 * min(v.std(ddof=1), (q3 - q1) / 1.34)
37
+ * len(v) ** (-0.2))
38
+
39
+
40
+ def _kde2d(x, y, n, lims, h=None):
41
+ """2-D product-normal kernel density on an n x n grid over
42
+ lims=(x_lo, x_hi, y_lo, y_hi). h: bandwidth pair on the
43
+ kde2d scale (kernel sd is h/4), default bandwidth.nrd per
44
+ axis. R analog: MASS::kde2d()"""
45
+ if h is None:
46
+ hx, hy = _bw_nrd(x) / 4, _bw_nrd(y) / 4
47
+ else:
48
+ hx, hy = h[0] / 4, h[1] / 4
49
+ if hx <= 0 or hy <= 0:
50
+ raise ValueError(
51
+ "cannot estimate the density: x or y has no spread")
52
+ gx = np.linspace(lims[0], lims[1], n)
53
+ gy = np.linspace(lims[2], lims[3], n)
54
+ ax = sps.norm.pdf((gx[:, None] - x[None, :]) / hx)
55
+ ay = sps.norm.pdf((gy[:, None] - y[None, :]) / hy)
56
+ z = ax @ ay.T / (len(x) * hx * hy)
57
+ return gx, gy, z # z[i, j] at (gx[i], gy[j])
58
+
59
+
60
+ def plt_contour(xv, yv, contour_n, contour_nbins, contour_points,
61
+ pt_size, x_lab, y_lab, main, digits_d, ell95,
62
+ ellipses, ellipse_color, ellipse_lwd,
63
+ fit_lines, fit_color, fit_lwd, legend=False,
64
+ axis_fmt="K", axis_x_pre="", axis_y_pre=""):
65
+ """Filled-contour display of the joint density of x and y,
66
+ with optional data point, ellipse, and fit line overlays.
67
+ R analog: .plt.contour()"""
68
+
69
+ # KDE — extend grid beyond data so contours are not clipped
70
+ # at the data edges
71
+ x_ext = np.ptp(xv) * 0.12
72
+ y_ext = np.ptp(yv) * 0.12
73
+ gx, gy, z = _kde2d(xv, yv, contour_nbins,
74
+ (xv.min() - x_ext, xv.max() + x_ext,
75
+ yv.min() - y_ext, yv.max() + y_ext))
76
+
77
+ # 95% mass threshold, then contour-derived ranges
78
+ dz = z / z.sum()
79
+ zs = np.sort(dz.ravel())[::-1]
80
+ thr = zs[np.searchsorted(np.cumsum(zs), 0.95)]
81
+ mask = dz >= thr
82
+ x_in = gx[mask.any(axis=1)]
83
+ y_in = gy[mask.any(axis=0)]
84
+
85
+ # ellipse-derived ranges: the 95% data ellipse truncated to
86
+ # the data range
87
+ e_x, e_y = ell95
88
+ keep = ((e_x >= xv.min()) & (e_x <= xv.max())
89
+ & (e_y >= yv.min()) & (e_y <= yv.max()))
90
+ e_x, e_y = e_x[keep], e_y[keep]
91
+
92
+ # plot limits: when the kde grid covers the full data range,
93
+ # use the grid extent so contours are not clipped; otherwise
94
+ # the union of the data and truncated-ellipse ranges
95
+ def _lim(cont, data, ell, grid):
96
+ if cont[0] > data[0] or cont[1] < data[1]:
97
+ if len(ell) > 0:
98
+ return [min(data[0], ell.min()),
99
+ max(data[1], ell.max())]
100
+ return list(data)
101
+ return list(grid)
102
+ x_lim = _lim((x_in.min(), x_in.max()), (xv.min(), xv.max()),
103
+ e_x, (gx[0], gx[-1]))
104
+ y_lim = _lim((y_in.min(), y_in.max()), (yv.min(), yv.max()),
105
+ e_y, (gy[0], gy[-1]))
106
+
107
+ if contour_points: # room for edge points
108
+ x_lim[1] += 0.015 * (x_lim[1] - x_lim[0])
109
+ y_lim[1] += 0.015 * (y_lim[1] - y_lim[0])
110
+
111
+ # ticks within the limits
112
+ axT1 = pretty(x_lim[0], x_lim[1])
113
+ axT2 = pretty(y_lim[0], y_lim[1])
114
+ fmt1 = get_tick_fmt(axT1, digits_d)
115
+ fmt2 = get_tick_fmt(axT2, digits_d)
116
+ axL1 = axis_format(axT1, digits_d, axis_fmt, axis_x_pre)
117
+ axL2 = axis_format(axT2, digits_d, axis_fmt, axis_y_pre)
118
+
119
+ style_opts = plotly_style()
120
+ fig = go.Figure()
121
+
122
+ # the filled contours: contour_n bands from min to max
123
+ # density; start at the first interior level so the region
124
+ # below it fills white, as in filled.contour()
125
+ lv0, lv1 = float(z.min()), float(z.max())
126
+ step = (lv1 - lv0) / contour_n
127
+ xpart = f"%{{x:{fmt1}}}" if fmt1 else "%{x}"
128
+ ypart = f"%{{y:{fmt2}}}" if fmt2 else "%{y}"
129
+ fig.add_trace(go.Contour(
130
+ x=gx, y=gy, z=z.T,
131
+ colorscale=_COLORSCALE,
132
+ contours=dict(start=lv0 + step, end=lv1, size=step),
133
+ line=dict(width=0),
134
+ showscale=bool(legend),
135
+ colorbar=dict(
136
+ tickfont=dict(
137
+ color=to_hex(get_option("axis_color", "black")),
138
+ size=16 * get_option("axis_size", 0.9)),
139
+ outlinewidth=0),
140
+ hovertemplate=(f"{x_lab}: {xpart}<br>{y_lab}: {ypart}"
141
+ "<extra></extra>"),
142
+ ))
143
+
144
+ # R overlay style: translucent black fill, thin white border
145
+ if contour_points:
146
+ px = float(pt_size) * 7.25
147
+ if not np.isfinite(px) or px <= 0:
148
+ px = 5
149
+ fig.add_trace(go.Scatter(
150
+ x=xv, y=yv, mode="markers",
151
+ marker=dict(symbol="circle", size=px,
152
+ sizemode="diameter",
153
+ color="rgba(0,0,0,0.27)",
154
+ line=dict(color="#FFFFFF", width=0.2)),
155
+ hoverinfo="skip", showlegend=False))
156
+
157
+ # data ellipses: line only on a contour (no fill, as in R)
158
+ for ex_l, ey_l in ellipses or []:
159
+ fig.add_trace(go.Scatter(
160
+ x=ex_l, y=ey_l, mode="lines",
161
+ line=dict(color=to_hex(ellipse_color),
162
+ width=ellipse_lwd),
163
+ hoverinfo="skip", showlegend=False))
164
+
165
+ for fl in fit_lines or []:
166
+ if len(fl["x"]) < 2:
167
+ continue
168
+ fig.add_trace(go.Scatter(
169
+ x=fl["x"], y=fl["y"], mode="lines",
170
+ line=dict(color=to_hex(fit_color), width=fit_lwd),
171
+ hoverinfo="skip", showlegend=False))
172
+
173
+ ax_x = axis_num(x_lab, axT1, axL1)
174
+ ax_y = axis_num(y_lab, axT2, axL2)
175
+ ax_x["range"] = x_lim
176
+ ax_y["range"] = y_lim
177
+ fig.update_layout(
178
+ xaxis=ax_x, yaxis=ax_y,
179
+ shapes=plot_border(),
180
+ template=None,
181
+ plot_bgcolor=to_hex(style_opts["panel_fill"]),
182
+ paper_bgcolor=to_hex(style_opts["window_fill"]),
183
+ )
184
+
185
+ if main:
186
+ title_size = round(16 * get_option("main_size", 1))
187
+ fig.update_layout(
188
+ title=dict(text=main, x=0.5, xanchor="center",
189
+ y=0.99, yanchor="top",
190
+ font=dict(size=title_size)),
191
+ margin=dict(t=round(title_size * 2.2)))
192
+ return fig
@@ -0,0 +1,194 @@
1
+ # plt_contour_facet.py — analog of plt.contourFacet.R
2
+ #
3
+ # Faceted form="contour" / form="smooth" for XY(): one density
4
+ # panel per facet cell, computed on a common KDE grid and
5
+ # bandwidth so density is comparable panel-to-panel — shared
6
+ # contour levels (contour) or a shared color range on the
7
+ # smooth_power-transformed density (smooth). As in R, the smooth
8
+ # panels use kde2d with smoothScatter's bandwidth ((q95-q05)/25
9
+ # per axis, h = 4x the kernel sd) so faceted and single-panel
10
+ # smooths are consistent; contour keeps the normal-reference
11
+ # bandwidth. A cell with < 3 points draws an empty panel; only
12
+ # if no cell can estimate does the call stop. Layout follows R:
13
+ # near-square single-facet grid (n_col computed by the caller),
14
+ # rows = facet2 levels for two facets.
15
+
16
+ import numpy as np
17
+ import plotly.graph_objects as go
18
+
19
+ from .plotly_utils import (
20
+ axis_format, facet_fig, facet_panels, finish_facet,
21
+ plotly_style, to_hex)
22
+ from .plt_contour import _COLORSCALE, _bw_nrd, _kde2d
23
+ from .plt_smooth import _RAMP as _SMOOTH_RAMP
24
+ from .utils import get_option, pretty
25
+
26
+
27
+ def _ss_bw(v):
28
+ """smoothScatter's kernel sd: the 5-95% quantile spread / 25.
29
+ R analog: the ss.bw() local of .plt.contour.facet"""
30
+ q05, q95 = np.percentile(v, [5, 95])
31
+ return (q95 - q05) / 25
32
+
33
+
34
+ def plt_contour_facet(xv, yv, facet_arr, facet_order, facet_name,
35
+ facet2_arr, facet2_order, facet2_name,
36
+ contour_n, contour_nbins, contour_points,
37
+ pt_size, x_lab, y_lab, main, digits_d,
38
+ ellipse, ellipse_color, ellipse_lwd,
39
+ fit, fit_power, fit_color, fit_lwd,
40
+ fit_fn, ellipse_fn,
41
+ render="contour", smooth_power=0.25,
42
+ smooth_points=100, smooth_bins=128,
43
+ n_col=1, axis_fmt="K", axis_x_pre="",
44
+ axis_y_pre=""):
45
+ """Faceted joint-density display. fit_fn/ellipse_fn: the
46
+ caller's per-panel fit and ellipse constructors (XY._plt_fit,
47
+ XY._ellipse_region). R analog: .plt.contour.facet()"""
48
+ smooth = render == "smooth"
49
+ n_grid = smooth_bins if smooth else contour_nbins
50
+
51
+ labels, pos, sel, n_row_g, n_col = facet_panels(
52
+ facet_arr, facet_order, facet2_arr, facet2_order,
53
+ facet_name, facet2_name, n_col)
54
+ n_f = len(labels)
55
+
56
+ # common KDE grid + bandwidth: densities comparable across
57
+ # panels
58
+ x_ext = np.ptp(xv) * 0.12
59
+ y_ext = np.ptp(yv) * 0.12
60
+ lims = (xv.min() - x_ext, xv.max() + x_ext,
61
+ yv.min() - y_ext, yv.max() + y_ext)
62
+ if smooth:
63
+ h_common = [4 * _ss_bw(xv), 4 * _ss_bw(yv)]
64
+ else:
65
+ h_common = [_bw_nrd(xv), _bw_nrd(yv)]
66
+
67
+ dens = [None] * n_f
68
+ z_lo = z_hi = None
69
+ for i in range(n_f):
70
+ xi, yi = xv[sel[i]], yv[sel[i]]
71
+ if len(xi) < 3:
72
+ continue
73
+ h_i = [h if h > 0 else fb for h, fb in
74
+ zip(h_common, (_bw_nrd(xi), _bw_nrd(yi)))]
75
+ if min(h_i) <= 0: # still degenerate (e.g., collinear)
76
+ continue
77
+ gx, gy, z = _kde2d(xi, yi, n_grid, lims, h=h_i)
78
+ dens[i] = (gx, gy, z)
79
+ z_lo = float(z.min()) if z_lo is None \
80
+ else min(z_lo, float(z.min()))
81
+ z_hi = float(z.max()) if z_hi is None \
82
+ else max(z_hi, float(z.max()))
83
+ if z_hi is None:
84
+ raise ValueError(
85
+ "No facet cell has enough data (>= 3 points) for a "
86
+ + ("smooth density." if smooth else "contour."))
87
+ step = (z_hi - z_lo) / contour_n
88
+ zlim_s = (max(z_lo, 0) ** smooth_power,
89
+ max(z_hi, 0) ** smooth_power)
90
+
91
+ # shared plot limits and ticks across panels
92
+ axT1 = pretty(float(xv.min()), float(xv.max()))
93
+ axT2 = pretty(float(yv.min()), float(yv.max()))
94
+ ax = {"axT1": axT1,
95
+ "axL1": axis_format(axT1, digits_d, axis_fmt,
96
+ axis_x_pre),
97
+ "axT2": axT2,
98
+ "axL2": axis_format(axT2, digits_d, axis_fmt,
99
+ axis_y_pre)}
100
+
101
+ ell_levels = [lv for lv in np.atleast_1d(ellipse) if lv]
102
+ px = float(pt_size) * 7.25
103
+ if not np.isfinite(px) or px <= 0:
104
+ px = 5
105
+
106
+ style_opts = plotly_style()
107
+ fig = facet_fig(n_row_g, n_col)
108
+ for i in range(n_f):
109
+ row, col = pos[i]
110
+ if dens[i] is None:
111
+ continue
112
+ gx, gy, z = dens[i]
113
+ xi, yi = xv[sel[i]], yv[sel[i]]
114
+ if smooth:
115
+ fig.add_trace(go.Heatmap(
116
+ x=gx, y=gy, z=(z ** smooth_power).T,
117
+ colorscale=_SMOOTH_RAMP, zsmooth="best",
118
+ zmin=zlim_s[0], zmax=zlim_s[1],
119
+ showscale=False, hoverinfo="skip",
120
+ ), row=row, col=col)
121
+ if smooth_points > 0: # least-dense points
122
+ ix = np.clip(np.searchsorted(gx, xi), 0,
123
+ len(gx) - 1)
124
+ iy = np.clip(np.searchsorted(gy, yi), 0,
125
+ len(gy) - 1)
126
+ take = np.argsort(z[ix, iy],
127
+ kind="stable")[:smooth_points]
128
+ fig.add_trace(go.Scatter(
129
+ x=xi[take], y=yi[take], mode="markers",
130
+ marker=dict(symbol="circle", size=px,
131
+ sizemode="diameter",
132
+ color="rgba(0,0,0,0.33)",
133
+ line=dict(color="#FFFFFF",
134
+ width=0.2)),
135
+ hoverinfo="skip", showlegend=False,
136
+ ), row=row, col=col)
137
+ else:
138
+ fig.add_trace(go.Contour(
139
+ x=gx, y=gy, z=z.T,
140
+ colorscale=_COLORSCALE,
141
+ contours=dict(start=z_lo + step, end=z_hi,
142
+ size=step),
143
+ line=dict(width=0), showscale=False,
144
+ hoverinfo="skip",
145
+ ), row=row, col=col)
146
+ if contour_points:
147
+ fig.add_trace(go.Scatter(
148
+ x=xi, y=yi, mode="markers",
149
+ marker=dict(symbol="circle", size=px,
150
+ sizemode="diameter",
151
+ color="rgba(0,0,0,0.27)",
152
+ line=dict(color="#FFFFFF",
153
+ width=0.2)),
154
+ hoverinfo="skip", showlegend=False,
155
+ ), row=row, col=col)
156
+ for lv in ell_levels: # data ellipse(s)
157
+ if len(xi) < 3:
158
+ break
159
+ ex, ey = ellipse_fn(xi, yi, float(lv))
160
+ fig.add_trace(go.Scatter(
161
+ x=ex, y=ey, mode="lines",
162
+ line=dict(color=to_hex(ellipse_color),
163
+ width=ellipse_lwd),
164
+ hoverinfo="skip", showlegend=False,
165
+ ), row=row, col=col)
166
+ if fit != "off" and len(xi) >= 3:
167
+ xs, _, f = fit_fn(xi, yi, fit, fit_power)
168
+ okf = np.isfinite(f)
169
+ if okf.sum() >= 2:
170
+ fig.add_trace(go.Scatter(
171
+ x=xs[okf], y=f[okf], mode="lines",
172
+ line=dict(color=to_hex(fit_color),
173
+ width=fit_lwd),
174
+ hoverinfo="skip", showlegend=False,
175
+ ), row=row, col=col)
176
+
177
+ # size the figure so each contour panel is roughly square
178
+ # (they read as scatter panels, not wide distribution strips)
179
+ panel = 300
180
+ finish_facet(fig, labels, ax, x_lab, y_lab, gridT1=None,
181
+ style_opts=style_opts, n_col=n_col, pos=pos,
182
+ height=150 + panel * n_row_g,
183
+ width=130 + panel * n_col)
184
+ # density panels span the data, clipped to the shared limits
185
+ pad_x = 0.04 * (axT1[-1] - axT1[0])
186
+ pad_y = 0.04 * (axT2[-1] - axT2[0])
187
+ fig.update_xaxes(range=[axT1[0] - pad_x, axT1[-1] + pad_x])
188
+ fig.update_yaxes(range=[axT2[0] - pad_y, axT2[-1] + pad_y])
189
+ if main:
190
+ title_size = round(16 * get_option("main_size", 1))
191
+ fig.update_layout(
192
+ title=dict(text=main, x=0.5, xanchor="center",
193
+ font=dict(size=title_size)))
194
+ return fig