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.
- lessPy/ANOVA.py +680 -0
- lessPy/Chart.py +1055 -0
- lessPy/Correlation.py +236 -0
- lessPy/Flows.py +116 -0
- lessPy/Logit.py +615 -0
- lessPy/Prop_test.py +267 -0
- lessPy/Regression.py +1491 -0
- lessPy/VariableLabels.py +119 -0
- lessPy/X.py +426 -0
- lessPy/XY.py +2007 -0
- lessPy/__init__.py +60 -0
- lessPy/anova_rmd.py +227 -0
- lessPy/bc_plotly.py +575 -0
- lessPy/bubble_plotly.py +470 -0
- lessPy/corCFA.py +316 -0
- lessPy/corEFA.py +220 -0
- lessPy/corPrint.py +45 -0
- lessPy/corProp.py +73 -0
- lessPy/corRead.py +48 -0
- lessPy/corReflect.py +72 -0
- lessPy/corReorder.py +161 -0
- lessPy/corScree.py +87 -0
- lessPy/data/Anova_1way.csv +25 -0
- lessPy/data/Anova_2way.csv +49 -0
- lessPy/data/Anova_rb.csv +8 -0
- lessPy/data/Anova_rbf.csv +49 -0
- lessPy/data/Anova_sp.csv +57 -0
- lessPy/data/BodyMeas.csv +341 -0
- lessPy/data/Cars93.csv +94 -0
- lessPy/data/Employee.csv +38 -0
- lessPy/data/Employee_lbl.csv +9 -0
- lessPy/data/FreqTable99.csv +5 -0
- lessPy/data/Jackets.csv +1026 -0
- lessPy/data/Learn.csv +35 -0
- lessPy/data/Mach4.csv +352 -0
- lessPy/data/Mach4_lbl.csv +21 -0
- lessPy/data/Reading.csv +101 -0
- lessPy/data/StockPrice.csv +1489 -0
- lessPy/data/WeightLoss.csv +11 -0
- lessPy/datasets.py +46 -0
- lessPy/date_infer.py +112 -0
- lessPy/details.py +314 -0
- lessPy/dn_plotly.py +495 -0
- lessPy/dot_plotly.py +385 -0
- lessPy/freq_poly_plotly.py +324 -0
- lessPy/getColors.py +399 -0
- lessPy/hier_plotly.py +352 -0
- lessPy/hs_plotly.py +395 -0
- lessPy/logit_rmd.py +410 -0
- lessPy/order_by.py +94 -0
- lessPy/pie_plotly.py +292 -0
- lessPy/pivot.py +158 -0
- lessPy/plotly_utils.py +787 -0
- lessPy/plt_add.py +129 -0
- lessPy/plt_contour.py +192 -0
- lessPy/plt_contour_facet.py +194 -0
- lessPy/plt_forecast.py +677 -0
- lessPy/plt_mat_plotly.py +201 -0
- lessPy/plt_plotly.py +216 -0
- lessPy/plt_smooth.py +170 -0
- lessPy/plt_time.py +143 -0
- lessPy/prob_norm.py +111 -0
- lessPy/prob_tcut.py +131 -0
- lessPy/prob_znorm.py +110 -0
- lessPy/radar_plotly.py +201 -0
- lessPy/reg_rmd.py +754 -0
- lessPy/rename.py +33 -0
- lessPy/reshape.py +95 -0
- lessPy/showColors.py +130 -0
- lessPy/simCImean.py +165 -0
- lessPy/simCLT.py +265 -0
- lessPy/simFlips.py +104 -0
- lessPy/simMeans.py +146 -0
- lessPy/stats_out.py +189 -0
- lessPy/ttest.py +641 -0
- lessPy/utils.py +235 -0
- lessPy/vbs_plotly.py +545 -0
- lesspython-0.1.0.dist-info/METADATA +93 -0
- lesspython-0.1.0.dist-info/RECORD +82 -0
- lesspython-0.1.0.dist-info/WHEEL +5 -0
- lesspython-0.1.0.dist-info/licenses/LICENSE +338 -0
- lesspython-0.1.0.dist-info/top_level.txt +1 -0
lessPy/bubble_plotly.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# bubble_plotly.py — analog of bubble.plotly.R
|
|
2
|
+
#
|
|
3
|
+
# Renders bubbles from ALREADY-TABULATED input:
|
|
4
|
+
# 1-D pandas Series: index = categories -> one row of bubbles
|
|
5
|
+
# along the x-axis, sized by value, with stems below
|
|
6
|
+
# 2-D pandas DataFrame: rows = `by` levels, columns = x
|
|
7
|
+
# categories -> bubble matrix (balloon plot), one bubble
|
|
8
|
+
# per cell, sized by the cell value
|
|
9
|
+
#
|
|
10
|
+
# Bubble diameter: max_px * v^power / max(v^power), with
|
|
11
|
+
# max_px = 2 * radius * dpi — size encodes magnitude, power
|
|
12
|
+
# tempers the visual dominance of large values.
|
|
13
|
+
#
|
|
14
|
+
# facet_tbls= renders the faceted 1-D grid: an ordered dict
|
|
15
|
+
# {facet level: 1-D Series on the full category set}, one panel
|
|
16
|
+
# per level, with bubble sizes and category hues normalized
|
|
17
|
+
# globally so the panels stay comparable.
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pandas as pd
|
|
21
|
+
import plotly.graph_objects as go
|
|
22
|
+
|
|
23
|
+
from .utils import fmt, get_option
|
|
24
|
+
from .plotly_utils import (
|
|
25
|
+
BASE_COLORS, contrast_text_for_hex, facet_layout, make_trans,
|
|
26
|
+
plotly_style, to_hex,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_LABEL_VALUES = ("%", "input", "prop", "off")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _diam_builder(all_values, power, radius, dpi):
|
|
33
|
+
"""Diameter function normalized to the global max.
|
|
34
|
+
R analog: bubble.diam.builder()"""
|
|
35
|
+
v = np.maximum(0, np.asarray(all_values, dtype=float))
|
|
36
|
+
with np.errstate(invalid="ignore"):
|
|
37
|
+
max_ru = np.nanmax(v ** power) if v.size else np.nan
|
|
38
|
+
max_px_diam = 2 * radius * dpi
|
|
39
|
+
|
|
40
|
+
def diam(vals):
|
|
41
|
+
vals = np.maximum(0, np.asarray(vals, dtype=float))
|
|
42
|
+
if not np.isfinite(max_ru) or max_ru <= 0:
|
|
43
|
+
return np.zeros(len(vals))
|
|
44
|
+
return max_px_diam * (vals ** power) / max_ru
|
|
45
|
+
return diam
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _label_text(vals, share_tot, labels, digits_d, lbl_d):
|
|
49
|
+
if labels == "input":
|
|
50
|
+
return [fmt(v, digits_d) for v in vals]
|
|
51
|
+
if labels == "%":
|
|
52
|
+
return [f"{100 * s:.{lbl_d}f}%" for s in share_tot]
|
|
53
|
+
return [f"{s:.{lbl_d}f}" for s in share_tot] # "prop"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def bubble_plotly(x=None, x_name=None, y_name=None, by_name=None,
|
|
57
|
+
x_lab=None, y_lab=None, main=None,
|
|
58
|
+
fill=None, border="black", opacity=None,
|
|
59
|
+
power=0.5, radius=0.50,
|
|
60
|
+
digits_d=0,
|
|
61
|
+
labels=None, labels_position="in",
|
|
62
|
+
labels_color=None, labels_size=0.90,
|
|
63
|
+
labels_decimals=None,
|
|
64
|
+
label_min_px=26, label_autocontrast=True,
|
|
65
|
+
facet_tbls=None, facet_name=None, n_col=None,
|
|
66
|
+
style_opts=None):
|
|
67
|
+
|
|
68
|
+
if labels is None:
|
|
69
|
+
labels = "%" # R analog: match.arg default
|
|
70
|
+
if labels not in _LABEL_VALUES:
|
|
71
|
+
raise ValueError(f"labels must be one of {_LABEL_VALUES}")
|
|
72
|
+
|
|
73
|
+
if facet_tbls is not None: # panels replace the x table
|
|
74
|
+
x = next(iter(facet_tbls.values()))
|
|
75
|
+
two_d = facet_tbls is None and isinstance(x, pd.DataFrame)
|
|
76
|
+
if not two_d and not isinstance(x, pd.Series):
|
|
77
|
+
raise TypeError("x must be a pandas Series (1-D) or "
|
|
78
|
+
"DataFrame (2-D, rows = by levels)")
|
|
79
|
+
|
|
80
|
+
if x_name is None:
|
|
81
|
+
x_name = (x.columns.name if two_d else x.index.name) or "x"
|
|
82
|
+
if y_name is None:
|
|
83
|
+
y_name = "Count" if two_d else (x.name or "Count")
|
|
84
|
+
if two_d and by_name is None:
|
|
85
|
+
by_name = x.index.name or "by"
|
|
86
|
+
if style_opts is None:
|
|
87
|
+
style_opts = plotly_style()
|
|
88
|
+
if fill is None:
|
|
89
|
+
fill = BASE_COLORS
|
|
90
|
+
|
|
91
|
+
title_size = round(16 * get_option("main_size", 1))
|
|
92
|
+
dpi = get_option("plotly_dpi", 96)
|
|
93
|
+
|
|
94
|
+
alpha_fill = 0.85 if opacity is None else float(opacity)
|
|
95
|
+
if not np.isfinite(alpha_fill):
|
|
96
|
+
alpha_fill = 0.85
|
|
97
|
+
alpha_fill = max(0.0, min(1.0, alpha_fill))
|
|
98
|
+
|
|
99
|
+
# "prop" plots proportions, which digits_d (0 for counts)
|
|
100
|
+
# would flatten to "0"
|
|
101
|
+
lbl_d = (int(labels_decimals) if labels_decimals is not None
|
|
102
|
+
else (2 if labels == "prop"
|
|
103
|
+
else max(0, int(digits_d))))
|
|
104
|
+
trace_mode = "markers" if labels == "off" else "markers+text"
|
|
105
|
+
# None = the default (R's labels_position %||% "in")
|
|
106
|
+
txt_pos = ("middle center"
|
|
107
|
+
if labels_position is None
|
|
108
|
+
or str(labels_position).lower() == "in"
|
|
109
|
+
else "top center")
|
|
110
|
+
txt_size = round(14 * labels_size * get_option("axis_size", 0.9))
|
|
111
|
+
|
|
112
|
+
lab_font = round(15 * get_option("lab_size", 1))
|
|
113
|
+
tick_font = round(16 * get_option("axis_size", 0.9))
|
|
114
|
+
|
|
115
|
+
def label_colors(base_hex, n):
|
|
116
|
+
if labels_color is not None:
|
|
117
|
+
return [to_hex(labels_color)] * n
|
|
118
|
+
if label_autocontrast:
|
|
119
|
+
return [contrast_text_for_hex(h) for h in base_hex]
|
|
120
|
+
return ["black"] * n
|
|
121
|
+
|
|
122
|
+
def title_layout(fig):
|
|
123
|
+
if main:
|
|
124
|
+
fig.update_layout(
|
|
125
|
+
title=dict(text=main, x=0.5, xanchor="center",
|
|
126
|
+
y=0.98, yanchor="top",
|
|
127
|
+
font=dict(size=title_size,
|
|
128
|
+
color=to_hex(get_option(
|
|
129
|
+
"lab_color", "black")))),
|
|
130
|
+
margin=dict(t=round(title_size * 2.2),
|
|
131
|
+
r=25, b=30, l=25))
|
|
132
|
+
|
|
133
|
+
# ---------- faceted 1-D grid ------------------------------------
|
|
134
|
+
# One 1-D bubble panel per facet level; sizes and category hues
|
|
135
|
+
# are normalized globally so the panels stay comparable.
|
|
136
|
+
# R analog: bubble.plotly.R faceted 1-D path
|
|
137
|
+
if facet_tbls is not None:
|
|
138
|
+
fac_levels = [str(k) for k in facet_tbls.keys()]
|
|
139
|
+
tabs = list(facet_tbls.values())
|
|
140
|
+
|
|
141
|
+
all_vals = np.concatenate(
|
|
142
|
+
[t.to_numpy(dtype=float) for t in tabs])
|
|
143
|
+
diam_fun = _diam_builder(all_vals, power, radius, dpi)
|
|
144
|
+
max_diam = float(np.nanmax(diam_fun(all_vals))) \
|
|
145
|
+
if all_vals.size else 1.0
|
|
146
|
+
if not np.isfinite(max_diam) or max_diam <= 0:
|
|
147
|
+
max_diam = 1.0
|
|
148
|
+
|
|
149
|
+
# color keyed by category name, hues shared across panels
|
|
150
|
+
cats_all = [str(c) for c in tabs[0].index]
|
|
151
|
+
cat_hex_map = {c: to_hex(fill[i % len(fill)]
|
|
152
|
+
if isinstance(fill, (list, tuple))
|
|
153
|
+
else fill)
|
|
154
|
+
for i, c in enumerate(cats_all)}
|
|
155
|
+
|
|
156
|
+
def ann_adj(i, a, d):
|
|
157
|
+
a["y"] = min(d["y"][1] + 0.04, 0.99)
|
|
158
|
+
return a
|
|
159
|
+
domains, anns = facet_layout(
|
|
160
|
+
fac_levels, facet_name or "",
|
|
161
|
+
yanchor="bottom", ann_adjust=ann_adj, n_col=n_col)
|
|
162
|
+
|
|
163
|
+
target_r = 0.35
|
|
164
|
+
y_top = target_r * 1.20
|
|
165
|
+
seg_color = to_hex(style_opts["grid_color"])
|
|
166
|
+
hover = (f"{x_name}: %{{customdata.xcat}}"
|
|
167
|
+
f"<br>{y_name}: %{{hovertext}}"
|
|
168
|
+
f"<br>% of {x_name}: %{{customdata.pct_x:.1%}}"
|
|
169
|
+
"<br>% of total: %{customdata.pct_tot:.1%}"
|
|
170
|
+
"<extra></extra>")
|
|
171
|
+
|
|
172
|
+
fig = go.Figure()
|
|
173
|
+
stems = []
|
|
174
|
+
axes = {}
|
|
175
|
+
for i, (lv, t) in enumerate(zip(fac_levels, tabs)):
|
|
176
|
+
suf = "" if i == 0 else str(i + 1)
|
|
177
|
+
cats = [str(c) for c in t.index]
|
|
178
|
+
vals = t.to_numpy(dtype=float)
|
|
179
|
+
n = len(cats)
|
|
180
|
+
|
|
181
|
+
diam_px = diam_fun(vals)
|
|
182
|
+
total = np.nansum(vals)
|
|
183
|
+
share_tot = (vals / total if total > 0
|
|
184
|
+
else np.zeros(n))
|
|
185
|
+
cat_hex = [cat_hex_map[c] for c in cats]
|
|
186
|
+
fill_rgba = make_trans(cat_hex, alpha_fill)
|
|
187
|
+
txt_cols = label_colors(cat_hex, n)
|
|
188
|
+
|
|
189
|
+
fmt_val = [fmt(v, digits_d) for v in vals]
|
|
190
|
+
if labels == "off":
|
|
191
|
+
label_show = [""] * n
|
|
192
|
+
else:
|
|
193
|
+
txt = _label_text(vals, share_tot, labels,
|
|
194
|
+
digits_d, lbl_d)
|
|
195
|
+
label_show = [tx if np.isfinite(dp)
|
|
196
|
+
and dp >= label_min_px else ""
|
|
197
|
+
for tx, dp in zip(txt, diam_px)]
|
|
198
|
+
customdata = [dict(xcat=c, bycat="", pct_x=float(s),
|
|
199
|
+
pct_tot=float(s))
|
|
200
|
+
for c, s in zip(cats, share_tot)]
|
|
201
|
+
|
|
202
|
+
fig.add_trace(go.Scatter(
|
|
203
|
+
mode=trace_mode,
|
|
204
|
+
x=cats,
|
|
205
|
+
y=[0] * n,
|
|
206
|
+
xaxis=f"x{suf}",
|
|
207
|
+
yaxis=f"y{suf}",
|
|
208
|
+
hovertext=fmt_val,
|
|
209
|
+
hovertemplate=hover,
|
|
210
|
+
customdata=customdata,
|
|
211
|
+
marker=dict(
|
|
212
|
+
symbol="circle",
|
|
213
|
+
size=diam_px.tolist(),
|
|
214
|
+
sizemode="diameter",
|
|
215
|
+
color=fill_rgba,
|
|
216
|
+
line=dict(color=to_hex(border), width=1),
|
|
217
|
+
sizemin=12,
|
|
218
|
+
),
|
|
219
|
+
text=label_show,
|
|
220
|
+
textposition=txt_pos,
|
|
221
|
+
textfont=dict(size=txt_size, color=txt_cols),
|
|
222
|
+
cliponaxis=False,
|
|
223
|
+
showlegend=False,
|
|
224
|
+
))
|
|
225
|
+
|
|
226
|
+
# stems from the panel bottom to the base of each bubble
|
|
227
|
+
radius_data = (diam_px / max_diam) * target_r
|
|
228
|
+
stems += [dict(type="line",
|
|
229
|
+
xref=f"x{suf}", yref=f"y{suf}",
|
|
230
|
+
x0=k, x1=k,
|
|
231
|
+
y0=-1, y1=-float(radius_data[k]),
|
|
232
|
+
line=dict(color=seg_color, width=1),
|
|
233
|
+
layer="below")
|
|
234
|
+
for k in range(n)]
|
|
235
|
+
|
|
236
|
+
axes[f"xaxis{suf}"] = dict(
|
|
237
|
+
type="category",
|
|
238
|
+
domain=list(domains[i]["x"]),
|
|
239
|
+
anchor=f"y{suf}",
|
|
240
|
+
title=dict(text=x_lab if x_lab is not None
|
|
241
|
+
else x_name,
|
|
242
|
+
standoff=10,
|
|
243
|
+
font=dict(size=lab_font)),
|
|
244
|
+
showgrid=False, zeroline=False, showline=True,
|
|
245
|
+
ticks="outside",
|
|
246
|
+
tickfont=dict(size=tick_font),
|
|
247
|
+
)
|
|
248
|
+
axes[f"yaxis{suf}"] = dict(
|
|
249
|
+
visible=False, fixedrange=True,
|
|
250
|
+
domain=list(domains[i]["y"]),
|
|
251
|
+
anchor=f"x{suf}",
|
|
252
|
+
range=[-1, y_top],
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
fig.update_layout(
|
|
256
|
+
hoverlabel=dict(align="left"),
|
|
257
|
+
annotations=anns,
|
|
258
|
+
shapes=stems,
|
|
259
|
+
margin=dict(t=round(title_size * 2.4) if main else 40,
|
|
260
|
+
b=8, l=20, r=20),
|
|
261
|
+
showlegend=False,
|
|
262
|
+
template=None,
|
|
263
|
+
plot_bgcolor=to_hex(style_opts["panel_fill"]),
|
|
264
|
+
paper_bgcolor=to_hex(style_opts["window_fill"]),
|
|
265
|
+
**axes,
|
|
266
|
+
)
|
|
267
|
+
if main:
|
|
268
|
+
fig.update_layout(title=dict(
|
|
269
|
+
text=main, x=0.5, xanchor="center",
|
|
270
|
+
y=0.98, yanchor="top",
|
|
271
|
+
font=dict(size=title_size,
|
|
272
|
+
color=to_hex(get_option("lab_color",
|
|
273
|
+
"black")))))
|
|
274
|
+
return fig
|
|
275
|
+
|
|
276
|
+
# ---------- single 1-D chart ------------------------------------
|
|
277
|
+
if not two_d:
|
|
278
|
+
cats = [str(c) for c in x.index]
|
|
279
|
+
vals = x.to_numpy(dtype=float)
|
|
280
|
+
n = len(cats)
|
|
281
|
+
|
|
282
|
+
diam_fun = _diam_builder(vals, power, radius, dpi)
|
|
283
|
+
diam_px = diam_fun(vals)
|
|
284
|
+
max_diam = float(np.nanmax(diam_px)) if n else 1.0
|
|
285
|
+
if not np.isfinite(max_diam) or max_diam <= 0:
|
|
286
|
+
max_diam = 1.0
|
|
287
|
+
|
|
288
|
+
total = np.nansum(vals)
|
|
289
|
+
share_tot = vals / total if total > 0 else np.zeros(n)
|
|
290
|
+
|
|
291
|
+
# one palette color per category, as in the bar chart
|
|
292
|
+
cat_hex = to_hex([fill[i % len(fill)]
|
|
293
|
+
if isinstance(fill, (list, tuple))
|
|
294
|
+
else fill for i in range(n)])
|
|
295
|
+
fill_rgba = make_trans(cat_hex, alpha_fill)
|
|
296
|
+
txt_cols = label_colors(cat_hex, n)
|
|
297
|
+
|
|
298
|
+
fmt_val = [fmt(v, digits_d) for v in vals]
|
|
299
|
+
if labels == "off":
|
|
300
|
+
label_show = [""] * n
|
|
301
|
+
else:
|
|
302
|
+
txt = _label_text(vals, share_tot, labels, digits_d,
|
|
303
|
+
lbl_d)
|
|
304
|
+
label_show = [t if np.isfinite(dp) and dp >= label_min_px
|
|
305
|
+
else "" for t, dp in zip(txt, diam_px)]
|
|
306
|
+
|
|
307
|
+
customdata = [dict(xcat=c, bycat="", pct_x=float(s),
|
|
308
|
+
pct_tot=float(s))
|
|
309
|
+
for c, s in zip(cats, share_tot)]
|
|
310
|
+
hover = (f"{x_name}: %{{customdata.xcat}}"
|
|
311
|
+
f"<br>{y_name}: %{{hovertext}}"
|
|
312
|
+
f"<br>% of {x_name}: %{{customdata.pct_x:.1%}}"
|
|
313
|
+
"<br>% of total: %{customdata.pct_tot:.1%}"
|
|
314
|
+
"<extra></extra>")
|
|
315
|
+
|
|
316
|
+
fig = go.Figure(go.Scatter(
|
|
317
|
+
mode=trace_mode,
|
|
318
|
+
x=cats,
|
|
319
|
+
y=[0] * n,
|
|
320
|
+
hovertext=fmt_val,
|
|
321
|
+
hovertemplate=hover,
|
|
322
|
+
customdata=customdata,
|
|
323
|
+
marker=dict(
|
|
324
|
+
symbol="circle",
|
|
325
|
+
size=diam_px.tolist(),
|
|
326
|
+
sizemode="diameter",
|
|
327
|
+
color=fill_rgba,
|
|
328
|
+
line=dict(color=to_hex(border), width=1),
|
|
329
|
+
sizemin=12,
|
|
330
|
+
),
|
|
331
|
+
text=label_show,
|
|
332
|
+
textposition=txt_pos,
|
|
333
|
+
textfont=dict(size=txt_size, color=txt_cols),
|
|
334
|
+
cliponaxis=False,
|
|
335
|
+
showlegend=False,
|
|
336
|
+
))
|
|
337
|
+
|
|
338
|
+
# stems from the plot bottom to the base of each bubble
|
|
339
|
+
target_r = 0.35
|
|
340
|
+
radius_data = (diam_px / max_diam) * target_r
|
|
341
|
+
y_top = target_r * 1.20
|
|
342
|
+
seg_color = to_hex(style_opts["grid_color"])
|
|
343
|
+
stems = [dict(type="line", xref="x", yref="y",
|
|
344
|
+
x0=i, x1=i, y0=-1, y1=-float(radius_data[i]),
|
|
345
|
+
line=dict(color=seg_color, width=1),
|
|
346
|
+
layer="below")
|
|
347
|
+
for i in range(n)]
|
|
348
|
+
|
|
349
|
+
fig.update_layout(
|
|
350
|
+
hoverlabel=dict(align="left"),
|
|
351
|
+
xaxis=dict(
|
|
352
|
+
type="category",
|
|
353
|
+
automargin=True,
|
|
354
|
+
title=dict(text=x_lab if x_lab is not None
|
|
355
|
+
else x_name,
|
|
356
|
+
standoff=10,
|
|
357
|
+
font=dict(size=lab_font)),
|
|
358
|
+
showgrid=False, zeroline=False, showline=True,
|
|
359
|
+
ticks="outside",
|
|
360
|
+
tickfont=dict(size=tick_font),
|
|
361
|
+
),
|
|
362
|
+
yaxis=dict(visible=False, fixedrange=True,
|
|
363
|
+
range=[-1, y_top]),
|
|
364
|
+
shapes=stems,
|
|
365
|
+
template=None,
|
|
366
|
+
plot_bgcolor=to_hex(style_opts["panel_fill"]),
|
|
367
|
+
paper_bgcolor=to_hex(style_opts["window_fill"]),
|
|
368
|
+
)
|
|
369
|
+
title_layout(fig)
|
|
370
|
+
return fig
|
|
371
|
+
|
|
372
|
+
# ---------- 2-D bubble matrix ------------------------------------
|
|
373
|
+
groups = [str(g) for g in x.index]
|
|
374
|
+
cats = [str(c) for c in x.columns]
|
|
375
|
+
mat = x.to_numpy(dtype=float)
|
|
376
|
+
n_g = len(groups)
|
|
377
|
+
|
|
378
|
+
total = np.nansum(mat)
|
|
379
|
+
share_tot = mat / total if total > 0 else np.zeros(mat.shape)
|
|
380
|
+
col_tot = np.nansum(mat, axis=0)
|
|
381
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
382
|
+
share_x = np.where(col_tot > 0, mat / col_tot, 0.0)
|
|
383
|
+
|
|
384
|
+
diam_fun = _diam_builder(mat.ravel(), power, radius, dpi)
|
|
385
|
+
|
|
386
|
+
# one color per by group
|
|
387
|
+
grp_hex = to_hex([fill[i % len(fill)]
|
|
388
|
+
if isinstance(fill, (list, tuple)) else fill
|
|
389
|
+
for i in range(n_g)])
|
|
390
|
+
grp_rgba = make_trans(grp_hex, alpha_fill)
|
|
391
|
+
grp_txt = label_colors(grp_hex, n_g)
|
|
392
|
+
|
|
393
|
+
hover = (f"{x_name}: %{{customdata.xcat}}"
|
|
394
|
+
f"<br>{by_name}: %{{customdata.bycat}}"
|
|
395
|
+
f"<br>{y_name}: %{{hovertext}}"
|
|
396
|
+
f"<br>% of {x_name}: %{{customdata.pct_x:.1%}}"
|
|
397
|
+
"<br>% of total: %{customdata.pct_tot:.1%}"
|
|
398
|
+
"<extra></extra>")
|
|
399
|
+
|
|
400
|
+
fig = go.Figure()
|
|
401
|
+
for i, g in enumerate(groups):
|
|
402
|
+
row = mat[i]
|
|
403
|
+
diam_px = diam_fun(row)
|
|
404
|
+
fmt_val = [fmt(v, digits_d) for v in row]
|
|
405
|
+
if labels == "off":
|
|
406
|
+
label_show = [""] * len(row)
|
|
407
|
+
else:
|
|
408
|
+
txt = _label_text(row, share_tot[i], labels, digits_d,
|
|
409
|
+
lbl_d)
|
|
410
|
+
label_show = [t if np.isfinite(dp) and dp >= label_min_px
|
|
411
|
+
else "" for t, dp in zip(txt, diam_px)]
|
|
412
|
+
customdata = [dict(xcat=c, bycat=g, pct_x=float(px),
|
|
413
|
+
pct_tot=float(pt))
|
|
414
|
+
for c, px, pt in zip(cats, share_x[i],
|
|
415
|
+
share_tot[i])]
|
|
416
|
+
fig.add_trace(go.Scatter(
|
|
417
|
+
mode=trace_mode,
|
|
418
|
+
x=cats,
|
|
419
|
+
y=[g] * len(cats),
|
|
420
|
+
hovertext=fmt_val,
|
|
421
|
+
hovertemplate=hover,
|
|
422
|
+
customdata=customdata,
|
|
423
|
+
marker=dict(
|
|
424
|
+
symbol="circle",
|
|
425
|
+
size=diam_px.tolist(),
|
|
426
|
+
sizemode="diameter",
|
|
427
|
+
color=grp_rgba[i],
|
|
428
|
+
line=dict(color=to_hex(border), width=1),
|
|
429
|
+
),
|
|
430
|
+
text=label_show,
|
|
431
|
+
textposition=txt_pos,
|
|
432
|
+
textfont=dict(size=txt_size, color=grp_txt[i]),
|
|
433
|
+
cliponaxis=False,
|
|
434
|
+
showlegend=False,
|
|
435
|
+
))
|
|
436
|
+
|
|
437
|
+
fig.update_layout(
|
|
438
|
+
hoverlabel=dict(align="left"),
|
|
439
|
+
xaxis=dict(
|
|
440
|
+
type="category",
|
|
441
|
+
automargin=True,
|
|
442
|
+
title=dict(text=x_lab if x_lab is not None else x_name,
|
|
443
|
+
font=dict(size=lab_font)),
|
|
444
|
+
showgrid=True,
|
|
445
|
+
gridcolor=to_hex(style_opts["grid_color"]),
|
|
446
|
+
gridwidth=1,
|
|
447
|
+
zeroline=False,
|
|
448
|
+
tickfont=dict(size=tick_font),
|
|
449
|
+
),
|
|
450
|
+
yaxis=dict(
|
|
451
|
+
type="category",
|
|
452
|
+
automargin=True,
|
|
453
|
+
title=dict(text=y_lab if y_lab is not None else by_name,
|
|
454
|
+
font=dict(size=lab_font)),
|
|
455
|
+
showgrid=False, zeroline=False,
|
|
456
|
+
categoryorder="array",
|
|
457
|
+
categoryarray=list(reversed(groups)),
|
|
458
|
+
tickfont=dict(size=tick_font),
|
|
459
|
+
),
|
|
460
|
+
template=None,
|
|
461
|
+
shapes=[dict(type="rect", xref="paper", yref="paper",
|
|
462
|
+
x0=0, x1=1, y0=0, y1=1,
|
|
463
|
+
line=dict(color=to_hex(
|
|
464
|
+
style_opts["panel_border"]), width=1),
|
|
465
|
+
layer="below")],
|
|
466
|
+
plot_bgcolor=to_hex(style_opts["panel_fill"]),
|
|
467
|
+
paper_bgcolor=to_hex(style_opts["window_fill"]),
|
|
468
|
+
)
|
|
469
|
+
title_layout(fig)
|
|
470
|
+
return fig
|