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.
- lessPy/ANOVA.py +74 -15
- lessPy/Chart.py +431 -88
- lessPy/Correlation.py +40 -11
- lessPy/Flows.py +3 -3
- lessPy/Logit.py +15 -11
- lessPy/Regression.py +27 -21
- lessPy/X.py +280 -42
- lessPy/XY.py +1332 -239
- lessPy/__init__.py +12 -2
- lessPy/_themes.py +5007 -0
- lessPy/bc_items_plotly.py +33 -20
- lessPy/bc_plotly.py +27 -10
- lessPy/bubble_plotly.py +33 -17
- lessPy/corCFA.py +9 -5
- lessPy/corProp.py +8 -4
- lessPy/corReflect.py +8 -4
- lessPy/corReorder.py +8 -4
- lessPy/corScree.py +12 -8
- lessPy/data/StockPrice.csv +1401 -1386
- lessPy/details.py +17 -4
- lessPy/dn_plotly.py +74 -54
- lessPy/dot_plotly.py +9 -8
- lessPy/freq_poly_plotly.py +18 -11
- lessPy/getColors.py +247 -20
- lessPy/hier_plotly.py +41 -14
- lessPy/hs_plotly.py +23 -11
- lessPy/pie_plotly.py +1 -1
- lessPy/pivot.py +39 -9
- lessPy/plotly_utils.py +162 -29
- lessPy/plt_add.py +2 -7
- lessPy/plt_contour.py +6 -6
- lessPy/plt_contour_facet.py +3 -3
- lessPy/plt_mat_plotly.py +85 -12
- lessPy/plt_plotly.py +34 -18
- lessPy/plt_smooth.py +4 -4
- lessPy/plt_time.py +10 -3
- lessPy/prob_norm.py +7 -3
- lessPy/prob_tcut.py +7 -3
- lessPy/prob_znorm.py +7 -1
- lessPy/profile_plotly.py +5 -4
- lessPy/radar_plotly.py +4 -3
- lessPy/rcolors.py +461 -0
- lessPy/simCImean.py +7 -3
- lessPy/simCLT.py +7 -3
- lessPy/simFlips.py +7 -3
- lessPy/simMeans.py +7 -3
- lessPy/stats_out.py +257 -11
- lessPy/style.py +167 -0
- lessPy/sunflower_plotly.py +141 -0
- lessPy/ttest.py +17 -13
- lessPy/utils.py +146 -6
- lessPy/vbs_plotly.py +39 -27
- lessPy/x_console.py +763 -0
- {lesspython-0.2.0.dist-info → lesspython-0.3.1.dist-info}/METADATA +13 -2
- lesspython-0.3.1.dist-info/RECORD +89 -0
- {lesspython-0.2.0.dist-info → lesspython-0.3.1.dist-info}/WHEEL +1 -1
- lesspython-0.2.0.dist-info/RECORD +0 -84
- {lesspython-0.2.0.dist-info → lesspython-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {lesspython-0.2.0.dist-info → lesspython-0.3.1.dist-info}/top_level.txt +0 -0
lessPy/Chart.py
CHANGED
|
@@ -35,14 +35,18 @@ from .plt_add import plt_add
|
|
|
35
35
|
from .bubble_plotly import bubble_plotly
|
|
36
36
|
from .dot_plotly import dot_plotly
|
|
37
37
|
from .hier_plotly import (
|
|
38
|
-
hier_aggregate, hier_color_resolve,
|
|
38
|
+
hier_aggregate, hier_color_resolve, hier_color_resolve_byfac,
|
|
39
|
+
hier_plotly,
|
|
39
40
|
)
|
|
40
41
|
from .pie_plotly import pie_plotly
|
|
41
42
|
from .plt_plotly import plt_plotly
|
|
42
43
|
from .profile_plotly import profile_facet_plotly
|
|
43
44
|
from .radar_plotly import radar_plotly
|
|
44
45
|
from .plotly_utils import build_title, font_scaled
|
|
45
|
-
from .stats_out import
|
|
46
|
+
from .stats_out import (
|
|
47
|
+
CALL_STATS, ChartStats, attach_stats, chart_stats,
|
|
48
|
+
chart_stats_data, resolve_quiet,
|
|
49
|
+
)
|
|
46
50
|
from .utils import (
|
|
47
51
|
STAT_FUN, STAT_LBL, category_order as _category_order,
|
|
48
52
|
facet_values, get_column as _get_column, get_option, pretty,
|
|
@@ -62,6 +66,36 @@ _THEME_PALETTE = {
|
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
|
|
69
|
+
# each theme's single bar color, a bar chart without by drawn in it
|
|
70
|
+
# (R: style(theme)$bar$bar_fill_discrete, alpha from trans_bar_fill)
|
|
71
|
+
_THEME_BAR_FILL = {
|
|
72
|
+
"lightbronze": "#7B8C96", "dodgerblue": "#1874CDE6",
|
|
73
|
+
"darkred": "#8B0000E6", "gray": "#595959E6", "gold": "#CD9B1DE6",
|
|
74
|
+
"darkgreen": "#006400E6", "blue": "#4876FFE6", "red": "#CD2626E6",
|
|
75
|
+
"rose": "#CD9B9BE6", "green": "#006400E6", "purple": "#9B30FFE6",
|
|
76
|
+
"sienna": "#CD6839E6", "brown": "#8B6969E6", "orange": "#FFA500E6",
|
|
77
|
+
"white": "#FFFFFFE6", "slatered": "#A35763E6",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _color_range(pal, n):
|
|
82
|
+
"""n colors of a named palette, alpha dropped. Two grays are set
|
|
83
|
+
further apart than the palette spaces them, gray60 and gray30,
|
|
84
|
+
as in R. R analog: .color_range()"""
|
|
85
|
+
from .getColors import getColors
|
|
86
|
+
if pal == "grays" and n == 2:
|
|
87
|
+
return ["#999999", "#4D4D4D"]
|
|
88
|
+
cols = getColors(pal, n=max(1, n), quiet=True)
|
|
89
|
+
return [c[:7] if len(c) == 9 and c.endswith("FF") else c
|
|
90
|
+
for c in cols]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _palette_names():
|
|
94
|
+
from .getColors import _SEQ_NAMES, _SEQ_R, _VIRIDIS
|
|
95
|
+
return set(_SEQ_NAMES) | set(_SEQ_R) | set(_VIRIDIS) | {
|
|
96
|
+
"hues", "Okabe-Ito", "Tableau", "distinct"}
|
|
97
|
+
|
|
98
|
+
|
|
65
99
|
def _theme_fill(theme, n):
|
|
66
100
|
"""A theme's fill: an n-color sequential palette in the theme's
|
|
67
101
|
hue. R analog: the theme branch of the fill assignment."""
|
|
@@ -80,9 +114,10 @@ def _facet_table(x_s, y_s, by_s, stat, is_agg, x_order, by_order,
|
|
|
80
114
|
"""Frequency or stat table for one facet level, on the global
|
|
81
115
|
category set(s) so panels stay aligned; absent cells fill 0,
|
|
82
116
|
as in R's xtabs. Series without by, DataFrame (by x cats)
|
|
83
|
-
with. proportion normalizes the per-panel counts
|
|
84
|
-
|
|
85
|
-
faceted radar and bubble
|
|
117
|
+
with. proportion normalizes the per-panel counts to sum to 1,
|
|
118
|
+
the faceted stat_x="proportion": the whole panel without by,
|
|
119
|
+
each by row with it. Used by the faceted radar and bubble
|
|
120
|
+
paths."""
|
|
86
121
|
if by_s is None:
|
|
87
122
|
if y_s is None:
|
|
88
123
|
t = x_s.groupby(x_s, observed=True).size() \
|
|
@@ -108,8 +143,12 @@ def _facet_table(x_s, y_s, by_s, stat, is_agg, x_order, by_order,
|
|
|
108
143
|
t = STAT_FUN[stat](
|
|
109
144
|
df.groupby(["by", "x"], observed=True)["y"]
|
|
110
145
|
).unstack("x")
|
|
111
|
-
|
|
112
|
-
|
|
146
|
+
t = t.reindex(index=by_order, columns=x_order).fillna(0)
|
|
147
|
+
if proportion and y_s is None:
|
|
148
|
+
# each group's distribution over x: every by row sums to 1
|
|
149
|
+
tot = t.sum(axis=1)
|
|
150
|
+
t = t.div(tot.where(tot != 0), axis=0).fillna(0)
|
|
151
|
+
return t
|
|
113
152
|
|
|
114
153
|
_FORMS = ("bar", "radar", "bubble", "dot", "pie", "icicle", "treemap",
|
|
115
154
|
"profile")
|
|
@@ -170,7 +209,7 @@ def _dot_origin_grid(vals, origin_in=None, is_counts=None):
|
|
|
170
209
|
def _chart_items(data, items, y, by, facet, form, stat, sort,
|
|
171
210
|
sort_miss, stack100, horiz, fill, theme, labels,
|
|
172
211
|
labels_size, gap, labels_cut, xlab, ylab, main,
|
|
173
|
-
legend_title,
|
|
212
|
+
legend_title, power, radius, one_plot,
|
|
174
213
|
n_col, n_row, axis_fmt, axis_x_pre, rotate_x,
|
|
175
214
|
rotate_y, quiet):
|
|
176
215
|
"""The multi-item stacked chart and its faceted form. R analog:
|
|
@@ -185,22 +224,74 @@ def _chart_items(data, items, y, by, facet, form, stat, sort,
|
|
|
185
224
|
if facet is not None and form != "bar":
|
|
186
225
|
raise ValueError(
|
|
187
226
|
'facet with multiple x variables requires form="bar"')
|
|
188
|
-
if form
|
|
189
|
-
raise NotImplementedError(
|
|
190
|
-
"the bubble plot frequency matrix of multiple x "
|
|
191
|
-
'variables is not yet ported; use form="bar"')
|
|
192
|
-
if form != "bar":
|
|
227
|
+
if form not in ("bar", "bubble"):
|
|
193
228
|
raise ValueError(
|
|
194
229
|
'Multiple x variables only available for form="bar"')
|
|
195
230
|
if y is not None or stat is not None:
|
|
196
231
|
raise ValueError(
|
|
197
232
|
"A multi-item chart counts the responses to each item, "
|
|
198
233
|
"so y and stat do not apply")
|
|
199
|
-
if not horiz:
|
|
200
|
-
raise NotImplementedError(
|
|
201
|
-
"the vertical multi-item chart is not yet ported")
|
|
202
234
|
cols = [_get_column(data, it, "x") for it in items]
|
|
235
|
+
|
|
236
|
+
# one_plot: the items compose one chart when they share one set of
|
|
237
|
+
# responses, decided from the data unless given; otherwise each
|
|
238
|
+
# variable is its own bar chart. R analog: Chart.R one_plot test
|
|
239
|
+
sets = [set(c.dropna().unique()) for c in cols]
|
|
240
|
+
widest = max(sets, key=len)
|
|
241
|
+
shared = not any(st - widest for st in sets)
|
|
242
|
+
if one_plot is None:
|
|
243
|
+
one_plot = shared
|
|
244
|
+
if not one_plot:
|
|
245
|
+
if facet is not None:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
"facet requires the multi-item stacked chart, which "
|
|
248
|
+
"needs the x variables to share the same response "
|
|
249
|
+
"categories: a separate chart per variable is not a "
|
|
250
|
+
"single composition, so there are no panels to "
|
|
251
|
+
"condition")
|
|
252
|
+
if form != "bar":
|
|
253
|
+
raise ValueError(
|
|
254
|
+
'A separate chart per variable is a bar chart, so it '
|
|
255
|
+
'needs form="bar"')
|
|
256
|
+
return _chart_per_variable(data, items, cols, fill, xlab,
|
|
257
|
+
main, quiet)
|
|
258
|
+
|
|
203
259
|
frq, wm, numeric = items_table(cols, items)
|
|
260
|
+
|
|
261
|
+
if form == "bubble":
|
|
262
|
+
# the bubble plot frequency matrix: one row of bubbles per
|
|
263
|
+
# item, one column per response, each bubble sized by its
|
|
264
|
+
# count, the items in the order given from the bottom unless
|
|
265
|
+
# sorted by their mean. R analog: .dpmat.main()
|
|
266
|
+
order = list(items)
|
|
267
|
+
if sort != "0":
|
|
268
|
+
order = list(wm.sort_values(ascending=(sort == "+"),
|
|
269
|
+
kind="stable").index)
|
|
270
|
+
_set_stats(ChartStats(n_dim=2, freq=frq[order].T,
|
|
271
|
+
mean=wm[order]),
|
|
272
|
+
items_stats_lines(
|
|
273
|
+
frq[order], wm[order],
|
|
274
|
+
"Frequencies of Responses by Variable", numeric,
|
|
275
|
+
with_sum=True), quiet)
|
|
276
|
+
# the matrix lists its first row at the top, so reverse the
|
|
277
|
+
# order to draw the first item at the bottom, as R does
|
|
278
|
+
mat = frq[order[::-1]].T.astype(float)
|
|
279
|
+
mat.index.name, mat.columns.name = "Item", "Response"
|
|
280
|
+
cut = power / 2.5 * float(np.nanmax(mat.to_numpy()))
|
|
281
|
+
if fill is None:
|
|
282
|
+
fill = get_option("bar_fill_cont", "#96AAC3")
|
|
283
|
+
return bubble_plotly(
|
|
284
|
+
mat, x_name="Response", y_name="Count", by_name="Item",
|
|
285
|
+
x_lab="" if xlab is None else xlab,
|
|
286
|
+
y_lab="" if ylab is None else ylab,
|
|
287
|
+
main=main or None,
|
|
288
|
+
fill=(list(fill)[::-1] if isinstance(fill, (list, tuple))
|
|
289
|
+
else [fill] * len(order)),
|
|
290
|
+
border=get_option("pt_color", "#324E5C"),
|
|
291
|
+
opacity=0.6, power=power, radius=radius, digits_d=0,
|
|
292
|
+
labels="input" if labels is None else labels,
|
|
293
|
+
labels_color=get_option("bubble_text_color", "#F7F2E6"),
|
|
294
|
+
label_min_value=cut)
|
|
204
295
|
fill_use = (fill if isinstance(fill, (list, tuple))
|
|
205
296
|
else [fill] * len(frq.index) if fill is not None
|
|
206
297
|
else items_fill(theme, len(frq.index)))
|
|
@@ -217,10 +308,11 @@ def _chart_items(data, items, y, by, facet, form, stat, sort,
|
|
|
217
308
|
if sort != "0":
|
|
218
309
|
order = list(wm.sort_values(ascending=(sort == "+"),
|
|
219
310
|
kind="stable").index)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
311
|
+
# the table lists the items top of the chart first
|
|
312
|
+
_set_stats(ChartStats(n_dim=2, freq=frq[order[::-1]].T,
|
|
313
|
+
mean=wm[order[::-1]]),
|
|
314
|
+
_items_print(frq[order[::-1]], wm[order[::-1]],
|
|
315
|
+
numeric), quiet)
|
|
224
316
|
return bc_items_plotly(
|
|
225
317
|
frq, order, fill_use,
|
|
226
318
|
labels="%" if labels is None else labels,
|
|
@@ -230,7 +322,7 @@ def _chart_items(data, items, y, by, facet, form, stat, sort,
|
|
|
230
322
|
y_lab="" if ylab is None else ylab,
|
|
231
323
|
legend_title=leg, main=main or None,
|
|
232
324
|
axis_fmt=axis_fmt, axis_x_pre=axis_x_pre,
|
|
233
|
-
rotate_x=rotate_x, rotate_y=rotate_y)
|
|
325
|
+
rotate_x=rotate_x, rotate_y=rotate_y, horiz=horiz)
|
|
234
326
|
|
|
235
327
|
# faceted: a panel is a fraction of the width of the single
|
|
236
328
|
# chart, too narrow for segment labels, and the paneled chart
|
|
@@ -262,24 +354,27 @@ def _chart_items(data, items, y, by, facet, form, stat, sort,
|
|
|
262
354
|
facet_order = _category_order(fac[keep])
|
|
263
355
|
resp = list(frq.index)
|
|
264
356
|
tbls = {}
|
|
357
|
+
rep_lines, panels = [], {}
|
|
265
358
|
for lv in facet_order:
|
|
266
359
|
m = keep & (fac == lv)
|
|
267
360
|
f_lv, w_lv, _ = items_table([c[m] for c in cols], items) \
|
|
268
361
|
if all(c[m].notna().any() for c in cols) else (
|
|
269
362
|
pd.DataFrame(0, index=resp, columns=items), None, None)
|
|
270
363
|
f_lv = f_lv.reindex(index=resp, fill_value=0)
|
|
271
|
-
if
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
364
|
+
if w_lv is None:
|
|
365
|
+
w_lv = pd.Series(np.nan, index=items)
|
|
366
|
+
rep_lines += _items_print(
|
|
367
|
+
f_lv, w_lv, numeric,
|
|
368
|
+
f"Frequencies of Responses by Variable, "
|
|
369
|
+
f"{facet_name}: {lv}")
|
|
370
|
+
panels[lv] = ChartStats(freq=f_lv.T, mean=w_lv)
|
|
278
371
|
t = f_lv.astype(float)
|
|
279
372
|
if stack100:
|
|
280
373
|
tot = t.sum(axis=0)
|
|
281
374
|
t = t.div(tot.where(tot != 0), axis=1).fillna(0)
|
|
282
|
-
|
|
375
|
+
# first item at the top, or at the left of a vertical chart
|
|
376
|
+
tbls[lv] = t[items[::-1]] if horiz else t[items]
|
|
377
|
+
_set_stats(ChartStats(n_dim=3, panels=panels), rep_lines, quiet)
|
|
283
378
|
tbl = pd.DataFrame([tbls[lv].sum(axis=0) for lv in facet_order],
|
|
284
379
|
index=facet_order)
|
|
285
380
|
tbl.index.name = facet_name
|
|
@@ -296,7 +391,86 @@ def _chart_items(data, items, y, by, facet, form, stat, sort,
|
|
|
296
391
|
rotate_x=rotate_x, rotate_y=rotate_y, main=main,
|
|
297
392
|
by_tbls=tbls, by_name=leg, beside=False,
|
|
298
393
|
val_name=("Proportion" if stack100 else "Count"),
|
|
299
|
-
legend_title=leg)
|
|
394
|
+
legend_title=leg, horiz=horiz)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _chart_per_variable(data, items, cols, fill, xlab, main, quiet):
|
|
398
|
+
"""One bar chart of counts per categorical variable, as panels of
|
|
399
|
+
one figure, each followed at the console by its frequencies and
|
|
400
|
+
chi-square test. Numeric variables are passed over, as are a
|
|
401
|
+
variable that identifies each case and one with a single value.
|
|
402
|
+
R analog: bc.data.frame()"""
|
|
403
|
+
from plotly.subplots import make_subplots
|
|
404
|
+
import plotly.graph_objects as go
|
|
405
|
+
from .plotly_utils import (
|
|
406
|
+
as_plotly_color, BASE_COLORS, auto_text_color,
|
|
407
|
+
axis_format, plotly_style, to_hex)
|
|
408
|
+
from .stats_out import _counts_1d
|
|
409
|
+
show = not resolve_quiet(quiet)
|
|
410
|
+
keep = []
|
|
411
|
+
for nm, c in zip(items, cols):
|
|
412
|
+
if is_numeric_dtype(c) and not isinstance(
|
|
413
|
+
c.dtype, pd.CategoricalDtype):
|
|
414
|
+
continue # R: .is.num.cat(x, 0) FALSE
|
|
415
|
+
v = c.dropna()
|
|
416
|
+
if v.nunique() >= len(v): # identifies each case
|
|
417
|
+
continue
|
|
418
|
+
if v.nunique() == 1:
|
|
419
|
+
if show:
|
|
420
|
+
print(f"\nVariable {nm} has only one value.\n"
|
|
421
|
+
" No bar chart produced.\n")
|
|
422
|
+
continue
|
|
423
|
+
keep.append((nm, c))
|
|
424
|
+
if not keep:
|
|
425
|
+
raise ValueError(
|
|
426
|
+
"No categorical variables, so no bar charts.\n\n"
|
|
427
|
+
"If you have integer variables that are categorical, "
|
|
428
|
+
"convert them to pandas Categorical, e.g.\n"
|
|
429
|
+
" d['Plan'] = d['Plan'].astype('category')")
|
|
430
|
+
|
|
431
|
+
n = len(keep)
|
|
432
|
+
n_col = min(3, n)
|
|
433
|
+
n_row = math.ceil(n / n_col)
|
|
434
|
+
fig = make_subplots(rows=n_row, cols=n_col,
|
|
435
|
+
subplot_titles=[nm for nm, _ in keep],
|
|
436
|
+
horizontal_spacing=0.08,
|
|
437
|
+
vertical_spacing=max(0.08, 0.3 / n_row))
|
|
438
|
+
st = plotly_style()
|
|
439
|
+
rep_lines, per_var = [], {}
|
|
440
|
+
for k, (nm, c) in enumerate(keep):
|
|
441
|
+
rep_lines += _counts_1d(c.dropna(), nm,
|
|
442
|
+
int(c.isna().sum())) + [""]
|
|
443
|
+
per_var[nm] = chart_stats_data(c.dropna(), None, None, None,
|
|
444
|
+
n_miss=int(c.isna().sum()))
|
|
445
|
+
order = _category_order(c.dropna())
|
|
446
|
+
cnt = c.value_counts().reindex(order, fill_value=0)
|
|
447
|
+
f = (fill if isinstance(fill, (list, tuple))
|
|
448
|
+
else [fill] if fill is not None else BASE_COLORS)
|
|
449
|
+
cols_k = [to_hex(f[i % len(f)]) for i in range(len(cnt))]
|
|
450
|
+
r, cc = k // n_col + 1, k % n_col + 1
|
|
451
|
+
fig.add_trace(go.Bar(
|
|
452
|
+
x=[str(v) for v in cnt.index], y=cnt.to_numpy(),
|
|
453
|
+
marker=dict(color=cols_k), text=[str(v) for v in cnt],
|
|
454
|
+
textposition="inside", showlegend=False,
|
|
455
|
+
textfont=dict(color=auto_text_color(cols_k)),
|
|
456
|
+
hovertemplate=f"{nm}: %{{x}}<br>Count: %{{y}}"
|
|
457
|
+
"<extra></extra>"), row=r, col=cc)
|
|
458
|
+
ticks = pretty(0.0, float(cnt.max()))
|
|
459
|
+
fig.update_yaxes(row=r, col=cc, tickvals=ticks,
|
|
460
|
+
ticktext=axis_format(ticks, 0),
|
|
461
|
+
range=[0, ticks[-1] * 1.04],
|
|
462
|
+
gridcolor="#E5E5E5",
|
|
463
|
+
title_text="Count" if cc == 1 else "")
|
|
464
|
+
fig.update_xaxes(row=r, col=cc, type="category",
|
|
465
|
+
title_text=xlab or "")
|
|
466
|
+
_set_stats(ChartStats(n_dim=1, variables=per_var), rep_lines,
|
|
467
|
+
quiet)
|
|
468
|
+
fig.update_layout(
|
|
469
|
+
template=None, height=140 + 260 * n_row,
|
|
470
|
+
plot_bgcolor=as_plotly_color(st["panel_fill"]),
|
|
471
|
+
paper_bgcolor=as_plotly_color(st["window_fill"]),
|
|
472
|
+
title=(dict(text=main, x=0.5) if main else None))
|
|
473
|
+
return fig
|
|
300
474
|
|
|
301
475
|
|
|
302
476
|
def _items_print(frq, wm, numeric, title=None):
|
|
@@ -305,6 +479,15 @@ def _items_print(frq, wm, numeric, title=None):
|
|
|
305
479
|
numeric)
|
|
306
480
|
|
|
307
481
|
|
|
482
|
+
def _set_stats(st, lines, quiet):
|
|
483
|
+
"""Keep the statistics of this call, with the report as their
|
|
484
|
+
text, and display the report unless quiet."""
|
|
485
|
+
st["text"] = "\n".join(lines)
|
|
486
|
+
CALL_STATS.set(st)
|
|
487
|
+
if not resolve_quiet(quiet):
|
|
488
|
+
print(st["text"])
|
|
489
|
+
|
|
490
|
+
|
|
308
491
|
def _profile_fill(theme):
|
|
309
492
|
"""One hue per series from the theme, as R's profile does:
|
|
310
493
|
.color_range(.get_fill(theme), n), the qualitative hues for the
|
|
@@ -352,7 +535,7 @@ def _dot_diff_lines(cats, ydf, x_name):
|
|
|
352
535
|
|
|
353
536
|
|
|
354
537
|
def _dot_paired(cats, ydf, sort, sort_miss, origin_x, show_diff,
|
|
355
|
-
x_name, **render):
|
|
538
|
+
x_name, quiet=None, **render):
|
|
356
539
|
"""Order a multi-series dot plot and draw it. Two series are a
|
|
357
540
|
pair, read for the gap between them, so by default they are
|
|
358
541
|
ordered by that difference, ascending so the largest positive
|
|
@@ -370,8 +553,23 @@ def _dot_paired(cats, ydf, sort, sort_miss, origin_x, show_diff,
|
|
|
370
553
|
kind="stable")
|
|
371
554
|
cats = [cats[i] for i in order]
|
|
372
555
|
ydf = ydf.iloc[order]
|
|
373
|
-
if dif_pair
|
|
374
|
-
|
|
556
|
+
if dif_pair:
|
|
557
|
+
# the difference the display presents, kept with the figure; it
|
|
558
|
+
# is reported where R lists it (show_diff), unless quiet
|
|
559
|
+
dif = pd.Series(
|
|
560
|
+
(ydf.iloc[:, 1] - ydf.iloc[:, 0]).to_numpy(dtype=float),
|
|
561
|
+
index=cats, name=f"{ydf.columns[1]} - {ydf.columns[0]}")
|
|
562
|
+
st = CALL_STATS.get()
|
|
563
|
+
if st is None:
|
|
564
|
+
st = ChartStats(n_dim=2, values=ydf.set_axis(cats), text="")
|
|
565
|
+
CALL_STATS.set(st)
|
|
566
|
+
st["diff"] = dif
|
|
567
|
+
if show_diff:
|
|
568
|
+
lines = _dot_diff_lines(cats, ydf, x_name)
|
|
569
|
+
st["text"] = "\n".join(
|
|
570
|
+
[t for t in (st.get("text", ""), "\n".join(lines)) if t])
|
|
571
|
+
if not resolve_quiet(quiet):
|
|
572
|
+
print("\n".join(lines))
|
|
375
573
|
origin, gridT = _dot_origin_grid(
|
|
376
574
|
ydf.to_numpy(dtype=float).ravel(),
|
|
377
575
|
origin_in=0 if (dif_pair and origin_x is None) else origin_x)
|
|
@@ -396,7 +594,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
396
594
|
theme=None,
|
|
397
595
|
labels=None, labels_position=None,
|
|
398
596
|
labels_color=None, labels_size=None, labels_decimals=None,
|
|
399
|
-
labels_cut=None, segments=None,
|
|
597
|
+
labels_cut=None, segments=None, one_plot=None,
|
|
400
598
|
legend_title=None, legend_position=None,
|
|
401
599
|
legend_labels=None, legend_horiz=False,
|
|
402
600
|
legend_size=None, legend_abbrev=None, legend_adjust=0,
|
|
@@ -453,6 +651,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
453
651
|
|
|
454
652
|
Variables are strings naming columns of the DataFrame `data`.
|
|
455
653
|
Returns a plotly Figure; call .show() to display from a script.
|
|
654
|
+
The statistics behind the chart come with it as fig.stats, read
|
|
655
|
+
as attributes or by name: fig.stats.freq, fig.stats.p_value,
|
|
656
|
+
fig.stats.text (the console report, kept when quiet=True), named
|
|
657
|
+
as R's Chart() names its returned list.
|
|
456
658
|
"""
|
|
457
659
|
|
|
458
660
|
# ----- validate parameters ------------------------------------
|
|
@@ -566,6 +768,13 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
566
768
|
raise ValueError('labels_position must be "in" or "out"')
|
|
567
769
|
if axis_fmt not in ("K", ",", ".", ""):
|
|
568
770
|
raise ValueError('axis_fmt must be "K", ",", "." or ""')
|
|
771
|
+
# proportions of a bar chart with by are taken within each bar,
|
|
772
|
+
# the 100% stacked chart. R analog: .bc.main()
|
|
773
|
+
# if (!is.null(by) && prop) stack100 <- TRUE
|
|
774
|
+
if (stat_x == "proportion" and by is not None and form == "bar"
|
|
775
|
+
and facet is None and y is None):
|
|
776
|
+
stack100 = True
|
|
777
|
+
stat_x = "count"
|
|
569
778
|
if stack100:
|
|
570
779
|
if form != "bar":
|
|
571
780
|
raise ValueError('stack100 rescales the bars of a bar '
|
|
@@ -662,9 +871,6 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
662
871
|
if sort != "0":
|
|
663
872
|
raise ValueError("Sort not applicable to Trellis "
|
|
664
873
|
"(faceted bar) charts")
|
|
665
|
-
if stat_x == "proportion" and by is not None:
|
|
666
|
-
raise NotImplementedError(
|
|
667
|
-
'stat_x="proportion" with by= is not yet ported')
|
|
668
874
|
|
|
669
875
|
# ----- resolve variables and filter ---------------------------
|
|
670
876
|
if filter is not None:
|
|
@@ -692,7 +898,8 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
692
898
|
stat=stat, sort=sort, sort_miss=sort_miss,
|
|
693
899
|
stack100=stack100, horiz=horiz, fill=fill, theme=theme,
|
|
694
900
|
labels=labels, labels_size=labels_size, gap=gap,
|
|
695
|
-
labels_cut=labels_cut,
|
|
901
|
+
labels_cut=labels_cut, power=power, radius=radius,
|
|
902
|
+
one_plot=one_plot,
|
|
696
903
|
xlab=xlab, ylab=ylab, main=main,
|
|
697
904
|
legend_title=legend_title, n_col=n_col, n_row=n_row,
|
|
698
905
|
axis_fmt=axis_fmt, axis_x_pre=axis_x_pre,
|
|
@@ -737,7 +944,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
737
944
|
x_lab_dot = (xlab if xlab else ylab if ylab else val_lab)
|
|
738
945
|
return _dot_paired(
|
|
739
946
|
cats, ydf.reset_index(drop=True), sort, sort_miss,
|
|
740
|
-
origin_x, show_diff=
|
|
947
|
+
origin_x, show_diff=True, quiet=quiet, x_name=x,
|
|
741
948
|
fill=fill, border=color, pt_size=pt_size,
|
|
742
949
|
x_lab=x_lab_dot, y_lab=x,
|
|
743
950
|
digits_d=2 if digits_d is None else digits_d,
|
|
@@ -814,12 +1021,53 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
814
1021
|
facet_order = (_category_order(facet_ser)
|
|
815
1022
|
if facet_ser is not None else None)
|
|
816
1023
|
|
|
817
|
-
# theme
|
|
818
|
-
#
|
|
819
|
-
if theme is
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1024
|
+
# a theme set by style(), or by this call's theme=, which the
|
|
1025
|
+
# wrapper has applied for the call
|
|
1026
|
+
if theme is None:
|
|
1027
|
+
theme = get_option("theme")
|
|
1028
|
+
|
|
1029
|
+
# an ordered category is ordinal, so its default fill is the
|
|
1030
|
+
# theme's sequential palette, light to dark in the order of the
|
|
1031
|
+
# levels, not distinct hues. Fill encodes x without by and by
|
|
1032
|
+
# with it, so the ordering of that variable is the one that
|
|
1033
|
+
# applies. R analog: Chart.R is.ordered() fill
|
|
1034
|
+
if (form == "bar" and fill is None and facet_ser is None
|
|
1035
|
+
and fill_split is None and not fill_scaled):
|
|
1036
|
+
enc = by_ser if by_ser is not None else x_ser
|
|
1037
|
+
if (isinstance(enc.dtype, pd.CategoricalDtype)
|
|
1038
|
+
and enc.cat.ordered):
|
|
1039
|
+
fill = _color_range(
|
|
1040
|
+
_THEME_PALETTE.get(theme, "blues"),
|
|
1041
|
+
len(by_order) if by_order is not None
|
|
1042
|
+
else len(x_order))
|
|
1043
|
+
|
|
1044
|
+
# theme=: a bar chart without by is drawn in the theme's single
|
|
1045
|
+
# bar color, which fill_scaled then varies in luminance; with
|
|
1046
|
+
# by, the levels span the theme's sequential palette, darkest
|
|
1047
|
+
# first. The default theme keeps the qualitative hues.
|
|
1048
|
+
# R analog: Chart.R fill block (theme.seq) and .bc.main()
|
|
1049
|
+
if (theme is not None and fill is None
|
|
1050
|
+
and theme not in ("colors", "hues")):
|
|
1051
|
+
if theme not in _THEME_PALETTE:
|
|
1052
|
+
raise ValueError(
|
|
1053
|
+
f"unknown theme '{theme}'; one of: "
|
|
1054
|
+
f"{', '.join(sorted(_THEME_PALETTE))}")
|
|
1055
|
+
if by_order is not None:
|
|
1056
|
+
fill = list(reversed(_color_range(_THEME_PALETTE[theme],
|
|
1057
|
+
len(by_order))))
|
|
1058
|
+
elif form == "bar":
|
|
1059
|
+
fill = [_THEME_BAR_FILL.get(theme, "#595959E6")] \
|
|
1060
|
+
* len(x_order)
|
|
1061
|
+
else:
|
|
1062
|
+
fill = _theme_fill(theme, len(x_order))
|
|
1063
|
+
|
|
1064
|
+
# a palette name in fill, such as "reds", names a range of colors,
|
|
1065
|
+
# one per bar, or per level of by. R analog: .bc.main()
|
|
1066
|
+
# .color_range(fill, n.levels)
|
|
1067
|
+
if (form == "bar" and isinstance(fill, str) and not fill_scaled
|
|
1068
|
+
and fill in _palette_names()):
|
|
1069
|
+
fill = _color_range(fill, len(by_order) if by_order is not None
|
|
1070
|
+
else len(x_order))
|
|
823
1071
|
|
|
824
1072
|
# explicit panel-grid layout: n_col wins, else derive from
|
|
825
1073
|
# n_row; None keeps each renderer's own default (single-column
|
|
@@ -915,35 +1163,50 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
915
1163
|
f"have a numerical variable y='{y}', so specify "
|
|
916
1164
|
'stat to define the aggregation, e.g., stat="mean"')
|
|
917
1165
|
|
|
918
|
-
# accompanying statistics: frequencies, or the stat of y
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1166
|
+
# accompanying statistics: frequencies, or the stat of y. They are
|
|
1167
|
+
# computed whether or not displayed, and returned with the figure
|
|
1168
|
+
# as fig.stats, as R's Chart() returns its list
|
|
1169
|
+
y_out = (y_ser if y_ser is not None
|
|
1170
|
+
and stat in STAT_FUN else None)
|
|
1171
|
+
if by_more_sers and (y_ser is None or y_out is not None):
|
|
1172
|
+
from .stats_out import nested_stats
|
|
1173
|
+
lines = nested_stats(
|
|
1174
|
+
[x_ser, by_ser] + by_more_sers, [x, by] + by_more,
|
|
1175
|
+
y_out, stat, y, 2 if digits_d is None else digits_d)
|
|
1176
|
+
cols_n = [x_ser, by_ser] + by_more_sers
|
|
1177
|
+
df_n = pd.DataFrame({nm: c for nm, c in
|
|
1178
|
+
zip([x, by] + by_more, cols_n)})
|
|
1179
|
+
_set_stats(ChartStats(
|
|
1180
|
+
n_dim=len(cols_n),
|
|
1181
|
+
freq=df_n.groupby([x, by] + by_more, observed=True).size()
|
|
1182
|
+
if y_out is None else None),
|
|
1183
|
+
lines, quiet)
|
|
1184
|
+
elif y_ser is None or y_out is not None:
|
|
1185
|
+
var_lbl = data.attrs.get("variable_labels", {}) or {}
|
|
1186
|
+
lines = chart_stats(
|
|
1187
|
+
x_ser, by_ser, y_out, stat, x, by, y,
|
|
1188
|
+
2 if digits_d is None else digits_d,
|
|
1189
|
+
stack100=stack100, n_miss=n_miss_x,
|
|
1190
|
+
x_lbl=var_lbl.get(x), y_lbl=var_lbl.get(y),
|
|
1191
|
+
facet_ser=facet_ser, facet_name=facet_name,
|
|
1192
|
+
form=form)
|
|
1193
|
+
_set_stats(chart_stats_data(x_ser, by_ser, y_out, stat,
|
|
1194
|
+
facet_ser=facet_ser,
|
|
1195
|
+
n_miss=n_miss_x),
|
|
1196
|
+
lines, quiet)
|
|
1197
|
+
|
|
1198
|
+
# labels default for aggregated data, the value of the statistic;
|
|
1199
|
+
# bars of counts are labeled with their percentages, as lessR's
|
|
1200
|
+
# do (getOption("values"), "%"): of the total, or of each bar for
|
|
1201
|
+
# the 100% stacked chart. R analog: Chart.R ~716, .bc.main()
|
|
941
1202
|
if labels is None and (stat is not None or
|
|
942
1203
|
(is_agg and y_ser is not None)):
|
|
943
1204
|
# the bars carry a statistic, so the label is its value;
|
|
944
1205
|
# beside only arranges the bars, and a percentage of a sum
|
|
945
1206
|
# of means is not a quantity
|
|
946
1207
|
labels = "input"
|
|
1208
|
+
elif labels is None and form == "bar" and y_ser is None:
|
|
1209
|
+
labels = "%"
|
|
947
1210
|
|
|
948
1211
|
# ----- hierarchical forms aggregate per nesting level ---------
|
|
949
1212
|
# from the raw columns, not from the flat table built below
|
|
@@ -962,6 +1225,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
962
1225
|
facet_order=facet_order)
|
|
963
1226
|
fill_vec = hier_color_resolve(x_order, fill, x=x_ser,
|
|
964
1227
|
y=y_ser, stat=stat)
|
|
1228
|
+
fill_vec_byfac = (None if facet_ser is None else
|
|
1229
|
+
hier_color_resolve_byfac(
|
|
1230
|
+
x_order, fill, x_ser, y_ser, stat,
|
|
1231
|
+
facet_ser, facet_order))
|
|
965
1232
|
if main is None:
|
|
966
1233
|
main = build_title(x, by_name=by_label, y_name=y,
|
|
967
1234
|
stat=stat, facet_name=facet_name)
|
|
@@ -974,6 +1241,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
974
1241
|
# keep the inner rings meaningful.
|
|
975
1242
|
return hier_plotly(
|
|
976
1243
|
agg, fill_vec, type=hier_type,
|
|
1244
|
+
fill_vec_byfac=fill_vec_byfac,
|
|
977
1245
|
x_name=x, by_name=by, facet_name=facet_name,
|
|
978
1246
|
main=main, border=color, digits_d=digits_d,
|
|
979
1247
|
labels="%" if labels is None else labels,
|
|
@@ -1021,6 +1289,11 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1021
1289
|
columns=x_order)
|
|
1022
1290
|
if y_ser is None:
|
|
1023
1291
|
t = t.fillna(0)
|
|
1292
|
+
if stat_x == "proportion" and not stack100:
|
|
1293
|
+
# every cell of the panel as a share of the panel,
|
|
1294
|
+
# as lattice's prop.table(margin=panel)
|
|
1295
|
+
tot = t.to_numpy().sum()
|
|
1296
|
+
t = t / tot if tot > 0 else t
|
|
1024
1297
|
if stack100:
|
|
1025
1298
|
# each bar scaled to its own total compares
|
|
1026
1299
|
# composition rather than magnitude
|
|
@@ -1048,7 +1321,9 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1048
1321
|
elif y_ser is not None:
|
|
1049
1322
|
val_name = y
|
|
1050
1323
|
elif by_ser is not None:
|
|
1051
|
-
|
|
1324
|
+
# named for x, as lattice's "Count of" / "Proportion of"
|
|
1325
|
+
val_name = ("Proportion of " if stat_x == "proportion"
|
|
1326
|
+
else "Count of ") + x
|
|
1052
1327
|
else:
|
|
1053
1328
|
val_name = None # Count/Proportion of x
|
|
1054
1329
|
return bc_facet_plotly(
|
|
@@ -1070,9 +1345,6 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1070
1345
|
)
|
|
1071
1346
|
|
|
1072
1347
|
if facet_ser is not None and form == "radar":
|
|
1073
|
-
if stat_x == "proportion" and by_ser is not None:
|
|
1074
|
-
raise NotImplementedError(
|
|
1075
|
-
'stat_x="proportion" with by= is not yet ported')
|
|
1076
1348
|
facets = {
|
|
1077
1349
|
str(lv): _facet_table(
|
|
1078
1350
|
x_ser[facet_ser == lv],
|
|
@@ -1174,6 +1446,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1174
1446
|
g = df.groupby(["f", "g", "x"], observed=True)["y"]
|
|
1175
1447
|
t = (g.sum() if y_ser is None
|
|
1176
1448
|
else STAT_FUN[stat](g) if agg_y else g.mean())
|
|
1449
|
+
if y_ser is None and stat_x == "proportion":
|
|
1450
|
+
# each series is its group's distribution over x in that
|
|
1451
|
+
# panel; without by, the panel's own distribution
|
|
1452
|
+
t = t / t.groupby(level=["f", "g"]).transform("sum")
|
|
1177
1453
|
cells = {}
|
|
1178
1454
|
for (f, gg, xx), v in t.items():
|
|
1179
1455
|
nm = None if by_ser is None else gg
|
|
@@ -1194,8 +1470,12 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1194
1470
|
digits_d = 0 if y_ser is None else 2
|
|
1195
1471
|
from .plotly_utils import axis_format
|
|
1196
1472
|
y_lab = (ylab if ylab is not None
|
|
1473
|
+
else f"Proportion of {x}" if (y_ser is None
|
|
1474
|
+
and stat_x == "proportion")
|
|
1197
1475
|
else "Count" if y_ser is None
|
|
1198
1476
|
else f"{STAT_LBL[stat]} of {y}" if agg_y else y)
|
|
1477
|
+
if y_ser is None and stat_x == "proportion" and digits_d == 0:
|
|
1478
|
+
digits_d = 2
|
|
1199
1479
|
return profile_facet_plotly(
|
|
1200
1480
|
cells, x_lv, series, y_tick,
|
|
1201
1481
|
axis_format(y_tick, digits_d, axis_fmt, axis_y_pre),
|
|
@@ -1220,9 +1500,6 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1220
1500
|
agg_y = not is_agg and stat is not None
|
|
1221
1501
|
cats_l, vals_l, fac_l = [], [], []
|
|
1222
1502
|
if by_ser is not None:
|
|
1223
|
-
if prop:
|
|
1224
|
-
raise ValueError(
|
|
1225
|
-
'stat_x="proportion" with by= is not yet ported')
|
|
1226
1503
|
rows = []
|
|
1227
1504
|
for lv in facet_order:
|
|
1228
1505
|
m = facet_ser == lv
|
|
@@ -1236,6 +1513,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1236
1513
|
wide = (t.unstack("by")
|
|
1237
1514
|
.reindex(index=[str(c) for c in x_order],
|
|
1238
1515
|
columns=[str(b) for b in by_order]))
|
|
1516
|
+
if prop and y_ser is None:
|
|
1517
|
+
# each group's distribution over x in this panel
|
|
1518
|
+
tot = wide.sum(axis=0)
|
|
1519
|
+
wide = wide.div(tot.where(tot != 0), axis=1)
|
|
1239
1520
|
rows.append(wide)
|
|
1240
1521
|
cats_l += list(wide.index)
|
|
1241
1522
|
fac_l += [str(lv)] * len(wide)
|
|
@@ -1266,8 +1547,8 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1266
1547
|
# an aggregated value is labeled by its statistic, as the
|
|
1267
1548
|
# unfaceted chart is: "Mean of Salary", not "Salary"
|
|
1268
1549
|
if y_ser is None:
|
|
1269
|
-
val_lab = ("
|
|
1270
|
-
else
|
|
1550
|
+
val_lab = (f"Proportion of {x}" if prop
|
|
1551
|
+
else "Count" if by_ser is not None
|
|
1271
1552
|
else f"Count of {x}")
|
|
1272
1553
|
else:
|
|
1273
1554
|
val_lab = f"{STAT_LBL[stat]} of {y}" if agg_y else y
|
|
@@ -1317,9 +1598,6 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1317
1598
|
|
|
1318
1599
|
# ----- build the table the renderer receives ------------------
|
|
1319
1600
|
if y_ser is None: # counts of x
|
|
1320
|
-
if stat_x == "proportion" and by_ser is not None:
|
|
1321
|
-
raise NotImplementedError(
|
|
1322
|
-
'stat_x="proportion" with by= is not yet ported')
|
|
1323
1601
|
|
|
1324
1602
|
if by_ser is None:
|
|
1325
1603
|
tbl = (x_ser.groupby(x_ser).size()
|
|
@@ -1339,11 +1617,20 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1339
1617
|
tbl = pd.crosstab(by_ser, x_ser)
|
|
1340
1618
|
tbl = tbl.reindex(index=by_order, columns=x_order,
|
|
1341
1619
|
fill_value=0)
|
|
1342
|
-
|
|
1620
|
+
if stat_x == "proportion":
|
|
1621
|
+
# each group's distribution over x, so the groups are
|
|
1622
|
+
# compared whatever their sizes: every by row sums
|
|
1623
|
+
# to 1 (the bar chart took its own path above, the
|
|
1624
|
+
# 100% stacked chart within each bar)
|
|
1625
|
+
tot = tbl.sum(axis=1)
|
|
1626
|
+
tbl = tbl.div(tot.where(tot != 0), axis=0).fillna(0)
|
|
1627
|
+
y_name = "Proportion"
|
|
1628
|
+
else:
|
|
1629
|
+
y_name = "Count"
|
|
1343
1630
|
if ylab is None:
|
|
1344
|
-
ylab = f"
|
|
1631
|
+
ylab = f"{y_name} of {x}"
|
|
1345
1632
|
if digits_d is None:
|
|
1346
|
-
digits_d = 0
|
|
1633
|
+
digits_d = 0 if stat_x == "count" else 2
|
|
1347
1634
|
|
|
1348
1635
|
elif stat == "deviation": # R analog: .agg()
|
|
1349
1636
|
if by_ser is not None:
|
|
@@ -1468,6 +1755,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1468
1755
|
labels_decimals=labels_decimals,
|
|
1469
1756
|
labels_size=0.90 if labels_size is None
|
|
1470
1757
|
else labels_size,
|
|
1758
|
+
shares=(y_ser is None and stat_x == "proportion"),
|
|
1471
1759
|
)
|
|
1472
1760
|
|
|
1473
1761
|
if form == "radar":
|
|
@@ -1564,11 +1852,13 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1564
1852
|
ydf.columns = [str(c) for c in ydf.columns]
|
|
1565
1853
|
cats = [str(c) for c in ydf.index]
|
|
1566
1854
|
val_lab = (xlab if xlab else ylab_user if ylab_user
|
|
1567
|
-
else
|
|
1855
|
+
else ylab if (y_ser is not None
|
|
1856
|
+
or stat_x == "proportion")
|
|
1857
|
+
else "Count")
|
|
1568
1858
|
return _dot_paired(
|
|
1569
1859
|
cats, ydf.reset_index(drop=True), sort, sort_miss,
|
|
1570
1860
|
origin_x,
|
|
1571
|
-
show_diff=
|
|
1861
|
+
show_diff=y_ser is None, quiet=quiet,
|
|
1572
1862
|
x_name=x,
|
|
1573
1863
|
fill=fill, border=color, pt_size=pt_size,
|
|
1574
1864
|
x_lab=val_lab, y_lab=x, digits_d=digits_d,
|
|
@@ -1627,6 +1917,26 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1627
1917
|
facet_name=facet_ttl, y_name=y_name, stat=stat)
|
|
1628
1918
|
elif main == "":
|
|
1629
1919
|
main = None
|
|
1920
|
+
# a palette name in fill names the palette, not a color, as
|
|
1921
|
+
# for the bar chart. A sequential palette shades each slice
|
|
1922
|
+
# by its value off the palette's ramp, larger darker, as
|
|
1923
|
+
# for the treemap. R analog: Chart.R pie palette branch
|
|
1924
|
+
if isinstance(fill, str) and fill in _palette_names():
|
|
1925
|
+
from .hier_plotly import _HIER_SEQ
|
|
1926
|
+
slices = list(tbl.columns if isinstance(tbl, pd.DataFrame)
|
|
1927
|
+
else tbl.index)
|
|
1928
|
+
vals = (tbl.sum(axis=0) if isinstance(tbl, pd.DataFrame)
|
|
1929
|
+
else tbl).to_numpy(dtype=float)
|
|
1930
|
+
if (fill.lower() in _HIER_SEQ and len(slices) > 1
|
|
1931
|
+
and np.ptp(vals) > 0):
|
|
1932
|
+
from .getColors import getColors
|
|
1933
|
+
ramp = [c[:7] for c in getColors(fill.lower(), n=256,
|
|
1934
|
+
quiet=True)]
|
|
1935
|
+
pos = (vals - vals.min()) / np.ptp(vals)
|
|
1936
|
+
fill = [ramp[i] for i in
|
|
1937
|
+
np.round(pos * (len(ramp) - 1)).astype(int)]
|
|
1938
|
+
else:
|
|
1939
|
+
fill = _color_range(fill, len(slices))
|
|
1630
1940
|
return pie_plotly(
|
|
1631
1941
|
tbl,
|
|
1632
1942
|
x_name=x, y_name=y_name, by_name=by, main=main,
|
|
@@ -1650,8 +1960,16 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1650
1960
|
if (fill_split is not None or fill_scaled) \
|
|
1651
1961
|
and isinstance(tbl, pd.Series):
|
|
1652
1962
|
from .getColors import bar_fill_colors
|
|
1653
|
-
fill
|
|
1654
|
-
|
|
1963
|
+
# one fill repeated over the bars is a single color; more
|
|
1964
|
+
# than two leave the hue to the theme, as R's .bc.main()
|
|
1965
|
+
f_in = fill
|
|
1966
|
+
if isinstance(fill, (list, tuple)):
|
|
1967
|
+
uniq = list(dict.fromkeys(fill))
|
|
1968
|
+
f_in = (uniq[0] if len(uniq) == 1
|
|
1969
|
+
else None if len(uniq) > 2 else uniq)
|
|
1970
|
+
fill = bar_fill_colors(tbl.to_numpy(dtype=float), f_in,
|
|
1971
|
+
fill_split, fill_scaled, fill_chroma,
|
|
1972
|
+
theme=theme)
|
|
1655
1973
|
|
|
1656
1974
|
fig = bc_plotly(
|
|
1657
1975
|
tbl,
|
|
@@ -1687,5 +2005,30 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1687
2005
|
return fig
|
|
1688
2006
|
|
|
1689
2007
|
|
|
2008
|
+
def _with_height(func):
|
|
2009
|
+
"""height= in inches, as lessR's Chart(height=), converted at
|
|
2010
|
+
96 pixels per inch, so pdf_file= also writes that height.
|
|
2011
|
+
None keeps the figure's own height."""
|
|
2012
|
+
import functools
|
|
2013
|
+
|
|
2014
|
+
@functools.wraps(func)
|
|
2015
|
+
def wrapper(*args, height=None, **kwargs):
|
|
2016
|
+
if height is not None and (isinstance(height, bool) or
|
|
2017
|
+
not isinstance(height, (int, float))
|
|
2018
|
+
or height <= 0):
|
|
2019
|
+
raise ValueError(
|
|
2020
|
+
f"height is in inches, a positive number such as "
|
|
2021
|
+
f"height=4, not {height!r}")
|
|
2022
|
+
fig = func(*args, **kwargs)
|
|
2023
|
+
if height is not None and hasattr(fig, "layout"):
|
|
2024
|
+
fig.update_layout(height=round(96 * height))
|
|
2025
|
+
return fig
|
|
2026
|
+
|
|
2027
|
+
return wrapper
|
|
2028
|
+
|
|
2029
|
+
|
|
1690
2030
|
# font_size= scales all text of the returned figure
|
|
1691
|
-
|
|
2031
|
+
from .style import with_theme as _with_theme # noqa: E402
|
|
2032
|
+
from .utils import file_output # noqa: E402
|
|
2033
|
+
Chart = file_output(_with_height(font_scaled(attach_stats(
|
|
2034
|
+
_with_theme(Chart, own_param=True)))))
|