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/bc_plotly.py
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
# bc_plotly.py — analog of bc.plotly.R (.bc.plotly)
|
|
2
|
+
#
|
|
3
|
+
# Renders a bar chart from ALREADY-TABULATED input, the same division
|
|
4
|
+
# of labor as lessR: aggregation happens upstream (later, in Chart()
|
|
5
|
+
# and X()); this function only renders.
|
|
6
|
+
#
|
|
7
|
+
# Input contract (mirrors the R named-vector / table contract):
|
|
8
|
+
# 1-D pandas Series: index = category names, values = counts/means
|
|
9
|
+
# 2-D pandas DataFrame: rows = `by` levels, columns = x categories
|
|
10
|
+
# (the shape produced by pandas.crosstab(by, x))
|
|
11
|
+
#
|
|
12
|
+
# counts= carries the frequencies behind heights that Chart has
|
|
13
|
+
# already rescaled to proportions (stack100), so the hover
|
|
14
|
+
# percentages and the labels="input" text report the data.
|
|
15
|
+
#
|
|
16
|
+
# One deliberate deviation from the R source: in lessR the caller
|
|
17
|
+
# computes axis ticks (ax) and grid positions (gridT) upstream. Here,
|
|
18
|
+
# when ax is None they are computed internally with utils.pretty(),
|
|
19
|
+
# so bc_plotly() is callable on its own until Chart() exists.
|
|
20
|
+
#
|
|
21
|
+
# gap= and break_x= are rendering controls that R applies only in its
|
|
22
|
+
# base-graphics path (.bc.plotly never receives them); scale_y=
|
|
23
|
+
# reaches R's plotly through the ax/gridT it computes upstream.
|
|
24
|
+
|
|
25
|
+
import math
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
import pandas as pd
|
|
29
|
+
import plotly.graph_objects as go
|
|
30
|
+
|
|
31
|
+
from .utils import fmt, get_option, pretty
|
|
32
|
+
from .plotly_utils import (
|
|
33
|
+
BASE_COLORS, as_plotly_color, axis_cat, axis_format, axis_num,
|
|
34
|
+
contrast_text_for_hex, facet_fig, facet_pos, finish_facet,
|
|
35
|
+
abbrev, get_tick_fmt, is_integer_valued, legend_style,
|
|
36
|
+
plot_border, plotly_style, to_hex, x_grid, y_grid,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_LABEL_VALUES = ("%", "input", "prop", "off")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _as_list(v):
|
|
43
|
+
if v is None:
|
|
44
|
+
return []
|
|
45
|
+
if isinstance(v, (list, tuple)):
|
|
46
|
+
return list(v)
|
|
47
|
+
return [v]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _rep_len(v, n):
|
|
51
|
+
v = _as_list(v)
|
|
52
|
+
return [v[i % len(v)] for i in range(n)]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _pick(v, name, i):
|
|
56
|
+
"""Color for group `name` (position i): by name if v is a dict,
|
|
57
|
+
else recycled by position. R analog: .pick()"""
|
|
58
|
+
if isinstance(v, dict):
|
|
59
|
+
if name in v:
|
|
60
|
+
return v[name]
|
|
61
|
+
v = list(v.values())
|
|
62
|
+
v = _as_list(v)
|
|
63
|
+
if not v:
|
|
64
|
+
return "black"
|
|
65
|
+
return v[i % len(v)]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _bar_labels(vals, share, labels, digits_d, counts=None,
|
|
69
|
+
labels_decimals=None):
|
|
70
|
+
"""vals are the plotted bar heights, share the proportion each
|
|
71
|
+
one represents. counts, supplied when the heights are already
|
|
72
|
+
proportions (Chart's stack100), are the frequencies behind them
|
|
73
|
+
that labels="input" displays in their place.
|
|
74
|
+
R analog: bc.main.R x.txt <- as.character(x.count)
|
|
75
|
+
|
|
76
|
+
labels_decimals sets the decimal places of the label text.
|
|
77
|
+
None keeps each mode's own default, which is what a direct
|
|
78
|
+
call to bc_plotly() gets; Chart() always resolves a value."""
|
|
79
|
+
if labels == "off":
|
|
80
|
+
return ["" for _ in vals]
|
|
81
|
+
if labels == "input":
|
|
82
|
+
src = vals if counts is None else counts
|
|
83
|
+
if labels_decimals is not None:
|
|
84
|
+
d = int(labels_decimals)
|
|
85
|
+
elif counts is None:
|
|
86
|
+
d = digits_d
|
|
87
|
+
else:
|
|
88
|
+
d = 0 if is_integer_valued(counts) else digits_d
|
|
89
|
+
return [fmt(v, d) for v in src]
|
|
90
|
+
if labels == "%":
|
|
91
|
+
d = 0 if labels_decimals is None else int(labels_decimals)
|
|
92
|
+
return [f"{100 * s:.{d}f}%" for s in share]
|
|
93
|
+
d = 2 if labels_decimals is None else int(labels_decimals)
|
|
94
|
+
return [f"{s:.{d}f}" for s in share] # "prop"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _break_labels(cats, break_x):
|
|
98
|
+
"""Category labels for the categorical axis: a space becomes a
|
|
99
|
+
line break when break_x, and "~" is the non-breaking space that
|
|
100
|
+
survives it. R analog: .get.val.ln() in zzz.R"""
|
|
101
|
+
if break_x:
|
|
102
|
+
return [c.replace(" ", "<br>").replace("~", " ")
|
|
103
|
+
for c in cats]
|
|
104
|
+
return [c.replace("~", " ") for c in cats]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _bar_gaps(gap, beside, n_groups):
|
|
108
|
+
"""R's barplot space= (gaps in multiples of bar width) as
|
|
109
|
+
plotly's bargap / bargroupgap (fractions of a category slot).
|
|
110
|
+
gap=None leaves plotly's own defaults in place.
|
|
111
|
+
R analog: bc.main.R gap default c(0.1, 1) beside, else 0.2"""
|
|
112
|
+
if gap is None:
|
|
113
|
+
return None, None
|
|
114
|
+
if isinstance(gap, (list, tuple, np.ndarray)):
|
|
115
|
+
within, between = float(gap[0]), float(gap[-1])
|
|
116
|
+
else:
|
|
117
|
+
within = between = float(gap)
|
|
118
|
+
if within < 0 or between < 0:
|
|
119
|
+
raise ValueError("gap cannot be negative")
|
|
120
|
+
if not beside:
|
|
121
|
+
return between / (1.0 + between), None
|
|
122
|
+
# a slot holds n_groups bars, the gaps between them, and one
|
|
123
|
+
# between-group gap; plotly splits what bargap leaves into
|
|
124
|
+
# n_groups equal sub-slots, each bargroupgap of it empty
|
|
125
|
+
k = max(1, n_groups)
|
|
126
|
+
slot = k + (k - 1) * within + between
|
|
127
|
+
return between / slot, (k - 1) * within / (k + (k - 1) * within)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _decimals(v):
|
|
131
|
+
"""Decimal places needed to write v exactly. R analog: .max.dd()"""
|
|
132
|
+
s = f"{float(v):.10f}".rstrip("0")
|
|
133
|
+
return len(s.split(".")[1]) if "." in s else 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def bc_plotly(x, x_name=None, y_name=None, by_name=None,
|
|
137
|
+
x_lab=None, y_lab=None,
|
|
138
|
+
fill=None, border="off", opacity=None,
|
|
139
|
+
beside=False, horiz=False, ax=None, grid=None,
|
|
140
|
+
counts=None, gap=None, scale_y=None, break_x=False,
|
|
141
|
+
digits_d=None, main=None, rotate_x=0, rotate_y=0,
|
|
142
|
+
axis_fmt="K", axis_x_pre="", axis_y_pre="",
|
|
143
|
+
labels=None, labels_size=0.90, labels_color=None,
|
|
144
|
+
labels_position=None, labels_decimals=None,
|
|
145
|
+
labels_autocontrast=False,
|
|
146
|
+
legend_title=None, legend_position=None,
|
|
147
|
+
legend_labels=None, legend_horiz=False,
|
|
148
|
+
legend_size=None, legend_abbrev=None,
|
|
149
|
+
legend_adjust=0, style_opts=None):
|
|
150
|
+
|
|
151
|
+
# R analog: missing(labels); None = caller did not supply labels=
|
|
152
|
+
missing_labels = labels is None
|
|
153
|
+
if not missing_labels and labels not in _LABEL_VALUES:
|
|
154
|
+
raise ValueError(f"labels must be one of {_LABEL_VALUES}")
|
|
155
|
+
if missing_labels:
|
|
156
|
+
labels = "input" # both R paths override to "input"
|
|
157
|
+
|
|
158
|
+
# labels_position None = auto-tune: plotly places each label
|
|
159
|
+
# inside its bar, or outside when the bar is too small to hold
|
|
160
|
+
# it. Stacked segments always label inside — outside placement
|
|
161
|
+
# would stack the labels on top of the next segment.
|
|
162
|
+
if labels_position not in (None, "in", "out"):
|
|
163
|
+
raise ValueError('labels_position must be "in" or "out"')
|
|
164
|
+
|
|
165
|
+
two_d = isinstance(x, pd.DataFrame)
|
|
166
|
+
if not two_d and not isinstance(x, pd.Series):
|
|
167
|
+
raise TypeError("x must be a pandas Series (1-D) or "
|
|
168
|
+
"DataFrame (2-D, rows = by levels)")
|
|
169
|
+
|
|
170
|
+
# counts: the frequencies behind heights that are already
|
|
171
|
+
# proportions (Chart's stack100), so the hover percentages and
|
|
172
|
+
# the labels="input" text come from the data, not the heights
|
|
173
|
+
prop_scaled = counts is not None
|
|
174
|
+
cnt = None
|
|
175
|
+
if prop_scaled:
|
|
176
|
+
cnt = np.asarray(counts, dtype=float)
|
|
177
|
+
if cnt.shape != np.asarray(x, dtype=float).shape:
|
|
178
|
+
raise ValueError("counts must have the same shape as x")
|
|
179
|
+
|
|
180
|
+
# derive names from pandas metadata when not given
|
|
181
|
+
if x_name is None:
|
|
182
|
+
x_name = (x.columns.name if two_d else x.index.name) or "x"
|
|
183
|
+
if y_name is None:
|
|
184
|
+
y_name = "Count" if two_d else (x.name or "Count")
|
|
185
|
+
if two_d and by_name is None:
|
|
186
|
+
by_name = x.index.name or "by"
|
|
187
|
+
x_lab = x_name if x_lab is None else x_lab
|
|
188
|
+
y_lab = y_name if y_lab is None else y_lab
|
|
189
|
+
if digits_d is None:
|
|
190
|
+
digits_d = get_option("digits_d", 2)
|
|
191
|
+
if style_opts is None:
|
|
192
|
+
style_opts = plotly_style()
|
|
193
|
+
if fill is None:
|
|
194
|
+
fill = BASE_COLORS
|
|
195
|
+
|
|
196
|
+
title_size = round(16 * get_option("main_size", 1))
|
|
197
|
+
alpha_fill = 0.92 if opacity is None else float(opacity)
|
|
198
|
+
|
|
199
|
+
# labels_size arrives as a cex value (e.g. 0.90); convert to px
|
|
200
|
+
labels_px = max(8, round(labels_size * 14))
|
|
201
|
+
|
|
202
|
+
# % lines in hover only when data are counts (labels="input"
|
|
203
|
+
# passed explicitly signals means / deviations)
|
|
204
|
+
show_pct_hover = missing_labels or labels != "input"
|
|
205
|
+
|
|
206
|
+
stacked = two_d and not beside
|
|
207
|
+
if stacked and labels_position == "out":
|
|
208
|
+
raise ValueError(
|
|
209
|
+
'labels_position="out" is not meaningful for a stacked '
|
|
210
|
+
"bar chart (a by= variable without beside=)")
|
|
211
|
+
if stacked:
|
|
212
|
+
text_pos = "inside"
|
|
213
|
+
elif labels_position == "out":
|
|
214
|
+
text_pos = "outside"
|
|
215
|
+
elif labels_position == "in":
|
|
216
|
+
text_pos = "inside"
|
|
217
|
+
else:
|
|
218
|
+
text_pos = "auto" # per-bar in/out, the auto-tune
|
|
219
|
+
# a label drawn outside sits on the panel background, not the
|
|
220
|
+
# bar fill, so it gets the axis text color (R: Chart.R ~287)
|
|
221
|
+
out_col = (to_hex(labels_color) if labels_color is not None
|
|
222
|
+
else to_hex(get_option("axis_color", "black")))
|
|
223
|
+
|
|
224
|
+
def text_colors(fill_hex, n):
|
|
225
|
+
if labels_color is not None:
|
|
226
|
+
return _rep_len(to_hex(labels_color), n)
|
|
227
|
+
if labels_autocontrast:
|
|
228
|
+
if isinstance(fill_hex, list):
|
|
229
|
+
return [contrast_text_for_hex(c) for c in fill_hex]
|
|
230
|
+
return _rep_len(contrast_text_for_hex(fill_hex), n)
|
|
231
|
+
# default: white labels inside the bar (like pie/sunburst);
|
|
232
|
+
# labels drawn OUTSIDE keep out_col, dark on the panel
|
|
233
|
+
return _rep_len("white", n)
|
|
234
|
+
|
|
235
|
+
# ------------------ 1-D ------------------
|
|
236
|
+
if not two_d:
|
|
237
|
+
cats = [str(c) for c in x.index]
|
|
238
|
+
vals = x.to_numpy(dtype=float)
|
|
239
|
+
|
|
240
|
+
total_n = np.nansum(vals)
|
|
241
|
+
share = (vals / total_n if total_n > 0
|
|
242
|
+
else np.full(len(vals), np.nan))
|
|
243
|
+
|
|
244
|
+
tick_fmt = get_tick_fmt(vals, digits_d)
|
|
245
|
+
pct_line = ("<br>% of total: %{customdata:.1%}"
|
|
246
|
+
if show_pct_hover else "")
|
|
247
|
+
if not horiz:
|
|
248
|
+
hover = (f"{x_name}: %{{x}}<br>"
|
|
249
|
+
f"{y_name}: %{{y:{tick_fmt}}}"
|
|
250
|
+
f"{pct_line}<extra></extra>")
|
|
251
|
+
else:
|
|
252
|
+
hover = (f"{x_name}: %{{y}}<br>"
|
|
253
|
+
f"{y_name}: %{{x:{tick_fmt}}}"
|
|
254
|
+
f"{pct_line}<extra></extra>")
|
|
255
|
+
|
|
256
|
+
fill_vec = as_plotly_color(_rep_len(fill, len(cats)))
|
|
257
|
+
border_vec = as_plotly_color(_rep_len(border, len(cats)))
|
|
258
|
+
|
|
259
|
+
txt = _bar_labels(vals, share, labels, digits_d, counts=cnt,
|
|
260
|
+
labels_decimals=labels_decimals)
|
|
261
|
+
txt_col = text_colors(fill_vec, len(cats))
|
|
262
|
+
|
|
263
|
+
plt = go.Figure(go.Bar(
|
|
264
|
+
x=cats if not horiz else vals,
|
|
265
|
+
y=vals if not horiz else cats,
|
|
266
|
+
orientation=None if not horiz else "h",
|
|
267
|
+
marker=dict(
|
|
268
|
+
color=fill_vec,
|
|
269
|
+
opacity=alpha_fill,
|
|
270
|
+
line=dict(color=border_vec, width=1),
|
|
271
|
+
),
|
|
272
|
+
customdata=share,
|
|
273
|
+
hovertemplate=hover,
|
|
274
|
+
text=txt,
|
|
275
|
+
textposition=text_pos,
|
|
276
|
+
insidetextanchor="middle",
|
|
277
|
+
textfont=dict(size=labels_px),
|
|
278
|
+
insidetextfont=dict(color=txt_col),
|
|
279
|
+
outsidetextfont=dict(color=out_col),
|
|
280
|
+
cliponaxis=False,
|
|
281
|
+
))
|
|
282
|
+
|
|
283
|
+
cat_array = list(dict.fromkeys(cats))
|
|
284
|
+
stack_max = np.nanmax(vals) if len(vals) else 1
|
|
285
|
+
stack_min = np.nanmin(vals) if len(vals) else 0
|
|
286
|
+
|
|
287
|
+
# ------------------ 2-D ------------------
|
|
288
|
+
else:
|
|
289
|
+
groups = [str(g) for g in x.index]
|
|
290
|
+
# the key text: legend_labels renames the by levels and
|
|
291
|
+
# legend_abbrev truncates them. R analog: legend_labels /
|
|
292
|
+
# legend_abbrev of Chart()
|
|
293
|
+
if legend_labels is not None:
|
|
294
|
+
if len(legend_labels) != len(groups):
|
|
295
|
+
raise ValueError(
|
|
296
|
+
f"legend_labels has {len(legend_labels)} entries "
|
|
297
|
+
f"for {len(groups)} levels of the by variable")
|
|
298
|
+
key_names = [str(v) for v in legend_labels]
|
|
299
|
+
else:
|
|
300
|
+
key_names = list(groups)
|
|
301
|
+
key_names = [abbrev(k, legend_abbrev) for k in key_names]
|
|
302
|
+
cats = [str(c) for c in x.columns]
|
|
303
|
+
mat = x.to_numpy(dtype=float)
|
|
304
|
+
|
|
305
|
+
# percentages come from the counts when the heights have
|
|
306
|
+
# already been rescaled to proportions (stack100)
|
|
307
|
+
base = mat if cnt is None else cnt
|
|
308
|
+
total_n = np.nansum(base)
|
|
309
|
+
share_total = (base / total_n if total_n > 0
|
|
310
|
+
else np.full(base.shape, np.nan))
|
|
311
|
+
x_totals = np.nansum(base, axis=0)
|
|
312
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
313
|
+
share_within_x = np.where(x_totals != 0,
|
|
314
|
+
base / x_totals, np.nan)
|
|
315
|
+
|
|
316
|
+
tick_fmt = get_tick_fmt(mat.ravel(), digits_d)
|
|
317
|
+
pct_lines_2d = (
|
|
318
|
+
f"<br>% of {x_name}: %{{customdata[1]:.1%}}"
|
|
319
|
+
"<br>% of total: %{customdata[0]:.1%}"
|
|
320
|
+
if show_pct_hover else "")
|
|
321
|
+
if not horiz:
|
|
322
|
+
hover = (f"{x_name}: %{{x}}<br>"
|
|
323
|
+
f"{by_name}: %{{fullData.name}}<br>"
|
|
324
|
+
f"{y_name}: %{{y:{tick_fmt}}}"
|
|
325
|
+
f"{pct_lines_2d}<extra></extra>")
|
|
326
|
+
else:
|
|
327
|
+
hover = (f"{x_name}: %{{y}}<br>"
|
|
328
|
+
f"{by_name}: %{{fullData.name}}<br>"
|
|
329
|
+
f"{y_name}: %{{x:{tick_fmt}}}"
|
|
330
|
+
f"{pct_lines_2d}<extra></extra>")
|
|
331
|
+
|
|
332
|
+
plt = go.Figure()
|
|
333
|
+
|
|
334
|
+
for i, gname in enumerate(groups):
|
|
335
|
+
row = mat[i]
|
|
336
|
+
fill_i = as_plotly_color(_pick(fill, gname, i))
|
|
337
|
+
border_i = as_plotly_color(_pick(border, gname, i))
|
|
338
|
+
|
|
339
|
+
cd = np.stack([share_total[i], share_within_x[i]],
|
|
340
|
+
axis=-1)
|
|
341
|
+
# a stack100 bar IS its within-x percentage, so that is
|
|
342
|
+
# what "%"/"prop" report there (R: .fmt(x * 100))
|
|
343
|
+
txt = _bar_labels(
|
|
344
|
+
row,
|
|
345
|
+
share_within_x[i] if prop_scaled else share_total[i],
|
|
346
|
+
labels, digits_d,
|
|
347
|
+
counts=None if cnt is None else cnt[i],
|
|
348
|
+
labels_decimals=labels_decimals)
|
|
349
|
+
txt_col = text_colors(fill_i, len(row))
|
|
350
|
+
|
|
351
|
+
plt.add_trace(go.Bar(
|
|
352
|
+
x=cats if not horiz else row,
|
|
353
|
+
y=row if not horiz else cats,
|
|
354
|
+
orientation=None if not horiz else "h",
|
|
355
|
+
name=key_names[i],
|
|
356
|
+
legendgroup=gname,
|
|
357
|
+
marker=dict(
|
|
358
|
+
color=fill_i,
|
|
359
|
+
opacity=alpha_fill,
|
|
360
|
+
line=dict(color=border_i, width=1),
|
|
361
|
+
),
|
|
362
|
+
customdata=cd,
|
|
363
|
+
hovertemplate=hover,
|
|
364
|
+
text=txt,
|
|
365
|
+
textposition=text_pos,
|
|
366
|
+
insidetextanchor="middle",
|
|
367
|
+
textfont=dict(size=labels_px),
|
|
368
|
+
insidetextfont=dict(color=txt_col),
|
|
369
|
+
outsidetextfont=dict(color=out_col),
|
|
370
|
+
cliponaxis=False,
|
|
371
|
+
))
|
|
372
|
+
|
|
373
|
+
plt.update_layout(barmode="group" if beside else "stack")
|
|
374
|
+
|
|
375
|
+
cat_array = list(dict.fromkeys(cats))
|
|
376
|
+
if beside:
|
|
377
|
+
stack_max = np.nanmax(mat)
|
|
378
|
+
stack_min = np.nanmin(mat)
|
|
379
|
+
else: # stacked: positive and negative runs sum separately
|
|
380
|
+
stack_max = np.nanmax(
|
|
381
|
+
np.nansum(np.clip(mat, 0, None), axis=0))
|
|
382
|
+
stack_min = np.nanmin(
|
|
383
|
+
np.nansum(np.clip(mat, None, 0), axis=0))
|
|
384
|
+
|
|
385
|
+
# --- axes & background ------------------------------------------
|
|
386
|
+
# ax not supplied: compute tick positions here (see header note);
|
|
387
|
+
# value-axis labels through the axis_fmt policies ("60K"), as in
|
|
388
|
+
# R (.axis.format), with the prefix of the physical value axis
|
|
389
|
+
val_range = None
|
|
390
|
+
if ax is None and scale_y is not None:
|
|
391
|
+
# (min, max, n_intervals) gives n_intervals + 1 ticks, the
|
|
392
|
+
# axTicks(axp=) contract; R caps the tick decimals at 2
|
|
393
|
+
lo, hi = float(scale_y[0]), float(scale_y[1])
|
|
394
|
+
tickvals = list(np.linspace(lo, hi, int(scale_y[2]) + 1))
|
|
395
|
+
nd = min(2, max(_decimals(t) for t in tickvals))
|
|
396
|
+
ax = {"tickvals": tickvals,
|
|
397
|
+
"ticktext": axis_format(
|
|
398
|
+
tickvals, nd, axis_fmt,
|
|
399
|
+
axis_x_pre if horiz else axis_y_pre)}
|
|
400
|
+
val_range = [lo, hi]
|
|
401
|
+
if ax is None:
|
|
402
|
+
tickvals = pretty(min(0.0, float(stack_min)),
|
|
403
|
+
max(0.0, float(stack_max)))
|
|
404
|
+
ax = {"tickvals": tickvals,
|
|
405
|
+
"ticktext": axis_format(
|
|
406
|
+
tickvals, digits_d, axis_fmt,
|
|
407
|
+
axis_x_pre if horiz else axis_y_pre)}
|
|
408
|
+
if grid is None:
|
|
409
|
+
grid = ax["tickvals"]
|
|
410
|
+
|
|
411
|
+
# category labels wrap at their spaces when break_x; the bar
|
|
412
|
+
# coordinates stay the raw category values
|
|
413
|
+
cat_text = _break_labels(cat_array, break_x)
|
|
414
|
+
cat_ticks = dict(tickmode="array",
|
|
415
|
+
tickvals=list(range(len(cat_array))),
|
|
416
|
+
ticktext=cat_text)
|
|
417
|
+
|
|
418
|
+
bargap, bargroupgap = _bar_gaps(
|
|
419
|
+
gap, beside and two_d, len(x.index) if two_d else 1)
|
|
420
|
+
|
|
421
|
+
border_shapes = plot_border()
|
|
422
|
+
|
|
423
|
+
if horiz:
|
|
424
|
+
y_axis_cat = axis_cat(x_lab)
|
|
425
|
+
y_axis_cat.update(categoryorder="array",
|
|
426
|
+
categoryarray=cat_array, **cat_ticks)
|
|
427
|
+
if rotate_y:
|
|
428
|
+
y_axis_cat["tickangle"] = -rotate_y
|
|
429
|
+
val_axis = axis_num(y_lab, ax["tickvals"], ax["ticktext"])
|
|
430
|
+
if rotate_x:
|
|
431
|
+
val_axis["tickangle"] = -rotate_x
|
|
432
|
+
if val_range is not None:
|
|
433
|
+
val_axis["range"] = val_range
|
|
434
|
+
plt.update_layout(
|
|
435
|
+
xaxis=val_axis,
|
|
436
|
+
yaxis=y_axis_cat,
|
|
437
|
+
shapes=x_grid(grid) + border_shapes,
|
|
438
|
+
template=None,
|
|
439
|
+
)
|
|
440
|
+
else:
|
|
441
|
+
x_axis_cat = axis_cat(x_lab)
|
|
442
|
+
x_axis_cat.update(categoryorder="array",
|
|
443
|
+
categoryarray=cat_array,
|
|
444
|
+
tickangle=-rotate_x, **cat_ticks)
|
|
445
|
+
val_axis = axis_num(y_lab, ax["tickvals"], ax["ticktext"])
|
|
446
|
+
if rotate_y:
|
|
447
|
+
val_axis["tickangle"] = -rotate_y
|
|
448
|
+
if val_range is not None:
|
|
449
|
+
val_axis["range"] = val_range
|
|
450
|
+
plt.update_layout(
|
|
451
|
+
xaxis=x_axis_cat,
|
|
452
|
+
yaxis=val_axis,
|
|
453
|
+
shapes=y_grid(grid) + border_shapes,
|
|
454
|
+
template=None,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
if bargap is not None:
|
|
458
|
+
plt.update_layout(bargap=bargap)
|
|
459
|
+
if bargroupgap is not None:
|
|
460
|
+
plt.update_layout(bargroupgap=bargroupgap)
|
|
461
|
+
|
|
462
|
+
plt.update_layout(
|
|
463
|
+
plot_bgcolor=to_hex(style_opts["panel_fill"]),
|
|
464
|
+
paper_bgcolor=to_hex(style_opts["window_fill"]),
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
if by_name:
|
|
468
|
+
plt.update_layout(legend=legend_style(
|
|
469
|
+
by_name, style_opts, title=legend_title,
|
|
470
|
+
position=legend_position, horiz=legend_horiz,
|
|
471
|
+
size=legend_size, adjust=legend_adjust,
|
|
472
|
+
abbrev_n=legend_abbrev))
|
|
473
|
+
|
|
474
|
+
if main:
|
|
475
|
+
plt.update_layout(
|
|
476
|
+
title=dict(text=main, x=0.5, xanchor="center",
|
|
477
|
+
y=0.99, yanchor="top",
|
|
478
|
+
font=dict(size=title_size)),
|
|
479
|
+
margin=dict(t=round(title_size * 2.2)),
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
# R's .finalize_plotly_widget() is RStudio-viewer machinery with
|
|
483
|
+
# no Python counterpart; the figure itself is the return value
|
|
484
|
+
return plt
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def bc_facet_plotly(x, x_name=None, facet_name=None,
|
|
488
|
+
x_lab=None, y_lab=None,
|
|
489
|
+
fill=None, border="off", opacity=None,
|
|
490
|
+
proportion=False, digits_d=0, main=None,
|
|
491
|
+
n_col=1, axis_fmt="K", axis_x_pre="",
|
|
492
|
+
rotate_x=0, rotate_y=0,
|
|
493
|
+
style_opts=None):
|
|
494
|
+
"""Faceted bar chart of counts: one panel per facet level,
|
|
495
|
+
stacked on a shared count axis with horizontal bars, as in the
|
|
496
|
+
lattice Trellis bar chart (barchart(x ~ Count | facet)) that
|
|
497
|
+
R renders for Chart(x, facet=). Input: DataFrame with rows =
|
|
498
|
+
facet levels, columns = x categories, values = counts.
|
|
499
|
+
n_col > 1 lays the panels out as a grid (the lattice n_col),
|
|
500
|
+
filled bottom-up as in finish_facet.
|
|
501
|
+
R analog: .bar.lattice T.type="bar" (plotly-only port)"""
|
|
502
|
+
|
|
503
|
+
if not isinstance(x, pd.DataFrame):
|
|
504
|
+
raise TypeError("x must be a pandas DataFrame with rows = "
|
|
505
|
+
"facet levels, columns = x categories")
|
|
506
|
+
if x_name is None:
|
|
507
|
+
x_name = x.columns.name or "x"
|
|
508
|
+
if facet_name is None:
|
|
509
|
+
facet_name = x.index.name or "facet"
|
|
510
|
+
if style_opts is None:
|
|
511
|
+
style_opts = plotly_style()
|
|
512
|
+
if fill is None:
|
|
513
|
+
fill = BASE_COLORS
|
|
514
|
+
|
|
515
|
+
fac_levels = [str(g) for g in x.index]
|
|
516
|
+
cats = [str(c) for c in x.columns]
|
|
517
|
+
mat = x.to_numpy(dtype=float)
|
|
518
|
+
n_f = len(fac_levels)
|
|
519
|
+
|
|
520
|
+
if proportion: # per-panel, ~ prop.table margin
|
|
521
|
+
row_tot = mat.sum(axis=1, keepdims=True)
|
|
522
|
+
mat = np.divide(mat, row_tot, out=np.zeros_like(mat),
|
|
523
|
+
where=row_tot > 0)
|
|
524
|
+
val_name = "Proportion" if proportion else "Count"
|
|
525
|
+
if x_lab is None:
|
|
526
|
+
x_lab = f"{val_name} of {x_name}"
|
|
527
|
+
if y_lab is None:
|
|
528
|
+
y_lab = x_name
|
|
529
|
+
|
|
530
|
+
alpha_fill = 0.92 if opacity is None else float(opacity)
|
|
531
|
+
fill_vec = as_plotly_color(_rep_len(fill, len(cats)))
|
|
532
|
+
border_vec = as_plotly_color(_rep_len(border, len(cats)))
|
|
533
|
+
|
|
534
|
+
axT1 = pretty(0.0, float(np.nanmax(mat)) if mat.size else 1.0)
|
|
535
|
+
ax = {"axT1": axT1,
|
|
536
|
+
"axL1": axis_format(axT1, digits_d, axis_fmt,
|
|
537
|
+
axis_x_pre)}
|
|
538
|
+
|
|
539
|
+
n_col = max(1, min(int(n_col or 1), n_f))
|
|
540
|
+
fig = facet_fig(math.ceil(n_f / n_col), n_col)
|
|
541
|
+
for i, lv in enumerate(fac_levels):
|
|
542
|
+
row = mat[i]
|
|
543
|
+
hover = [
|
|
544
|
+
(f"{x_name}: {c}"
|
|
545
|
+
f"<br>{val_name}: "
|
|
546
|
+
+ (f"{v:.{max(2, digits_d)}f}" if proportion
|
|
547
|
+
else f"{v:g}")
|
|
548
|
+
+ f"<br>{facet_name}: {lv}")
|
|
549
|
+
for c, v in zip(cats, row)]
|
|
550
|
+
r, c = facet_pos(i, n_f, n_col) # first level bottom-left
|
|
551
|
+
fig.add_trace(go.Bar(
|
|
552
|
+
x=row, y=cats, orientation="h",
|
|
553
|
+
marker=dict(color=fill_vec, opacity=alpha_fill,
|
|
554
|
+
line=dict(color=border_vec, width=1)),
|
|
555
|
+
hoverinfo="text", hovertext=hover,
|
|
556
|
+
showlegend=False,
|
|
557
|
+
), row=r, col=c)
|
|
558
|
+
|
|
559
|
+
finish_facet(fig, fac_levels, ax, x_lab, y_lab, gridT1=axT1,
|
|
560
|
+
style_opts=style_opts, y_cat=cats, n_col=n_col)
|
|
561
|
+
if rotate_x:
|
|
562
|
+
fig.update_xaxes(tickangle=-rotate_x)
|
|
563
|
+
if rotate_y:
|
|
564
|
+
fig.update_yaxes(tickangle=-rotate_y)
|
|
565
|
+
|
|
566
|
+
if main:
|
|
567
|
+
title_size = round(16 * get_option("main_size", 1))
|
|
568
|
+
fig.update_layout(
|
|
569
|
+
title=dict(text=main, x=0.5, xanchor="center",
|
|
570
|
+
y=0.99, yanchor="top",
|
|
571
|
+
font=dict(size=title_size,
|
|
572
|
+
color=to_hex(get_option(
|
|
573
|
+
"lab_color", "black")))),
|
|
574
|
+
margin=dict(t=round(title_size * 2.2)))
|
|
575
|
+
return fig
|