lessPython 0.1.1__py3-none-any.whl → 0.3.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 +5 -5
- lessPy/Chart.py +1088 -154
- lessPy/Correlation.py +14 -3
- lessPy/Flows.py +5 -1
- lessPy/Logit.py +2 -2
- lessPy/Regression.py +6 -4
- lessPy/X.py +280 -43
- lessPy/XY.py +894 -238
- lessPy/__init__.py +12 -2
- lessPy/_themes.py +5007 -0
- lessPy/bc_items_plotly.py +208 -0
- lessPy/bc_plotly.py +116 -36
- lessPy/bubble_plotly.py +17 -2
- lessPy/corCFA.py +3 -3
- lessPy/corProp.py +2 -2
- lessPy/corReflect.py +2 -2
- lessPy/corReorder.py +2 -2
- lessPy/corScree.py +3 -3
- lessPy/data/StockPrice.csv +1401 -1386
- lessPy/details.py +17 -4
- lessPy/dn_plotly.py +66 -46
- lessPy/dot_plotly.py +112 -41
- lessPy/freq_poly_plotly.py +13 -6
- lessPy/getColors.py +253 -21
- lessPy/hier_plotly.py +84 -14
- lessPy/hs_plotly.py +19 -7
- lessPy/pie_plotly.py +17 -7
- lessPy/pivot.py +6 -3
- lessPy/plotly_utils.py +146 -23
- lessPy/plt_mat_plotly.py +74 -2
- lessPy/plt_plotly.py +15 -3
- lessPy/plt_time.py +11 -4
- lessPy/prob_norm.py +3 -3
- lessPy/prob_tcut.py +3 -3
- lessPy/prob_znorm.py +3 -1
- lessPy/profile_plotly.py +121 -0
- lessPy/rcolors.py +461 -0
- lessPy/simCImean.py +3 -3
- lessPy/simCLT.py +3 -3
- lessPy/simFlips.py +3 -3
- lessPy/simMeans.py +3 -3
- lessPy/stats_out.py +639 -38
- lessPy/style.py +160 -0
- lessPy/sunflower_plotly.py +140 -0
- lessPy/ttest.py +5 -5
- lessPy/utils.py +97 -20
- lessPy/vbs_plotly.py +31 -16
- lessPy/x_console.py +763 -0
- {lesspython-0.1.1.dist-info → lesspython-0.3.0.dist-info}/METADATA +9 -3
- lesspython-0.3.0.dist-info/RECORD +89 -0
- {lesspython-0.1.1.dist-info → lesspython-0.3.0.dist-info}/WHEEL +1 -1
- lesspython-0.1.1.dist-info/RECORD +0 -82
- {lesspython-0.1.1.dist-info → lesspython-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {lesspython-0.1.1.dist-info → lesspython-0.3.0.dist-info}/top_level.txt +0 -0
lessPy/Chart.py
CHANGED
|
@@ -28,16 +28,25 @@ import pandas as pd
|
|
|
28
28
|
from pandas.api.types import is_numeric_dtype
|
|
29
29
|
|
|
30
30
|
from .bc_plotly import bc_facet_plotly, bc_plotly
|
|
31
|
+
from .bc_items_plotly import (
|
|
32
|
+
bc_items_plotly, items_fill, items_stats_lines, items_table,
|
|
33
|
+
)
|
|
31
34
|
from .plt_add import plt_add
|
|
32
35
|
from .bubble_plotly import bubble_plotly
|
|
33
36
|
from .dot_plotly import dot_plotly
|
|
34
37
|
from .hier_plotly import (
|
|
35
|
-
hier_aggregate, hier_color_resolve,
|
|
38
|
+
hier_aggregate, hier_color_resolve, hier_color_resolve_byfac,
|
|
39
|
+
hier_plotly,
|
|
36
40
|
)
|
|
37
41
|
from .pie_plotly import pie_plotly
|
|
42
|
+
from .plt_plotly import plt_plotly
|
|
43
|
+
from .profile_plotly import profile_facet_plotly
|
|
38
44
|
from .radar_plotly import radar_plotly
|
|
39
45
|
from .plotly_utils import build_title, font_scaled
|
|
40
|
-
from .stats_out import
|
|
46
|
+
from .stats_out import (
|
|
47
|
+
CALL_STATS, ChartStats, attach_stats, chart_stats,
|
|
48
|
+
chart_stats_data, resolve_quiet,
|
|
49
|
+
)
|
|
41
50
|
from .utils import (
|
|
42
51
|
STAT_FUN, STAT_LBL, category_order as _category_order,
|
|
43
52
|
facet_values, get_column as _get_column, get_option, pretty,
|
|
@@ -57,6 +66,36 @@ _THEME_PALETTE = {
|
|
|
57
66
|
}
|
|
58
67
|
|
|
59
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
|
+
|
|
60
99
|
def _theme_fill(theme, n):
|
|
61
100
|
"""A theme's fill: an n-color sequential palette in the theme's
|
|
62
101
|
hue. R analog: the theme branch of the fill assignment."""
|
|
@@ -75,9 +114,10 @@ def _facet_table(x_s, y_s, by_s, stat, is_agg, x_order, by_order,
|
|
|
75
114
|
"""Frequency or stat table for one facet level, on the global
|
|
76
115
|
category set(s) so panels stay aligned; absent cells fill 0,
|
|
77
116
|
as in R's xtabs. Series without by, DataFrame (by x cats)
|
|
78
|
-
with. proportion normalizes the per-panel counts
|
|
79
|
-
|
|
80
|
-
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."""
|
|
81
121
|
if by_s is None:
|
|
82
122
|
if y_s is None:
|
|
83
123
|
t = x_s.groupby(x_s, observed=True).size() \
|
|
@@ -103,10 +143,15 @@ def _facet_table(x_s, y_s, by_s, stat, is_agg, x_order, by_order,
|
|
|
103
143
|
t = STAT_FUN[stat](
|
|
104
144
|
df.groupby(["by", "x"], observed=True)["y"]
|
|
105
145
|
).unstack("x")
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
152
|
+
|
|
153
|
+
_FORMS = ("bar", "radar", "bubble", "dot", "pie", "icicle", "treemap",
|
|
154
|
+
"profile")
|
|
110
155
|
_STATS = ("mean", "sum", "sd", "deviation", "min", "median", "max")
|
|
111
156
|
_SORTS = ("0", "-", "+")
|
|
112
157
|
|
|
@@ -161,21 +206,394 @@ def _dot_origin_grid(vals, origin_in=None, is_counts=None):
|
|
|
161
206
|
return origin, gridT
|
|
162
207
|
|
|
163
208
|
|
|
209
|
+
def _chart_items(data, items, y, by, facet, form, stat, sort,
|
|
210
|
+
sort_miss, stack100, horiz, fill, theme, labels,
|
|
211
|
+
labels_size, gap, labels_cut, xlab, ylab, main,
|
|
212
|
+
legend_title, power, radius, one_plot,
|
|
213
|
+
n_col, n_row, axis_fmt, axis_x_pre, rotate_x,
|
|
214
|
+
rotate_y, quiet):
|
|
215
|
+
"""The multi-item stacked chart and its faceted form. R analog:
|
|
216
|
+
the multiple-x branch of Chart.R, .bc.main() multi, and
|
|
217
|
+
.bar.stackedLattice()"""
|
|
218
|
+
if by is not None:
|
|
219
|
+
raise ValueError(
|
|
220
|
+
"by is not available for multiple x variables: the "
|
|
221
|
+
"items claim position and the responses color, so a by "
|
|
222
|
+
"variable has no encoding left. Use facet, which draws "
|
|
223
|
+
"each group in a panel of its own.")
|
|
224
|
+
if facet is not None and form != "bar":
|
|
225
|
+
raise ValueError(
|
|
226
|
+
'facet with multiple x variables requires form="bar"')
|
|
227
|
+
if form not in ("bar", "bubble"):
|
|
228
|
+
raise ValueError(
|
|
229
|
+
'Multiple x variables only available for form="bar"')
|
|
230
|
+
if y is not None or stat is not None:
|
|
231
|
+
raise ValueError(
|
|
232
|
+
"A multi-item chart counts the responses to each item, "
|
|
233
|
+
"so y and stat do not apply")
|
|
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
|
+
|
|
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)
|
|
295
|
+
fill_use = (fill if isinstance(fill, (list, tuple))
|
|
296
|
+
else [fill] * len(frq.index) if fill is not None
|
|
297
|
+
else items_fill(theme, len(frq.index)))
|
|
298
|
+
leg = "Responses" if legend_title is None else legend_title
|
|
299
|
+
show = not resolve_quiet(quiet)
|
|
300
|
+
|
|
301
|
+
if facet is None:
|
|
302
|
+
# ordered by mean response, the largest at the top: the bars
|
|
303
|
+
# build upward from the bottom, so ascending unless sort
|
|
304
|
+
# says otherwise
|
|
305
|
+
if sort_miss:
|
|
306
|
+
sort = "+"
|
|
307
|
+
order = list(items)
|
|
308
|
+
if sort != "0":
|
|
309
|
+
order = list(wm.sort_values(ascending=(sort == "+"),
|
|
310
|
+
kind="stable").index)
|
|
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)
|
|
316
|
+
return bc_items_plotly(
|
|
317
|
+
frq, order, fill_use,
|
|
318
|
+
labels="%" if labels is None else labels,
|
|
319
|
+
labels_size=labels_size, stack100=stack100,
|
|
320
|
+
labels_cut=0.04 if labels_cut is None else labels_cut,
|
|
321
|
+
x_lab="" if xlab is None else xlab,
|
|
322
|
+
y_lab="" if ylab is None else ylab,
|
|
323
|
+
legend_title=leg, main=main or None,
|
|
324
|
+
axis_fmt=axis_fmt, axis_x_pre=axis_x_pre,
|
|
325
|
+
rotate_x=rotate_x, rotate_y=rotate_y, horiz=horiz)
|
|
326
|
+
|
|
327
|
+
# faceted: a panel is a fraction of the width of the single
|
|
328
|
+
# chart, too narrow for segment labels, and the paneled chart
|
|
329
|
+
# keeps the order given so the panels read alike
|
|
330
|
+
if labels is not None and labels != "off":
|
|
331
|
+
raise ValueError(
|
|
332
|
+
"labels are not drawn on a faceted multi-item chart. Each "
|
|
333
|
+
"panel is a fraction of the width of the single chart, so "
|
|
334
|
+
"its segments are too narrow for a legible label. The "
|
|
335
|
+
"counts for each panel are reported at the console.")
|
|
336
|
+
if gap is not None:
|
|
337
|
+
raise ValueError(
|
|
338
|
+
"gap is not available for a faceted multi-item chart")
|
|
339
|
+
if not sort_miss and sort != "0":
|
|
340
|
+
raise ValueError(
|
|
341
|
+
"sort is not available for a faceted multi-item chart: "
|
|
342
|
+
"the panels keep the items in the order given, so they "
|
|
343
|
+
"read alike")
|
|
344
|
+
if isinstance(facet, (list, tuple)):
|
|
345
|
+
fcols = [_get_column(data, f, "facet") for f in facet]
|
|
346
|
+
fac = fcols[0].astype(str)
|
|
347
|
+
for c in fcols[1:]:
|
|
348
|
+
fac = fac + " / " + c.astype(str)
|
|
349
|
+
facet_name = ", ".join(facet)
|
|
350
|
+
else:
|
|
351
|
+
fac = _get_column(data, facet, "facet")
|
|
352
|
+
facet_name = facet
|
|
353
|
+
keep = fac.notna()
|
|
354
|
+
facet_order = _category_order(fac[keep])
|
|
355
|
+
resp = list(frq.index)
|
|
356
|
+
tbls = {}
|
|
357
|
+
rep_lines, panels = [], {}
|
|
358
|
+
for lv in facet_order:
|
|
359
|
+
m = keep & (fac == lv)
|
|
360
|
+
f_lv, w_lv, _ = items_table([c[m] for c in cols], items) \
|
|
361
|
+
if all(c[m].notna().any() for c in cols) else (
|
|
362
|
+
pd.DataFrame(0, index=resp, columns=items), None, None)
|
|
363
|
+
f_lv = f_lv.reindex(index=resp, fill_value=0)
|
|
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)
|
|
371
|
+
t = f_lv.astype(float)
|
|
372
|
+
if stack100:
|
|
373
|
+
tot = t.sum(axis=0)
|
|
374
|
+
t = t.div(tot.where(tot != 0), axis=1).fillna(0)
|
|
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)
|
|
378
|
+
tbl = pd.DataFrame([tbls[lv].sum(axis=0) for lv in facet_order],
|
|
379
|
+
index=facet_order)
|
|
380
|
+
tbl.index.name = facet_name
|
|
381
|
+
n_col_use = (max(1, int(n_col)) if n_col is not None
|
|
382
|
+
else max(1, math.ceil(len(facet_order) / int(n_row)))
|
|
383
|
+
if n_row is not None else 1)
|
|
384
|
+
return bc_facet_plotly(
|
|
385
|
+
tbl, x_name="", facet_name=facet_name,
|
|
386
|
+
x_lab="" if xlab is None else xlab,
|
|
387
|
+
y_lab="" if ylab is None else ylab,
|
|
388
|
+
fill=fill_use, border="off",
|
|
389
|
+
digits_d=2 if stack100 else 0, n_col=n_col_use,
|
|
390
|
+
axis_fmt=axis_fmt, axis_x_pre=axis_x_pre,
|
|
391
|
+
rotate_x=rotate_x, rotate_y=rotate_y, main=main,
|
|
392
|
+
by_tbls=tbls, by_name=leg, beside=False,
|
|
393
|
+
val_name=("Proportion" if stack100 else "Count"),
|
|
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 (BASE_COLORS, auto_text_color,
|
|
406
|
+
axis_format, plotly_style, to_hex)
|
|
407
|
+
from .stats_out import _counts_1d
|
|
408
|
+
show = not resolve_quiet(quiet)
|
|
409
|
+
keep = []
|
|
410
|
+
for nm, c in zip(items, cols):
|
|
411
|
+
if is_numeric_dtype(c) and not isinstance(
|
|
412
|
+
c.dtype, pd.CategoricalDtype):
|
|
413
|
+
continue # R: .is.num.cat(x, 0) FALSE
|
|
414
|
+
v = c.dropna()
|
|
415
|
+
if v.nunique() >= len(v): # identifies each case
|
|
416
|
+
continue
|
|
417
|
+
if v.nunique() == 1:
|
|
418
|
+
if show:
|
|
419
|
+
print(f"\nVariable {nm} has only one value.\n"
|
|
420
|
+
" No bar chart produced.\n")
|
|
421
|
+
continue
|
|
422
|
+
keep.append((nm, c))
|
|
423
|
+
if not keep:
|
|
424
|
+
raise ValueError(
|
|
425
|
+
"No categorical variables, so no bar charts.\n\n"
|
|
426
|
+
"If you have integer variables that are categorical, "
|
|
427
|
+
"convert them to pandas Categorical, e.g.\n"
|
|
428
|
+
" d['Plan'] = d['Plan'].astype('category')")
|
|
429
|
+
|
|
430
|
+
n = len(keep)
|
|
431
|
+
n_col = min(3, n)
|
|
432
|
+
n_row = math.ceil(n / n_col)
|
|
433
|
+
fig = make_subplots(rows=n_row, cols=n_col,
|
|
434
|
+
subplot_titles=[nm for nm, _ in keep],
|
|
435
|
+
horizontal_spacing=0.08,
|
|
436
|
+
vertical_spacing=max(0.08, 0.3 / n_row))
|
|
437
|
+
st = plotly_style()
|
|
438
|
+
rep_lines, per_var = [], {}
|
|
439
|
+
for k, (nm, c) in enumerate(keep):
|
|
440
|
+
rep_lines += _counts_1d(c.dropna(), nm,
|
|
441
|
+
int(c.isna().sum())) + [""]
|
|
442
|
+
per_var[nm] = chart_stats_data(c.dropna(), None, None, None,
|
|
443
|
+
n_miss=int(c.isna().sum()))
|
|
444
|
+
order = _category_order(c.dropna())
|
|
445
|
+
cnt = c.value_counts().reindex(order, fill_value=0)
|
|
446
|
+
f = (fill if isinstance(fill, (list, tuple))
|
|
447
|
+
else [fill] if fill is not None else BASE_COLORS)
|
|
448
|
+
cols_k = [to_hex(f[i % len(f)]) for i in range(len(cnt))]
|
|
449
|
+
r, cc = k // n_col + 1, k % n_col + 1
|
|
450
|
+
fig.add_trace(go.Bar(
|
|
451
|
+
x=[str(v) for v in cnt.index], y=cnt.to_numpy(),
|
|
452
|
+
marker=dict(color=cols_k), text=[str(v) for v in cnt],
|
|
453
|
+
textposition="inside", showlegend=False,
|
|
454
|
+
textfont=dict(color=auto_text_color(cols_k)),
|
|
455
|
+
hovertemplate=f"{nm}: %{{x}}<br>Count: %{{y}}"
|
|
456
|
+
"<extra></extra>"), row=r, col=cc)
|
|
457
|
+
ticks = pretty(0.0, float(cnt.max()))
|
|
458
|
+
fig.update_yaxes(row=r, col=cc, tickvals=ticks,
|
|
459
|
+
ticktext=axis_format(ticks, 0),
|
|
460
|
+
range=[0, ticks[-1] * 1.04],
|
|
461
|
+
gridcolor="#E5E5E5",
|
|
462
|
+
title_text="Count" if cc == 1 else "")
|
|
463
|
+
fig.update_xaxes(row=r, col=cc, type="category",
|
|
464
|
+
title_text=xlab or "")
|
|
465
|
+
_set_stats(ChartStats(n_dim=1, variables=per_var), rep_lines,
|
|
466
|
+
quiet)
|
|
467
|
+
fig.update_layout(
|
|
468
|
+
template=None, height=140 + 260 * n_row,
|
|
469
|
+
plot_bgcolor=to_hex(st["panel_fill"]),
|
|
470
|
+
paper_bgcolor=to_hex(st["window_fill"]),
|
|
471
|
+
title=(dict(text=main, x=0.5) if main else None))
|
|
472
|
+
return fig
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _items_print(frq, wm, numeric, title=None):
|
|
476
|
+
return items_stats_lines(
|
|
477
|
+
frq, wm, title or "Frequencies of Responses by Variable",
|
|
478
|
+
numeric)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _set_stats(st, lines, quiet):
|
|
482
|
+
"""Keep the statistics of this call, with the report as their
|
|
483
|
+
text, and display the report unless quiet."""
|
|
484
|
+
st["text"] = "\n".join(lines)
|
|
485
|
+
CALL_STATS.set(st)
|
|
486
|
+
if not resolve_quiet(quiet):
|
|
487
|
+
print(st["text"])
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _profile_fill(theme):
|
|
491
|
+
"""One hue per series from the theme, as R's profile does:
|
|
492
|
+
.color_range(.get_fill(theme), n), the qualitative hues for the
|
|
493
|
+
default theme."""
|
|
494
|
+
from .plotly_utils import BASE_COLORS
|
|
495
|
+
if theme is None or theme == "colors":
|
|
496
|
+
return list(BASE_COLORS)
|
|
497
|
+
return _theme_fill(theme, 8)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _max_dd(vals):
|
|
501
|
+
"""Most decimal digits among the first 200 values as R format()
|
|
502
|
+
writes them, 7 significant digits. R analog: .max.dd()"""
|
|
503
|
+
mx = 0
|
|
504
|
+
for v in list(vals)[:200]:
|
|
505
|
+
if v is None or not np.isfinite(v):
|
|
506
|
+
continue
|
|
507
|
+
txt = f"{float(v):.7g}"
|
|
508
|
+
if "." in txt and "e" not in txt:
|
|
509
|
+
mx = max(mx, len(txt) - txt.index(".") - 1)
|
|
510
|
+
return mx
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _dot_diff_lines(cats, ydf, x_name):
|
|
514
|
+
"""The difference a paired dot plot presents, listed in the
|
|
515
|
+
order drawn, top of the plot first. R analog: the dif.pair
|
|
516
|
+
listing of Chart.R's paired dot path."""
|
|
517
|
+
difs = (ydf.iloc[:, 1] - ydf.iloc[:, 0]).to_numpy(dtype=float)
|
|
518
|
+
dd = min(_max_dd(np.r_[ydf.iloc[:, 0], ydf.iloc[:, 1]]) + 1, 7)
|
|
519
|
+
ny = len(difs)
|
|
520
|
+
mx_i = len(str(ny))
|
|
521
|
+
mx_d = max(len(f"{v:.{dd}f}") for v in difs)
|
|
522
|
+
mx_f = max([5] + [len(c) for c in cats])
|
|
523
|
+
lines = ["", f"{ydf.columns[1]} - {ydf.columns[0]}",
|
|
524
|
+
f"{'n'.rjust(mx_i)} {' diff'.rjust(mx_d)} {x_name}",
|
|
525
|
+
"-" * (mx_i + mx_d + mx_f + 2)]
|
|
526
|
+
shown = (range(1, ny + 1) if ny <= 20
|
|
527
|
+
else list(range(1, 11)) + list(range(ny - 9, ny + 1)))
|
|
528
|
+
for i in range(1, ny + 1):
|
|
529
|
+
k = ny - i
|
|
530
|
+
if i in shown:
|
|
531
|
+
lines.append(f"{str(i).rjust(mx_i)} "
|
|
532
|
+
f"{f'{difs[k]:.{dd}f}'.rjust(mx_d)} {cats[k]}")
|
|
533
|
+
return lines + [""]
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _dot_paired(cats, ydf, sort, sort_miss, origin_x, show_diff,
|
|
537
|
+
x_name, quiet=None, **render):
|
|
538
|
+
"""Order a multi-series dot plot and draw it. Two series are a
|
|
539
|
+
pair, read for the gap between them, so by default they are
|
|
540
|
+
ordered by that difference, ascending so the largest positive
|
|
541
|
+
difference is drawn at the top, and the value axis begins at
|
|
542
|
+
zero. More series are ordered by their row mean when sort is
|
|
543
|
+
given. R analog: Chart.R paired dot path."""
|
|
544
|
+
dif_pair = ydf.shape[1] == 2
|
|
545
|
+
if dif_pair and sort_miss:
|
|
546
|
+
sort = "+"
|
|
547
|
+
if sort != "0":
|
|
548
|
+
key = (ydf.iloc[:, 1] - ydf.iloc[:, 0] if dif_pair
|
|
549
|
+
else ydf.mean(axis=1))
|
|
550
|
+
order = np.argsort(key.to_numpy(dtype=float)
|
|
551
|
+
* (-1 if sort == "-" else 1),
|
|
552
|
+
kind="stable")
|
|
553
|
+
cats = [cats[i] for i in order]
|
|
554
|
+
ydf = ydf.iloc[order]
|
|
555
|
+
if dif_pair:
|
|
556
|
+
# the difference the display presents, kept with the figure; it
|
|
557
|
+
# is reported where R lists it (show_diff), unless quiet
|
|
558
|
+
dif = pd.Series(
|
|
559
|
+
(ydf.iloc[:, 1] - ydf.iloc[:, 0]).to_numpy(dtype=float),
|
|
560
|
+
index=cats, name=f"{ydf.columns[1]} - {ydf.columns[0]}")
|
|
561
|
+
st = CALL_STATS.get()
|
|
562
|
+
if st is None:
|
|
563
|
+
st = ChartStats(n_dim=2, values=ydf.set_axis(cats), text="")
|
|
564
|
+
CALL_STATS.set(st)
|
|
565
|
+
st["diff"] = dif
|
|
566
|
+
if show_diff:
|
|
567
|
+
lines = _dot_diff_lines(cats, ydf, x_name)
|
|
568
|
+
st["text"] = "\n".join(
|
|
569
|
+
[t for t in (st.get("text", ""), "\n".join(lines)) if t])
|
|
570
|
+
if not resolve_quiet(quiet):
|
|
571
|
+
print("\n".join(lines))
|
|
572
|
+
origin, gridT = _dot_origin_grid(
|
|
573
|
+
ydf.to_numpy(dtype=float).ravel(),
|
|
574
|
+
origin_in=0 if (dif_pair and origin_x is None) else origin_x)
|
|
575
|
+
if origin is None:
|
|
576
|
+
origin = 0
|
|
577
|
+
return dot_plotly(cats, ydf, gridT=gridT, origin_x=origin,
|
|
578
|
+
**render)
|
|
579
|
+
|
|
580
|
+
|
|
164
581
|
def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
165
582
|
form="bar",
|
|
166
583
|
n_row=None, n_col=None,
|
|
167
|
-
hole=0.
|
|
584
|
+
hole=0.62,
|
|
168
585
|
radius=0.50, power=0.5,
|
|
169
586
|
pt_size=1, origin_x=None, origin_y=None,
|
|
170
587
|
segments_x=None, segments_y=None,
|
|
171
588
|
stat=None, stat_x="count",
|
|
172
|
-
horiz=
|
|
589
|
+
horiz=None, sort=None, beside=False, stack100=False,
|
|
173
590
|
gap=None, scale_y=None, break_x=None,
|
|
174
591
|
fill=None, color=None, transparency=None,
|
|
175
592
|
fill_split=None, fill_scaled=False, fill_chroma=75,
|
|
176
593
|
theme=None,
|
|
177
594
|
labels=None, labels_position=None,
|
|
178
595
|
labels_color=None, labels_size=None, labels_decimals=None,
|
|
596
|
+
labels_cut=None, segments=None, one_plot=None,
|
|
179
597
|
legend_title=None, legend_position=None,
|
|
180
598
|
legend_labels=None, legend_horiz=False,
|
|
181
599
|
legend_size=None, legend_abbrev=None, legend_adjust=0,
|
|
@@ -216,8 +634,26 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
216
634
|
"K": "60K" for thousands, commas past 9999); axis_x_pre /
|
|
217
635
|
axis_y_pre prepend a per-axis prefix (e.g. "$").
|
|
218
636
|
|
|
637
|
+
form="dot" is Cleveland's dot plot: categories on the vertical
|
|
638
|
+
axis (horiz defaults to True for this form alone), the value
|
|
639
|
+
axis from zero, no title unless main= is given. With by=, one
|
|
640
|
+
series of dots per level; with exactly two levels, or a pair
|
|
641
|
+
of y variables (y=["Pre", "Post"]), the pair is joined by a
|
|
642
|
+
segment and ordered by its difference, largest positive at
|
|
643
|
+
the top. sort="0" keeps the data order instead.
|
|
644
|
+
|
|
645
|
+
form="profile" plots one point per category of x and connects
|
|
646
|
+
the points across the categories; with by= it draws one
|
|
647
|
+
profile per level, the interaction plot of a two-way ANOVA.
|
|
648
|
+
origin_y sets where the value axis begins; facet= draws one
|
|
649
|
+
panel per level on a shared value scale.
|
|
650
|
+
|
|
219
651
|
Variables are strings naming columns of the DataFrame `data`.
|
|
220
652
|
Returns a plotly Figure; call .show() to display from a script.
|
|
653
|
+
The statistics behind the chart come with it as fig.stats, read
|
|
654
|
+
as attributes or by name: fig.stats.freq, fig.stats.p_value,
|
|
655
|
+
fig.stats.text (the console report, kept when quiet=True), named
|
|
656
|
+
as R's Chart() names its returned list.
|
|
221
657
|
"""
|
|
222
658
|
|
|
223
659
|
# ----- validate parameters ------------------------------------
|
|
@@ -226,6 +662,62 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
226
662
|
if form not in _FORMS:
|
|
227
663
|
raise ValueError(f"form must be one of {_FORMS}")
|
|
228
664
|
|
|
665
|
+
# Cleveland's dot plot places the categories on the vertical
|
|
666
|
+
# axis so their labels read across. A horizontal chart lists
|
|
667
|
+
# its first category at the bottom, so sort is inverted to keep
|
|
668
|
+
# "-" meaning largest at the top. R analog: Chart.R horiz.miss
|
|
669
|
+
sort_miss = sort is None
|
|
670
|
+
if sort is None:
|
|
671
|
+
sort = "0"
|
|
672
|
+
multi_x = isinstance(x, (list, tuple))
|
|
673
|
+
if horiz is None:
|
|
674
|
+
# the dot plot and the multi-item chart list their categories
|
|
675
|
+
# down the vertical axis
|
|
676
|
+
horiz = form == "dot" or multi_x
|
|
677
|
+
if horiz and sort in ("+", "-"):
|
|
678
|
+
sort = "+" if sort == "-" else "-"
|
|
679
|
+
|
|
680
|
+
# segments joins the points of a profile; the dot chart's stems
|
|
681
|
+
# are set along its value axis
|
|
682
|
+
if segments is not None and form != "profile":
|
|
683
|
+
raise ValueError(
|
|
684
|
+
"segments joins the points of a profile chart, so it "
|
|
685
|
+
'needs form="profile"; for the segments of a dot chart '
|
|
686
|
+
"set segments_x or segments_y")
|
|
687
|
+
if labels_cut is not None and form != "bar":
|
|
688
|
+
raise ValueError(
|
|
689
|
+
"labels_cut leaves off the value labels of small bar "
|
|
690
|
+
'segments, so it needs form="bar"')
|
|
691
|
+
|
|
692
|
+
# the value axis of a profile is y, so origin_x has nothing to set
|
|
693
|
+
if form == "profile" and origin_x is not None:
|
|
694
|
+
raise ValueError(
|
|
695
|
+
"origin_x does not apply to a profile chart: its value "
|
|
696
|
+
"axis is y, so set origin_y")
|
|
697
|
+
|
|
698
|
+
# Only the segments parameter along the value axis applies. The
|
|
699
|
+
# dot plot's orientation decides which one that is, so say which
|
|
700
|
+
# parameter to set instead rather than let the given one go
|
|
701
|
+
# inert. An unfaceted plot of several series is always
|
|
702
|
+
# horizontal.
|
|
703
|
+
# R analog: Chart.R .seg_note
|
|
704
|
+
if form == "dot":
|
|
705
|
+
dot_h = horiz or (facet is None and (
|
|
706
|
+
by is not None or isinstance(y, (list, tuple))))
|
|
707
|
+
given, instead, cat_ax, seg_ax = (
|
|
708
|
+
("segments_y", "segments_x", "y", "x") if dot_h
|
|
709
|
+
else ("segments_x", "segments_y", "x", "y"))
|
|
710
|
+
if (segments_y if dot_h else segments_x) is not None:
|
|
711
|
+
print(f">>> {given} does not apply to this dot plot.\n"
|
|
712
|
+
f" The categories are on the {cat_ax}-axis, so "
|
|
713
|
+
"the line segments run along the "
|
|
714
|
+
f"{seg_ax}-axis.\n"
|
|
715
|
+
f" To control them, set {instead}.\n")
|
|
716
|
+
if dot_h:
|
|
717
|
+
segments_y = None
|
|
718
|
+
else:
|
|
719
|
+
segments_x = None
|
|
720
|
+
|
|
229
721
|
# hierarchical routing: treemap/icicle always; pie with by=
|
|
230
722
|
# nests the by rings inside the x wedges as a sunburst
|
|
231
723
|
# R analog: Chart.R hier dispatch
|
|
@@ -234,14 +726,33 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
234
726
|
hier_type = form
|
|
235
727
|
elif form == "pie" and by is not None:
|
|
236
728
|
hier_type = "sunburst"
|
|
729
|
+
if isinstance(by, (list, tuple)) and len(by) > 1 \
|
|
730
|
+
and hier_type is None:
|
|
731
|
+
raise ValueError(
|
|
732
|
+
f"Only one by variable is permitted for a {form} chart, "
|
|
733
|
+
"but more than one specified.\n\n"
|
|
734
|
+
"The groups of by are overlaid within the display, one "
|
|
735
|
+
"color\n to each. That one channel is carried by the "
|
|
736
|
+
"first variable,\n so a second has no encoding left by "
|
|
737
|
+
"which to separate its\n levels.\n"
|
|
738
|
+
"Multiple by variables apply only to the hierarchical\n"
|
|
739
|
+
" charts -- pie/sunburst, treemap, and icicle -- where\n"
|
|
740
|
+
" nesting accepts depth and each added variable is one\n"
|
|
741
|
+
" level deeper.\n"
|
|
742
|
+
f"To stratify a {form} chart by a second variable, use\n"
|
|
743
|
+
" facet, which draws each group in a panel of its own.")
|
|
744
|
+
# hierarchical forms: each further by variable is one level
|
|
745
|
+
# deeper; the first plays the part of a single by elsewhere
|
|
746
|
+
by_more = []
|
|
747
|
+
if isinstance(by, (list, tuple)):
|
|
748
|
+
by_more = list(by[1:])
|
|
749
|
+
by = by[0] if by else None
|
|
750
|
+
by_label = ", ".join([by] + by_more) if by is not None else None
|
|
237
751
|
if hier_type is not None and stat == "deviation":
|
|
238
752
|
raise ValueError('stat="deviation" is not meaningful for '
|
|
239
753
|
"hierarchical charts: negative values "
|
|
240
754
|
"cannot form part-of-whole areas")
|
|
241
755
|
|
|
242
|
-
if form == "dot" and by is not None:
|
|
243
|
-
raise ValueError("The by variable is not meaningful for "
|
|
244
|
-
"dot charts. Do a bar chart.")
|
|
245
756
|
if isinstance(y, (list, tuple)) and form != "dot":
|
|
246
757
|
raise ValueError('Multiple y variables are only supported '
|
|
247
758
|
'for form="dot" (paired dot chart)')
|
|
@@ -256,15 +767,22 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
256
767
|
raise ValueError('labels_position must be "in" or "out"')
|
|
257
768
|
if axis_fmt not in ("K", ",", ".", ""):
|
|
258
769
|
raise ValueError('axis_fmt must be "K", ",", "." or ""')
|
|
770
|
+
# proportions of a bar chart with by are taken within each bar,
|
|
771
|
+
# the 100% stacked chart. R analog: .bc.main()
|
|
772
|
+
# if (!is.null(by) && prop) stack100 <- TRUE
|
|
773
|
+
if (stat_x == "proportion" and by is not None and form == "bar"
|
|
774
|
+
and facet is None and y is None):
|
|
775
|
+
stack100 = True
|
|
776
|
+
stat_x = "count"
|
|
259
777
|
if stack100:
|
|
260
778
|
if form != "bar":
|
|
261
779
|
raise ValueError('stack100 rescales the bars of a bar '
|
|
262
780
|
'chart, so it needs form="bar"')
|
|
263
|
-
if facet is not None:
|
|
781
|
+
if facet is not None and by is None:
|
|
264
782
|
raise ValueError(
|
|
265
|
-
"stack100 rescales each bar
|
|
266
|
-
"
|
|
267
|
-
"
|
|
783
|
+
"stack100 rescales each bar to show how the levels "
|
|
784
|
+
"of a by variable divide it, so a faceted bar chart "
|
|
785
|
+
"needs by= for it")
|
|
268
786
|
if by is None and stat_x == "proportion":
|
|
269
787
|
raise ValueError(
|
|
270
788
|
'stack100 without a by variable is the same as '
|
|
@@ -291,6 +809,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
291
809
|
"legend_abbrev": legend_abbrev,
|
|
292
810
|
"legend_adjust": legend_adjust or None}
|
|
293
811
|
_leg_set = [k for k, v in _leg.items() if v is not None]
|
|
812
|
+
# a dot plot of several series heads its legend with legend_title
|
|
813
|
+
if form == "dot" and (by is not None
|
|
814
|
+
or isinstance(y, (list, tuple))):
|
|
815
|
+
_leg_set = [k for k in _leg_set if k != "legend_title"]
|
|
294
816
|
if _leg_set:
|
|
295
817
|
if form != "bar":
|
|
296
818
|
raise ValueError(
|
|
@@ -300,7 +822,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
300
822
|
raise ValueError(
|
|
301
823
|
f"{', '.join(_leg_set)} style the legend that a by "
|
|
302
824
|
"variable creates, so they need by=")
|
|
303
|
-
if facet is not None:
|
|
825
|
+
if facet is not None and _leg_set != ["legend_title"]:
|
|
304
826
|
raise ValueError(
|
|
305
827
|
f"{', '.join(_leg_set)} apply to a single-panel bar "
|
|
306
828
|
"chart; a faceted (Trellis) bar chart has no by "
|
|
@@ -348,62 +870,88 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
348
870
|
if sort != "0":
|
|
349
871
|
raise ValueError("Sort not applicable to Trellis "
|
|
350
872
|
"(faceted bar) charts")
|
|
351
|
-
if stat is not None:
|
|
352
|
-
raise ValueError(
|
|
353
|
-
"Only the original data work with Trellis "
|
|
354
|
-
"plots, no data aggregation with parameter "
|
|
355
|
-
"stat. Use by instead of facet.")
|
|
356
|
-
if y is not None:
|
|
357
|
-
raise ValueError(
|
|
358
|
-
"The faceted bar chart displays counts of x, "
|
|
359
|
-
"so a y variable does not apply. Use by "
|
|
360
|
-
"instead of facet.")
|
|
361
|
-
if by is not None:
|
|
362
|
-
raise ValueError(
|
|
363
|
-
"by and facet do not combine for bar charts: "
|
|
364
|
-
"each facet panel displays the counts of x. "
|
|
365
|
-
"Use one or the other.")
|
|
366
873
|
|
|
367
874
|
# ----- resolve variables and filter ---------------------------
|
|
368
875
|
if filter is not None:
|
|
369
876
|
data = data.query(filter)
|
|
370
877
|
|
|
371
|
-
#
|
|
372
|
-
#
|
|
878
|
+
# row_names: the data frame's row labels as the categorical x,
|
|
879
|
+
# in their own order rather than alphabetical; the axis label
|
|
880
|
+
# is dropped unless given. R analog: Chart.R row_names
|
|
881
|
+
if (isinstance(x, str) and x in ("row_names", "row.names")
|
|
882
|
+
and x not in data.columns):
|
|
883
|
+
names = data.index.astype(str)
|
|
884
|
+
data = data.copy()
|
|
885
|
+
data[x] = pd.Categorical(
|
|
886
|
+
names, categories=list(dict.fromkeys(names)))
|
|
887
|
+
if xlab is None:
|
|
888
|
+
xlab = ""
|
|
889
|
+
|
|
890
|
+
# a list of x variables that share one response scale: the
|
|
891
|
+
# multi-item stacked chart. Its bands claim position and color
|
|
892
|
+
# the responses within them, so by has no encoding left; facet
|
|
893
|
+
# panels remain. R analog: Chart.R multiple-x path
|
|
894
|
+
if multi_x:
|
|
895
|
+
return _chart_items(
|
|
896
|
+
data, list(x), y=y, by=by, facet=facet, form=form,
|
|
897
|
+
stat=stat, sort=sort, sort_miss=sort_miss,
|
|
898
|
+
stack100=stack100, horiz=horiz, fill=fill, theme=theme,
|
|
899
|
+
labels=labels, labels_size=labels_size, gap=gap,
|
|
900
|
+
labels_cut=labels_cut, power=power, radius=radius,
|
|
901
|
+
one_plot=one_plot,
|
|
902
|
+
xlab=xlab, ylab=ylab, main=main,
|
|
903
|
+
legend_title=legend_title, n_col=n_col, n_row=n_row,
|
|
904
|
+
axis_fmt=axis_fmt, axis_x_pre=axis_x_pre,
|
|
905
|
+
rotate_x=rotate_x, rotate_y=rotate_y, quiet=quiet)
|
|
906
|
+
|
|
907
|
+
# paired dot chart: x=labels, y=[col1, col2, ...]. Displayed
|
|
908
|
+
# directly when x identifies the cases; with a repeated x each
|
|
909
|
+
# column is aggregated over x by stat, which is then required.
|
|
910
|
+
# R analog: Chart.R paired-dot path and its multi-column y stat
|
|
373
911
|
if isinstance(y, (list, tuple)):
|
|
374
|
-
if stat is not None:
|
|
375
|
-
raise ValueError("stat does not apply to a paired dot "
|
|
376
|
-
"chart; the values display directly")
|
|
377
912
|
x_ser = _get_column(data, x, "x")
|
|
378
913
|
y_cols = [_get_column(data, yi, "y") for yi in y]
|
|
379
914
|
sub = pd.concat([x_ser] + y_cols, axis=1).dropna()
|
|
380
|
-
cats = sub[x].astype(str).tolist()
|
|
381
915
|
ydf = sub[list(y)]
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
916
|
+
y_lbl = " & ".join(y)
|
|
917
|
+
if sub[x].duplicated().any():
|
|
918
|
+
if stat is None:
|
|
919
|
+
raise ValueError(
|
|
920
|
+
"The data are not a summary (pivot) table, and "
|
|
921
|
+
f"you have numerical variables, y = {y_lbl}, so "
|
|
922
|
+
"specify stat to define the aggregation, e.g., "
|
|
923
|
+
'stat="mean"')
|
|
924
|
+
if stat not in STAT_FUN and stat != "deviation":
|
|
925
|
+
raise ValueError(f"stat must be one of {_STATS}")
|
|
926
|
+
grp = ydf.groupby(sub[x], observed=True, sort=True)
|
|
927
|
+
if stat == "deviation":
|
|
928
|
+
ydf = grp.mean()
|
|
929
|
+
ydf = ydf - ydf.mean()
|
|
930
|
+
else:
|
|
931
|
+
ydf = STAT_FUN[stat](grp)
|
|
932
|
+
ydf = ydf.reindex([c for c in _category_order(sub[x])
|
|
933
|
+
if c in ydf.index])
|
|
934
|
+
cats = [str(c) for c in ydf.index]
|
|
935
|
+
val_lab = f"{STAT_LBL[stat]} of {y_lbl}"
|
|
936
|
+
else:
|
|
937
|
+
if stat is not None:
|
|
938
|
+
raise ValueError(
|
|
939
|
+
"The data are a summary table, so do not specify "
|
|
940
|
+
"stat: the aggregation has already been done")
|
|
941
|
+
cats = sub[x].astype(str).tolist()
|
|
942
|
+
val_lab = y_lbl
|
|
943
|
+
x_lab_dot = (xlab if xlab else ylab if ylab else val_lab)
|
|
944
|
+
return _dot_paired(
|
|
945
|
+
cats, ydf.reset_index(drop=True), sort, sort_miss,
|
|
946
|
+
origin_x, show_diff=True, quiet=quiet, x_name=x,
|
|
947
|
+
fill=fill, border=color, pt_size=pt_size,
|
|
948
|
+
x_lab=x_lab_dot, y_lab=x,
|
|
402
949
|
digits_d=2 if digits_d is None else digits_d,
|
|
403
950
|
pt_opacity=1 - (get_option("trans_pt_fill", 0.10)
|
|
404
951
|
if transparency is None
|
|
405
952
|
else transparency),
|
|
406
|
-
|
|
953
|
+
main=None if not main else main,
|
|
954
|
+
legend_title=legend_title,
|
|
407
955
|
axis_fmt=axis_fmt, axis_x_pre=axis_x_pre,
|
|
408
956
|
rotate_x=rotate_x, rotate_y=rotate_y,
|
|
409
957
|
segments_x=(True if segments_x is None
|
|
@@ -412,6 +960,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
412
960
|
|
|
413
961
|
x_ser = _get_column(data, x, "x")
|
|
414
962
|
by_ser = _get_column(data, by, "by") if by is not None else None
|
|
963
|
+
by_more_sers = [_get_column(data, b, "by") for b in by_more]
|
|
415
964
|
y_ser = _get_column(data, y, "y") if y is not None else None
|
|
416
965
|
|
|
417
966
|
# multiple facet variables define the panel grid, not extra
|
|
@@ -431,11 +980,17 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
431
980
|
fac_cols = [ser]
|
|
432
981
|
facet_name = str(ser.name)
|
|
433
982
|
|
|
983
|
+
# count missing x before the removal below, or it is always 0
|
|
984
|
+
# R analog: Chart.R n.miss.x
|
|
985
|
+
n_miss_x = int(x_ser.isna().sum())
|
|
986
|
+
|
|
434
987
|
# casewise deletion over the variables in the analysis
|
|
435
988
|
used = [s for s in (x_ser, by_ser, y_ser) if s is not None]
|
|
989
|
+
used += by_more_sers
|
|
436
990
|
used += fac_cols or []
|
|
437
991
|
keep = ~pd.concat(used, axis=1).isna().any(axis=1)
|
|
438
992
|
x_ser = x_ser[keep]
|
|
993
|
+
by_more_sers = [b[keep] for b in by_more_sers]
|
|
439
994
|
if by_ser is not None:
|
|
440
995
|
by_ser = by_ser[keep]
|
|
441
996
|
if y_ser is not None:
|
|
@@ -451,15 +1006,67 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
451
1006
|
|
|
452
1007
|
x_order = _category_order(x_ser)
|
|
453
1008
|
by_order = _category_order(by_ser) if by_ser is not None else None
|
|
1009
|
+
|
|
1010
|
+
# A numeric x with many distinct values yields one category per
|
|
1011
|
+
# value, usually a distribution in disguise. Advisory only: no
|
|
1012
|
+
# count of levels reliably separates categorical from
|
|
1013
|
+
# continuous, so the call is honored. R analog: Chart.R n.x > 12
|
|
1014
|
+
if (y_ser is None and pd.api.types.is_numeric_dtype(x_ser)
|
|
1015
|
+
and len(x_order) > 12):
|
|
1016
|
+
print(f">>> {x} is numeric with {len(x_order)} distinct "
|
|
1017
|
+
"values, so each\n becomes its own category. For "
|
|
1018
|
+
"the distribution of a continuous\n variable: "
|
|
1019
|
+
f'X("{x}")\n')
|
|
454
1020
|
facet_order = (_category_order(facet_ser)
|
|
455
1021
|
if facet_ser is not None else None)
|
|
456
1022
|
|
|
457
|
-
# theme
|
|
458
|
-
#
|
|
459
|
-
if theme is
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
1023
|
+
# a theme set by style(), or by this call's theme=, which the
|
|
1024
|
+
# wrapper has applied for the call
|
|
1025
|
+
if theme is None:
|
|
1026
|
+
theme = get_option("theme")
|
|
1027
|
+
|
|
1028
|
+
# an ordered category is ordinal, so its default fill is the
|
|
1029
|
+
# theme's sequential palette, light to dark in the order of the
|
|
1030
|
+
# levels, not distinct hues. Fill encodes x without by and by
|
|
1031
|
+
# with it, so the ordering of that variable is the one that
|
|
1032
|
+
# applies. R analog: Chart.R is.ordered() fill
|
|
1033
|
+
if (form == "bar" and fill is None and facet_ser is None
|
|
1034
|
+
and fill_split is None and not fill_scaled):
|
|
1035
|
+
enc = by_ser if by_ser is not None else x_ser
|
|
1036
|
+
if (isinstance(enc.dtype, pd.CategoricalDtype)
|
|
1037
|
+
and enc.cat.ordered):
|
|
1038
|
+
fill = _color_range(
|
|
1039
|
+
_THEME_PALETTE.get(theme, "blues"),
|
|
1040
|
+
len(by_order) if by_order is not None
|
|
1041
|
+
else len(x_order))
|
|
1042
|
+
|
|
1043
|
+
# theme=: a bar chart without by is drawn in the theme's single
|
|
1044
|
+
# bar color, which fill_scaled then varies in luminance; with
|
|
1045
|
+
# by, the levels span the theme's sequential palette, darkest
|
|
1046
|
+
# first. The default theme keeps the qualitative hues.
|
|
1047
|
+
# R analog: Chart.R fill block (theme.seq) and .bc.main()
|
|
1048
|
+
if (theme is not None and fill is None
|
|
1049
|
+
and theme not in ("colors", "hues")):
|
|
1050
|
+
if theme not in _THEME_PALETTE:
|
|
1051
|
+
raise ValueError(
|
|
1052
|
+
f"unknown theme '{theme}'; one of: "
|
|
1053
|
+
f"{', '.join(sorted(_THEME_PALETTE))}")
|
|
1054
|
+
if by_order is not None:
|
|
1055
|
+
fill = list(reversed(_color_range(_THEME_PALETTE[theme],
|
|
1056
|
+
len(by_order))))
|
|
1057
|
+
elif form == "bar":
|
|
1058
|
+
fill = [_THEME_BAR_FILL.get(theme, "#595959E6")] \
|
|
1059
|
+
* len(x_order)
|
|
1060
|
+
else:
|
|
1061
|
+
fill = _theme_fill(theme, len(x_order))
|
|
1062
|
+
|
|
1063
|
+
# a palette name in fill, such as "reds", names a range of colors,
|
|
1064
|
+
# one per bar, or per level of by. R analog: .bc.main()
|
|
1065
|
+
# .color_range(fill, n.levels)
|
|
1066
|
+
if (form == "bar" and isinstance(fill, str) and not fill_scaled
|
|
1067
|
+
and fill in _palette_names()):
|
|
1068
|
+
fill = _color_range(fill, len(by_order) if by_order is not None
|
|
1069
|
+
else len(x_order))
|
|
463
1070
|
|
|
464
1071
|
# explicit panel-grid layout: n_col wins, else derive from
|
|
465
1072
|
# n_row; None keeps each renderer's own default (single-column
|
|
@@ -523,6 +1130,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
523
1130
|
n_x = x_ser.nunique()
|
|
524
1131
|
if by_ser is None:
|
|
525
1132
|
is_agg = n_x >= len(x_ser)
|
|
1133
|
+
elif by_more_sers:
|
|
1134
|
+
# each cell of the nesting appears once in a summary table
|
|
1135
|
+
cells = pd.concat([x_ser, by_ser] + by_more_sers, axis=1)
|
|
1136
|
+
is_agg = not cells.astype(str).duplicated().any()
|
|
526
1137
|
else:
|
|
527
1138
|
is_agg = n_x * by_ser.nunique() >= len(by_ser)
|
|
528
1139
|
|
|
@@ -551,36 +1162,75 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
551
1162
|
f"have a numerical variable y='{y}', so specify "
|
|
552
1163
|
'stat to define the aggregation, e.g., stat="mean"')
|
|
553
1164
|
|
|
554
|
-
# accompanying statistics: frequencies, or the stat of y
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
1165
|
+
# accompanying statistics: frequencies, or the stat of y. They are
|
|
1166
|
+
# computed whether or not displayed, and returned with the figure
|
|
1167
|
+
# as fig.stats, as R's Chart() returns its list
|
|
1168
|
+
y_out = (y_ser if y_ser is not None
|
|
1169
|
+
and stat in STAT_FUN else None)
|
|
1170
|
+
if by_more_sers and (y_ser is None or y_out is not None):
|
|
1171
|
+
from .stats_out import nested_stats
|
|
1172
|
+
lines = nested_stats(
|
|
1173
|
+
[x_ser, by_ser] + by_more_sers, [x, by] + by_more,
|
|
1174
|
+
y_out, stat, y, 2 if digits_d is None else digits_d)
|
|
1175
|
+
cols_n = [x_ser, by_ser] + by_more_sers
|
|
1176
|
+
df_n = pd.DataFrame({nm: c for nm, c in
|
|
1177
|
+
zip([x, by] + by_more, cols_n)})
|
|
1178
|
+
_set_stats(ChartStats(
|
|
1179
|
+
n_dim=len(cols_n),
|
|
1180
|
+
freq=df_n.groupby([x, by] + by_more, observed=True).size()
|
|
1181
|
+
if y_out is None else None),
|
|
1182
|
+
lines, quiet)
|
|
1183
|
+
elif y_ser is None or y_out is not None:
|
|
1184
|
+
var_lbl = data.attrs.get("variable_labels", {}) or {}
|
|
1185
|
+
lines = chart_stats(
|
|
1186
|
+
x_ser, by_ser, y_out, stat, x, by, y,
|
|
1187
|
+
2 if digits_d is None else digits_d,
|
|
1188
|
+
stack100=stack100, n_miss=n_miss_x,
|
|
1189
|
+
x_lbl=var_lbl.get(x), y_lbl=var_lbl.get(y),
|
|
1190
|
+
facet_ser=facet_ser, facet_name=facet_name,
|
|
1191
|
+
form=form)
|
|
1192
|
+
_set_stats(chart_stats_data(x_ser, by_ser, y_out, stat,
|
|
1193
|
+
facet_ser=facet_ser,
|
|
1194
|
+
n_miss=n_miss_x),
|
|
1195
|
+
lines, quiet)
|
|
1196
|
+
|
|
1197
|
+
# labels default for aggregated data, the value of the statistic;
|
|
1198
|
+
# bars of counts are labeled with their percentages, as lessR's
|
|
1199
|
+
# do (getOption("values"), "%"): of the total, or of each bar for
|
|
1200
|
+
# the 100% stacked chart. R analog: Chart.R ~716, .bc.main()
|
|
566
1201
|
if labels is None and (stat is not None or
|
|
567
1202
|
(is_agg and y_ser is not None)):
|
|
568
|
-
|
|
1203
|
+
# the bars carry a statistic, so the label is its value;
|
|
1204
|
+
# beside only arranges the bars, and a percentage of a sum
|
|
1205
|
+
# of means is not a quantity
|
|
1206
|
+
labels = "input"
|
|
1207
|
+
elif labels is None and form == "bar" and y_ser is None:
|
|
1208
|
+
labels = "%"
|
|
569
1209
|
|
|
570
1210
|
# ----- hierarchical forms aggregate per nesting level ---------
|
|
571
1211
|
# from the raw columns, not from the flat table built below
|
|
572
1212
|
if hier_type is not None:
|
|
573
1213
|
if digits_d is None:
|
|
574
1214
|
digits_d = 0 if y_ser is None else 2
|
|
575
|
-
agg = hier_aggregate(x_ser,
|
|
1215
|
+
agg = hier_aggregate(x_ser,
|
|
1216
|
+
by=([by_ser] + by_more_sers if by_more
|
|
1217
|
+
else by_ser),
|
|
1218
|
+
y=y_ser, stat=stat,
|
|
576
1219
|
facet=facet_ser,
|
|
577
|
-
x_name=x,
|
|
1220
|
+
x_name=x,
|
|
1221
|
+
by_name=[by] + by_more if by_more else by,
|
|
1222
|
+
y_name=y,
|
|
578
1223
|
facet_name=facet_name,
|
|
579
1224
|
facet_order=facet_order)
|
|
580
|
-
fill_vec = hier_color_resolve(x_order, fill
|
|
1225
|
+
fill_vec = hier_color_resolve(x_order, fill, x=x_ser,
|
|
1226
|
+
y=y_ser, stat=stat)
|
|
1227
|
+
fill_vec_byfac = (None if facet_ser is None else
|
|
1228
|
+
hier_color_resolve_byfac(
|
|
1229
|
+
x_order, fill, x_ser, y_ser, stat,
|
|
1230
|
+
facet_ser, facet_order))
|
|
581
1231
|
if main is None:
|
|
582
|
-
main = build_title(x, by_name=
|
|
583
|
-
facet_name=facet_name)
|
|
1232
|
+
main = build_title(x, by_name=by_label, y_name=y,
|
|
1233
|
+
stat=stat, facet_name=facet_name)
|
|
584
1234
|
elif main == "":
|
|
585
1235
|
main = None
|
|
586
1236
|
# Chart resolves the labels default before hier sees it:
|
|
@@ -590,6 +1240,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
590
1240
|
# keep the inner rings meaningful.
|
|
591
1241
|
return hier_plotly(
|
|
592
1242
|
agg, fill_vec, type=hier_type,
|
|
1243
|
+
fill_vec_byfac=fill_vec_byfac,
|
|
593
1244
|
x_name=x, by_name=by, facet_name=facet_name,
|
|
594
1245
|
main=main, border=color, digits_d=digits_d,
|
|
595
1246
|
labels="%" if labels is None else labels,
|
|
@@ -604,32 +1255,95 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
604
1255
|
# (hier forms handled above; pie facet became the by grouping)
|
|
605
1256
|
|
|
606
1257
|
if facet_ser is not None and form == "bar":
|
|
607
|
-
# Trellis bar chart: horizontal
|
|
608
|
-
#
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
1258
|
+
# Trellis bar chart: horizontal bars per panel, the plotly
|
|
1259
|
+
# port of R's .bar.lattice rendering. Counts of x, or the
|
|
1260
|
+
# stat of y aggregated within each panel; a by variable
|
|
1261
|
+
# divides each bar into its levels, as the bar chart of a
|
|
1262
|
+
# single panel does, drawn within every panel
|
|
1263
|
+
agg_y = y_ser is not None and stat is not None
|
|
1264
|
+
if y_ser is not None and not agg_y and not is_agg:
|
|
1265
|
+
raise ValueError(
|
|
1266
|
+
"The data are not a summary (pivot) table, so "
|
|
1267
|
+
'specify stat to define the aggregation, e.g., '
|
|
1268
|
+
'stat="mean"')
|
|
1269
|
+
|
|
1270
|
+
def cell_tbl(m):
|
|
1271
|
+
xs = x_ser[m]
|
|
1272
|
+
if by_ser is None:
|
|
1273
|
+
if y_ser is None:
|
|
1274
|
+
t = xs.groupby(xs, observed=True).size()
|
|
1275
|
+
elif agg_y:
|
|
1276
|
+
t = STAT_FUN[stat](y_ser[m].groupby(xs,
|
|
1277
|
+
observed=True))
|
|
1278
|
+
else:
|
|
1279
|
+
t = y_ser[m].groupby(xs, observed=True).first()
|
|
1280
|
+
return t.reindex(x_order)
|
|
1281
|
+
df = pd.DataFrame({"b": by_ser[m], "x": xs,
|
|
1282
|
+
"y": 1.0 if y_ser is None
|
|
1283
|
+
else y_ser[m].astype(float)})
|
|
1284
|
+
g = df.groupby(["b", "x"], observed=True)["y"]
|
|
1285
|
+
t = (g.sum() if y_ser is None
|
|
1286
|
+
else STAT_FUN[stat](g) if agg_y else g.first())
|
|
1287
|
+
t = t.unstack("x").reindex(index=by_order,
|
|
1288
|
+
columns=x_order)
|
|
1289
|
+
if y_ser is None:
|
|
1290
|
+
t = t.fillna(0)
|
|
1291
|
+
if stat_x == "proportion" and not stack100:
|
|
1292
|
+
# every cell of the panel as a share of the panel,
|
|
1293
|
+
# as lattice's prop.table(margin=panel)
|
|
1294
|
+
tot = t.to_numpy().sum()
|
|
1295
|
+
t = t / tot if tot > 0 else t
|
|
1296
|
+
if stack100:
|
|
1297
|
+
# each bar scaled to its own total compares
|
|
1298
|
+
# composition rather than magnitude
|
|
1299
|
+
tot = t.sum(axis=0)
|
|
1300
|
+
t = t.div(tot.where(tot != 0), axis=1)
|
|
1301
|
+
return t
|
|
1302
|
+
|
|
1303
|
+
tbls = {lv: cell_tbl(facet_ser == lv) for lv in facet_order}
|
|
1304
|
+
if by_ser is None:
|
|
1305
|
+
tbl = pd.DataFrame([tbls[lv] for lv in facet_order],
|
|
1306
|
+
index=facet_order)
|
|
1307
|
+
by_tbls = None
|
|
1308
|
+
else:
|
|
1309
|
+
# the panel totals stand in for the shape of the grid
|
|
1310
|
+
tbl = pd.DataFrame([tbls[lv].sum(axis=0)
|
|
1311
|
+
for lv in facet_order],
|
|
1312
|
+
index=facet_order)
|
|
1313
|
+
by_tbls = tbls
|
|
612
1314
|
tbl.index.name = facet_name
|
|
613
1315
|
tbl.columns.name = x
|
|
1316
|
+
if stack100:
|
|
1317
|
+
val_name = f"Proportion within {x}"
|
|
1318
|
+
elif agg_y:
|
|
1319
|
+
val_name = f"{STAT_LBL[stat]} of {y}"
|
|
1320
|
+
elif y_ser is not None:
|
|
1321
|
+
val_name = y
|
|
1322
|
+
elif by_ser is not None:
|
|
1323
|
+
# named for x, as lattice's "Count of" / "Proportion of"
|
|
1324
|
+
val_name = ("Proportion of " if stat_x == "proportion"
|
|
1325
|
+
else "Count of ") + x
|
|
1326
|
+
else:
|
|
1327
|
+
val_name = None # Count/Proportion of x
|
|
614
1328
|
return bc_facet_plotly(
|
|
615
1329
|
tbl, x_name=x, facet_name=facet_name,
|
|
616
1330
|
x_lab=xlab, y_lab=ylab, fill=fill,
|
|
617
1331
|
border="off" if color is None else color,
|
|
618
1332
|
opacity=(None if transparency is None
|
|
619
1333
|
else 1 - transparency),
|
|
620
|
-
proportion=stat_x == "proportion",
|
|
1334
|
+
proportion=stat_x == "proportion" and by_ser is None,
|
|
621
1335
|
digits_d=(digits_d if digits_d is not None
|
|
622
|
-
else
|
|
1336
|
+
else 2 if (y_ser is not None or stack100
|
|
1337
|
+
or stat_x != "count") else 0),
|
|
623
1338
|
n_col=n_col_use or 1,
|
|
624
1339
|
axis_fmt=axis_fmt, axis_x_pre=axis_x_pre,
|
|
625
1340
|
rotate_x=rotate_x, rotate_y=rotate_y,
|
|
626
1341
|
main=main,
|
|
1342
|
+
by_tbls=by_tbls, by_name=by, beside=beside,
|
|
1343
|
+
val_name=val_name, legend_title=legend_title,
|
|
627
1344
|
)
|
|
628
1345
|
|
|
629
1346
|
if facet_ser is not None and form == "radar":
|
|
630
|
-
if stat_x == "proportion" and by_ser is not None:
|
|
631
|
-
raise NotImplementedError(
|
|
632
|
-
'stat_x="proportion" with by= is not yet ported')
|
|
633
1347
|
facets = {
|
|
634
1348
|
str(lv): _facet_table(
|
|
635
1349
|
x_ser[facet_ser == lv],
|
|
@@ -707,54 +1421,154 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
707
1421
|
n_col=n_col_use,
|
|
708
1422
|
)
|
|
709
1423
|
|
|
1424
|
+
if facet_ser is not None and form == "profile":
|
|
1425
|
+
# one profile per group within every panel, on the categories
|
|
1426
|
+
# and value scale all panels share. Without y the value is a
|
|
1427
|
+
# count; with y and no stat the values are supplied directly,
|
|
1428
|
+
# one per cell. R analog: Chart.R faceted profile, drawn by
|
|
1429
|
+
# .plt.profile.facet() in base R; here a plotly grid
|
|
1430
|
+
agg_y = not is_agg and stat is not None
|
|
1431
|
+
if y_ser is not None and not agg_y and not is_agg:
|
|
1432
|
+
raise ValueError(
|
|
1433
|
+
"The data are not a summary (pivot) table, so "
|
|
1434
|
+
'specify stat to define the aggregation, e.g., '
|
|
1435
|
+
'stat="mean"')
|
|
1436
|
+
series = ([str(b) for b in by_order] if by_ser is not None
|
|
1437
|
+
else [None])
|
|
1438
|
+
x_lv = [str(c) for c in x_order]
|
|
1439
|
+
pos = {c: k + 1 for k, c in enumerate(x_lv)}
|
|
1440
|
+
df = pd.DataFrame({
|
|
1441
|
+
"f": facet_ser.astype(str), "x": x_ser.astype(str),
|
|
1442
|
+
"g": (by_ser.astype(str) if by_ser is not None
|
|
1443
|
+
else "\0"),
|
|
1444
|
+
"y": 1.0 if y_ser is None else y_ser.astype(float)})
|
|
1445
|
+
g = df.groupby(["f", "g", "x"], observed=True)["y"]
|
|
1446
|
+
t = (g.sum() if y_ser is None
|
|
1447
|
+
else STAT_FUN[stat](g) if agg_y else g.mean())
|
|
1448
|
+
if y_ser is None and stat_x == "proportion":
|
|
1449
|
+
# each series is its group's distribution over x in that
|
|
1450
|
+
# panel; without by, the panel's own distribution
|
|
1451
|
+
t = t / t.groupby(level=["f", "g"]).transform("sum")
|
|
1452
|
+
cells = {}
|
|
1453
|
+
for (f, gg, xx), v in t.items():
|
|
1454
|
+
nm = None if by_ser is None else gg
|
|
1455
|
+
xs, ys = cells.setdefault(f, {}).setdefault(nm, ([], []))
|
|
1456
|
+
xs.append(pos[xx])
|
|
1457
|
+
ys.append(float(v))
|
|
1458
|
+
for f in cells: # draw left to right
|
|
1459
|
+
for nm, (xs, ys) in cells[f].items():
|
|
1460
|
+
o = np.argsort(xs)
|
|
1461
|
+
cells[f][nm] = ([xs[k] for k in o], [ys[k] for k in o])
|
|
1462
|
+
vals = t.to_numpy(dtype=float)
|
|
1463
|
+
lo, hi = float(np.nanmin(vals)), float(np.nanmax(vals))
|
|
1464
|
+
# the panels share one value scale, so the origin widens it
|
|
1465
|
+
if origin_y is not None:
|
|
1466
|
+
lo, hi = min(lo, origin_y), max(hi, origin_y)
|
|
1467
|
+
y_tick = pretty(lo, hi)
|
|
1468
|
+
if digits_d is None:
|
|
1469
|
+
digits_d = 0 if y_ser is None else 2
|
|
1470
|
+
from .plotly_utils import axis_format
|
|
1471
|
+
y_lab = (ylab if ylab is not None
|
|
1472
|
+
else f"Proportion of {x}" if (y_ser is None
|
|
1473
|
+
and stat_x == "proportion")
|
|
1474
|
+
else "Count" if y_ser is None
|
|
1475
|
+
else f"{STAT_LBL[stat]} of {y}" if agg_y else y)
|
|
1476
|
+
if y_ser is None and stat_x == "proportion" and digits_d == 0:
|
|
1477
|
+
digits_d = 2
|
|
1478
|
+
return profile_facet_plotly(
|
|
1479
|
+
cells, x_lv, series, y_tick,
|
|
1480
|
+
axis_format(y_tick, digits_d, axis_fmt, axis_y_pre),
|
|
1481
|
+
connect=True if segments is None else bool(segments),
|
|
1482
|
+
x_lab=x if xlab is None else xlab, y_lab=y_lab,
|
|
1483
|
+
by_name=by,
|
|
1484
|
+
fill=(fill if isinstance(fill, (list, tuple))
|
|
1485
|
+
else [fill] if fill is not None
|
|
1486
|
+
else _profile_fill(theme)),
|
|
1487
|
+
pt_size=1.5 * pt_size,
|
|
1488
|
+
facet_levels=[str(f) for f in facet_order],
|
|
1489
|
+
facet_name=facet_name, n_col=n_col_use,
|
|
1490
|
+
main=None if not main else main, rotate_x=rotate_x)
|
|
1491
|
+
|
|
710
1492
|
if facet_ser is not None and form == "dot":
|
|
711
1493
|
# aggregate and sort per facet panel; a panel shows only
|
|
712
|
-
# its own categories.
|
|
1494
|
+
# its own categories. With by=, one series per level of by in
|
|
1495
|
+
# every panel, on the full x-by-panel grid so a combination
|
|
1496
|
+
# absent from the data is an empty cell. R analog: Chart.R
|
|
1497
|
+
# faceted dot prep and its by reshape
|
|
713
1498
|
prop = stat_x == "proportion"
|
|
1499
|
+
agg_y = not is_agg and stat is not None
|
|
714
1500
|
cats_l, vals_l, fac_l = [], [], []
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
t =
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1501
|
+
if by_ser is not None:
|
|
1502
|
+
rows = []
|
|
1503
|
+
for lv in facet_order:
|
|
1504
|
+
m = facet_ser == lv
|
|
1505
|
+
df = pd.DataFrame({"x": x_ser[m].astype(str),
|
|
1506
|
+
"by": by_ser[m].astype(str),
|
|
1507
|
+
"y": (1.0 if y_ser is None
|
|
1508
|
+
else y_ser[m].astype(float))})
|
|
1509
|
+
g = df.groupby(["x", "by"], observed=True)["y"]
|
|
1510
|
+
t = (g.sum() if y_ser is None
|
|
1511
|
+
else STAT_FUN[stat](g) if agg_y else g.first())
|
|
1512
|
+
wide = (t.unstack("by")
|
|
1513
|
+
.reindex(index=[str(c) for c in x_order],
|
|
1514
|
+
columns=[str(b) for b in by_order]))
|
|
1515
|
+
if prop and y_ser is None:
|
|
1516
|
+
# each group's distribution over x in this panel
|
|
1517
|
+
tot = wide.sum(axis=0)
|
|
1518
|
+
wide = wide.div(tot.where(tot != 0), axis=1)
|
|
1519
|
+
rows.append(wide)
|
|
1520
|
+
cats_l += list(wide.index)
|
|
1521
|
+
fac_l += [str(lv)] * len(wide)
|
|
1522
|
+
vals_l = pd.concat(rows, ignore_index=True)
|
|
1523
|
+
all_vals = vals_l.to_numpy(dtype=float).ravel()
|
|
1524
|
+
else:
|
|
1525
|
+
for lv in facet_order:
|
|
1526
|
+
m = facet_ser == lv
|
|
1527
|
+
xs = x_ser[m].astype(str)
|
|
1528
|
+
if y_ser is None: # counts per panel
|
|
1529
|
+
t = xs.groupby(xs).size()
|
|
1530
|
+
if prop:
|
|
1531
|
+
tot = t.sum()
|
|
1532
|
+
t = t / tot if tot > 0 else t.astype(float)
|
|
1533
|
+
elif agg_y:
|
|
1534
|
+
t = STAT_FUN[stat](y_ser[m].groupby(xs))
|
|
1535
|
+
else: # pre-aggregated rows
|
|
1536
|
+
t = pd.Series(y_ser[m].to_numpy(dtype=float),
|
|
1537
|
+
index=xs.values)
|
|
1538
|
+
if sort != "0":
|
|
1539
|
+
t = t.sort_values(ascending=(sort == "+"),
|
|
1540
|
+
kind="stable")
|
|
1541
|
+
cats_l += [str(c) for c in t.index]
|
|
1542
|
+
vals_l += [float(v) for v in t.to_numpy()]
|
|
1543
|
+
fac_l += [str(lv)] * len(t)
|
|
1544
|
+
all_vals = vals_l
|
|
1545
|
+
|
|
1546
|
+
# an aggregated value is labeled by its statistic, as the
|
|
1547
|
+
# unfaceted chart is: "Mean of Salary", not "Salary"
|
|
1548
|
+
if y_ser is None:
|
|
1549
|
+
val_lab = (f"Proportion of {x}" if prop
|
|
1550
|
+
else "Count" if by_ser is not None
|
|
1551
|
+
else f"Count of {x}")
|
|
1552
|
+
else:
|
|
1553
|
+
val_lab = f"{STAT_LBL[stat]} of {y}" if agg_y else y
|
|
737
1554
|
if horiz:
|
|
738
1555
|
orientation = "h"
|
|
739
|
-
x_lab_arg = xlab if xlab is not None
|
|
740
|
-
|
|
1556
|
+
x_lab_arg = (xlab if xlab is not None
|
|
1557
|
+
else ylab if ylab is not None else val_lab)
|
|
1558
|
+
y_lab_arg = x
|
|
741
1559
|
else:
|
|
742
1560
|
orientation = "v"
|
|
743
1561
|
x_lab_arg = xlab if xlab is not None else x
|
|
744
1562
|
y_lab_arg = ylab if ylab is not None else val_lab
|
|
745
1563
|
|
|
1564
|
+
# the length of the segment carries the value, so the value
|
|
1565
|
+
# axis begins at zero unless an origin is specified
|
|
1566
|
+
org_in = origin_x if horiz else origin_y
|
|
746
1567
|
origin, gridT = _dot_origin_grid(
|
|
747
|
-
|
|
748
|
-
origin_in=origin_x if horiz else origin_y,
|
|
1568
|
+
all_vals, origin_in=0 if org_in is None else org_in,
|
|
749
1569
|
is_counts=True if y_ser is None else None)
|
|
750
1570
|
if digits_d is None:
|
|
751
1571
|
digits_d = 2 if (y_ser is not None or prop) else 0
|
|
752
|
-
if main is None:
|
|
753
|
-
main = build_title(
|
|
754
|
-
x, y_name="Proportion" if prop else y,
|
|
755
|
-
stat=stat, facet_name=facet_name)
|
|
756
|
-
elif main == "":
|
|
757
|
-
main = None
|
|
758
1572
|
return dot_plotly(
|
|
759
1573
|
cats_l, vals_l, orientation=orientation,
|
|
760
1574
|
fill=fill, border=color, pt_size=pt_size,
|
|
@@ -763,7 +1577,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
763
1577
|
pt_opacity=1 - (get_option("trans_pt_fill", 0.10)
|
|
764
1578
|
if transparency is None
|
|
765
1579
|
else transparency),
|
|
766
|
-
gridT=gridT, origin_x=origin,
|
|
1580
|
+
gridT=gridT, origin_x=origin,
|
|
1581
|
+
main=None if not main else main,
|
|
1582
|
+
legend_title=(legend_title if legend_title is not None
|
|
1583
|
+
else by),
|
|
767
1584
|
facet=fac_l, facet_name=facet_name, n_col=n_col_use,
|
|
768
1585
|
axis_fmt=axis_fmt, axis_x_pre=axis_x_pre,
|
|
769
1586
|
axis_y_pre=axis_y_pre,
|
|
@@ -780,9 +1597,6 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
780
1597
|
|
|
781
1598
|
# ----- build the table the renderer receives ------------------
|
|
782
1599
|
if y_ser is None: # counts of x
|
|
783
|
-
if stat_x == "proportion" and by_ser is not None:
|
|
784
|
-
raise NotImplementedError(
|
|
785
|
-
'stat_x="proportion" with by= is not yet ported')
|
|
786
1600
|
|
|
787
1601
|
if by_ser is None:
|
|
788
1602
|
tbl = (x_ser.groupby(x_ser).size()
|
|
@@ -802,11 +1616,20 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
802
1616
|
tbl = pd.crosstab(by_ser, x_ser)
|
|
803
1617
|
tbl = tbl.reindex(index=by_order, columns=x_order,
|
|
804
1618
|
fill_value=0)
|
|
805
|
-
|
|
1619
|
+
if stat_x == "proportion":
|
|
1620
|
+
# each group's distribution over x, so the groups are
|
|
1621
|
+
# compared whatever their sizes: every by row sums
|
|
1622
|
+
# to 1 (the bar chart took its own path above, the
|
|
1623
|
+
# 100% stacked chart within each bar)
|
|
1624
|
+
tot = tbl.sum(axis=1)
|
|
1625
|
+
tbl = tbl.div(tot.where(tot != 0), axis=0).fillna(0)
|
|
1626
|
+
y_name = "Proportion"
|
|
1627
|
+
else:
|
|
1628
|
+
y_name = "Count"
|
|
806
1629
|
if ylab is None:
|
|
807
|
-
ylab = f"
|
|
1630
|
+
ylab = f"{y_name} of {x}"
|
|
808
1631
|
if digits_d is None:
|
|
809
|
-
digits_d = 0
|
|
1632
|
+
digits_d = 0 if stat_x == "count" else 2
|
|
810
1633
|
|
|
811
1634
|
elif stat == "deviation": # R analog: .agg()
|
|
812
1635
|
if by_ser is not None:
|
|
@@ -838,7 +1661,11 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
838
1661
|
else:
|
|
839
1662
|
tbl = fun(df.groupby(["by", "x"])["y"]).unstack("x")
|
|
840
1663
|
tbl = tbl.reindex(index=by_order, columns=x_order)
|
|
841
|
-
|
|
1664
|
+
# a profile or dot plot of several groups draws an empty
|
|
1665
|
+
# cell as a missing point; the other forms need every cell
|
|
1666
|
+
gaps_ok = form in ("profile", "dot") and by_ser is not None
|
|
1667
|
+
if not gaps_ok and not np.isfinite(
|
|
1668
|
+
tbl.to_numpy(dtype=float)).all():
|
|
842
1669
|
raise ValueError(
|
|
843
1670
|
"The summary table of the transformed data has "
|
|
844
1671
|
"missing or non-finite values, likely because some "
|
|
@@ -882,7 +1709,9 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
882
1709
|
elif beside:
|
|
883
1710
|
ylab = "Percentage"
|
|
884
1711
|
else:
|
|
885
|
-
|
|
1712
|
+
# the axis holds proportions, so it is named for them;
|
|
1713
|
+
# the legend names by, so it is not repeated here
|
|
1714
|
+
ylab = f"Proportion within {x}"
|
|
886
1715
|
if digits_d_user is None:
|
|
887
1716
|
digits_d = 2
|
|
888
1717
|
|
|
@@ -925,6 +1754,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
925
1754
|
labels_decimals=labels_decimals,
|
|
926
1755
|
labels_size=0.90 if labels_size is None
|
|
927
1756
|
else labels_size,
|
|
1757
|
+
shares=(y_ser is None and stat_x == "proportion"),
|
|
928
1758
|
)
|
|
929
1759
|
|
|
930
1760
|
if form == "radar":
|
|
@@ -954,32 +1784,122 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
954
1784
|
digits_d=digits_d, val_label=val_label,
|
|
955
1785
|
)
|
|
956
1786
|
|
|
1787
|
+
if form == "profile":
|
|
1788
|
+
# one point per category of x, connected across the
|
|
1789
|
+
# categories, one profile per level of by: the interaction
|
|
1790
|
+
# plot of the analysis of variance. The connecting segments
|
|
1791
|
+
# are the purpose of the form. R analog: Chart.R profile ->
|
|
1792
|
+
# .plt.main(cat.x=TRUE, segments=TRUE) -> plt.plotly()
|
|
1793
|
+
if isinstance(tbl, pd.DataFrame):
|
|
1794
|
+
groups = [(str(b), tbl.columns, tbl.loc[b])
|
|
1795
|
+
for b in tbl.index]
|
|
1796
|
+
else:
|
|
1797
|
+
groups = [(None, tbl.index, tbl)]
|
|
1798
|
+
cats = [str(c) for c in groups[0][1]]
|
|
1799
|
+
pos = list(range(1, len(cats) + 1))
|
|
1800
|
+
# an empty cell stays NaN, which plotly draws as a gap
|
|
1801
|
+
groups = [(nm, pos, vals.to_numpy(dtype=float).tolist())
|
|
1802
|
+
for nm, _, vals in groups]
|
|
1803
|
+
vals = tbl.to_numpy(dtype=float).ravel()
|
|
1804
|
+
lo, hi = float(np.nanmin(vals)), float(np.nanmax(vals))
|
|
1805
|
+
# the value axis of a profile is y, so origin_y sets where
|
|
1806
|
+
# it begins, as it does for the dot chart
|
|
1807
|
+
if origin_y is not None:
|
|
1808
|
+
lo, hi = min(lo, origin_y), max(hi, origin_y)
|
|
1809
|
+
axT2 = pretty(lo, hi)
|
|
1810
|
+
from .plotly_utils import axis_format
|
|
1811
|
+
y_lab = (ylab_user if ylab_user is not None
|
|
1812
|
+
else "Count" if y_ser is None and stat_x == "count"
|
|
1813
|
+
else ylab)
|
|
1814
|
+
fig = plt_plotly(
|
|
1815
|
+
groups, by_name=by,
|
|
1816
|
+
fill=(fill if isinstance(fill, (list, tuple))
|
|
1817
|
+
else [fill] if fill is not None
|
|
1818
|
+
else _profile_fill(theme)),
|
|
1819
|
+
pt_size=1.5 * pt_size,
|
|
1820
|
+
x_lab=x if xlab is None else xlab, y_lab=y_lab,
|
|
1821
|
+
ax={"axT1": pos, "axL1": cats, "axT2": axT2,
|
|
1822
|
+
"axL2": axis_format(axT2, digits_d, axis_fmt,
|
|
1823
|
+
axis_y_pre)},
|
|
1824
|
+
gridT1=pos, gridT2=axT2,
|
|
1825
|
+
# no automatic title: it would only repeat the axis labels
|
|
1826
|
+
main=None if not main else main,
|
|
1827
|
+
digits_d=digits_d,
|
|
1828
|
+
connect=True if segments is None else bool(segments),
|
|
1829
|
+
pt_opacity=1,
|
|
1830
|
+
style_opts=None)
|
|
1831
|
+
pad = 0.04 * (axT2[-1] - axT2[0])
|
|
1832
|
+
fig.update_yaxes(range=[axT2[0] - pad, axT2[-1] + pad])
|
|
1833
|
+
fig.update_xaxes(range=[0.5, len(cats) + 0.5])
|
|
1834
|
+
if rotate_x:
|
|
1835
|
+
fig.update_xaxes(tickangle=-rotate_x)
|
|
1836
|
+
if rotate_y:
|
|
1837
|
+
fig.update_yaxes(tickangle=-rotate_y)
|
|
1838
|
+
return fig
|
|
1839
|
+
|
|
957
1840
|
if form == "dot":
|
|
958
|
-
|
|
1841
|
+
quiet_use = resolve_quiet(quiet)
|
|
1842
|
+
pt_op = 1 - (get_option("trans_pt_fill", 0.10)
|
|
1843
|
+
if transparency is None else transparency)
|
|
1844
|
+
if isinstance(tbl, pd.DataFrame):
|
|
1845
|
+
# by=: one series of dots per level of by, drawn as the
|
|
1846
|
+
# paired geometry, one column per level. A two-level by
|
|
1847
|
+
# with a stat already lists its difference as the Diff
|
|
1848
|
+
# row of the summary table. R analog: Chart.R dot by
|
|
1849
|
+
# reshape into the paired path
|
|
1850
|
+
ydf = tbl.T.reindex([c for c in x_order if c in tbl.columns])
|
|
1851
|
+
ydf.columns = [str(c) for c in ydf.columns]
|
|
1852
|
+
cats = [str(c) for c in ydf.index]
|
|
1853
|
+
val_lab = (xlab if xlab else ylab_user if ylab_user
|
|
1854
|
+
else ylab if (y_ser is not None
|
|
1855
|
+
or stat_x == "proportion")
|
|
1856
|
+
else "Count")
|
|
1857
|
+
return _dot_paired(
|
|
1858
|
+
cats, ydf.reset_index(drop=True), sort, sort_miss,
|
|
1859
|
+
origin_x,
|
|
1860
|
+
show_diff=y_ser is None, quiet=quiet,
|
|
1861
|
+
x_name=x,
|
|
1862
|
+
fill=fill, border=color, pt_size=pt_size,
|
|
1863
|
+
x_lab=val_lab, y_lab=x, digits_d=digits_d,
|
|
1864
|
+
pt_opacity=pt_op,
|
|
1865
|
+
main=None if not main else main,
|
|
1866
|
+
legend_title=(legend_title if legend_title is not None
|
|
1867
|
+
else by),
|
|
1868
|
+
axis_fmt=axis_fmt, axis_x_pre=axis_x_pre,
|
|
1869
|
+
rotate_x=rotate_x, rotate_y=rotate_y,
|
|
1870
|
+
segments_x=(True if segments_x is None
|
|
1871
|
+
else segments_x),
|
|
1872
|
+
)
|
|
1873
|
+
|
|
959
1874
|
cats = [str(c) for c in tbl.index]
|
|
960
1875
|
vals = tbl.to_numpy(dtype=float)
|
|
1876
|
+
# the length of the segment carries the value, so the value
|
|
1877
|
+
# axis begins at zero unless an origin is specified
|
|
1878
|
+
org_in = origin_x if horiz else origin_y
|
|
961
1879
|
origin, gridT = _dot_origin_grid(
|
|
962
|
-
vals,
|
|
963
|
-
origin_in=origin_x if horiz else origin_y,
|
|
1880
|
+
vals, origin_in=0 if org_in is None else org_in,
|
|
964
1881
|
is_counts=True if y_ser is None else None)
|
|
965
|
-
cat_lab = x if xlab is None else xlab
|
|
966
1882
|
val_lab = ylab # set with tbl above
|
|
967
|
-
if
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1883
|
+
if horiz:
|
|
1884
|
+
# the value axis takes the plotted quantity, from xlab or
|
|
1885
|
+
# the statistic; the category axis takes x's own name
|
|
1886
|
+
x_lab_arg = xlab if xlab is not None else val_lab
|
|
1887
|
+
y_lab_arg = x
|
|
1888
|
+
else:
|
|
1889
|
+
x_lab_arg = xlab if xlab is not None else x
|
|
1890
|
+
y_lab_arg = val_lab
|
|
1891
|
+
# the value axis already names the plotted quantity, so an
|
|
1892
|
+
# unrequested title would only repeat it
|
|
971
1893
|
return dot_plotly(
|
|
972
1894
|
cats, vals,
|
|
973
1895
|
orientation="h" if horiz else "v",
|
|
974
1896
|
fill=fill, border=color,
|
|
975
1897
|
pt_size=pt_size,
|
|
976
|
-
x_lab=
|
|
977
|
-
y_lab=cat_lab if horiz else val_lab,
|
|
1898
|
+
x_lab=x_lab_arg, y_lab=y_lab_arg,
|
|
978
1899
|
digits_d=digits_d,
|
|
979
|
-
pt_opacity=
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
gridT=gridT, origin_x=origin, main=main,
|
|
1900
|
+
pt_opacity=pt_op,
|
|
1901
|
+
gridT=gridT, origin_x=origin,
|
|
1902
|
+
main=None if not main else main,
|
|
983
1903
|
axis_fmt=axis_fmt, axis_x_pre=axis_x_pre,
|
|
984
1904
|
axis_y_pre=axis_y_pre,
|
|
985
1905
|
rotate_x=rotate_x, rotate_y=rotate_y,
|
|
@@ -1019,8 +1939,16 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1019
1939
|
if (fill_split is not None or fill_scaled) \
|
|
1020
1940
|
and isinstance(tbl, pd.Series):
|
|
1021
1941
|
from .getColors import bar_fill_colors
|
|
1022
|
-
fill
|
|
1023
|
-
|
|
1942
|
+
# one fill repeated over the bars is a single color; more
|
|
1943
|
+
# than two leave the hue to the theme, as R's .bc.main()
|
|
1944
|
+
f_in = fill
|
|
1945
|
+
if isinstance(fill, (list, tuple)):
|
|
1946
|
+
uniq = list(dict.fromkeys(fill))
|
|
1947
|
+
f_in = (uniq[0] if len(uniq) == 1
|
|
1948
|
+
else None if len(uniq) > 2 else uniq)
|
|
1949
|
+
fill = bar_fill_colors(tbl.to_numpy(dtype=float), f_in,
|
|
1950
|
+
fill_split, fill_scaled, fill_chroma,
|
|
1951
|
+
theme=theme)
|
|
1024
1952
|
|
|
1025
1953
|
fig = bc_plotly(
|
|
1026
1954
|
tbl,
|
|
@@ -1041,6 +1969,11 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1041
1969
|
labels_size=0.90 if labels_size is None else labels_size,
|
|
1042
1970
|
labels_color=labels_color,
|
|
1043
1971
|
labels_decimals=labels_decimals,
|
|
1972
|
+
# applied only when given; R cuts by share, which a single
|
|
1973
|
+
# series of a statistic does not carry, nor outside labels
|
|
1974
|
+
labels_cut=(None if (labels_position == "out" or (
|
|
1975
|
+
isinstance(tbl, pd.Series) and y_ser is not None))
|
|
1976
|
+
else labels_cut),
|
|
1044
1977
|
legend_title=legend_title, legend_position=legend_position,
|
|
1045
1978
|
legend_labels=legend_labels, legend_horiz=legend_horiz,
|
|
1046
1979
|
legend_size=legend_size, legend_abbrev=legend_abbrev,
|
|
@@ -1052,4 +1985,5 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1052
1985
|
|
|
1053
1986
|
|
|
1054
1987
|
# font_size= scales all text of the returned figure
|
|
1055
|
-
|
|
1988
|
+
from .style import with_theme as _with_theme # noqa: E402
|
|
1989
|
+
Chart = font_scaled(attach_stats(_with_theme(Chart, own_param=True)))
|