lessPython 0.2.0__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.
Files changed (48) hide show
  1. lessPy/ANOVA.py +5 -5
  2. lessPy/Chart.py +386 -88
  3. lessPy/Correlation.py +2 -2
  4. lessPy/Logit.py +2 -2
  5. lessPy/Regression.py +6 -4
  6. lessPy/X.py +279 -42
  7. lessPy/XY.py +814 -158
  8. lessPy/__init__.py +12 -2
  9. lessPy/_themes.py +5007 -0
  10. lessPy/bc_items_plotly.py +31 -18
  11. lessPy/bc_plotly.py +23 -7
  12. lessPy/bubble_plotly.py +17 -2
  13. lessPy/corCFA.py +3 -3
  14. lessPy/corProp.py +2 -2
  15. lessPy/corReflect.py +2 -2
  16. lessPy/corReorder.py +2 -2
  17. lessPy/corScree.py +3 -3
  18. lessPy/data/StockPrice.csv +1401 -1386
  19. lessPy/details.py +17 -4
  20. lessPy/dn_plotly.py +65 -45
  21. lessPy/freq_poly_plotly.py +12 -5
  22. lessPy/getColors.py +247 -20
  23. lessPy/hier_plotly.py +36 -9
  24. lessPy/hs_plotly.py +16 -4
  25. lessPy/plotly_utils.py +78 -9
  26. lessPy/plt_mat_plotly.py +74 -2
  27. lessPy/plt_time.py +10 -3
  28. lessPy/prob_norm.py +3 -3
  29. lessPy/prob_tcut.py +3 -3
  30. lessPy/prob_znorm.py +3 -1
  31. lessPy/rcolors.py +461 -0
  32. lessPy/simCImean.py +3 -3
  33. lessPy/simCLT.py +3 -3
  34. lessPy/simFlips.py +3 -3
  35. lessPy/simMeans.py +3 -3
  36. lessPy/stats_out.py +257 -11
  37. lessPy/style.py +160 -0
  38. lessPy/sunflower_plotly.py +140 -0
  39. lessPy/ttest.py +5 -5
  40. lessPy/utils.py +43 -6
  41. lessPy/vbs_plotly.py +16 -5
  42. lessPy/x_console.py +763 -0
  43. {lesspython-0.2.0.dist-info → lesspython-0.3.0.dist-info}/METADATA +6 -2
  44. lesspython-0.3.0.dist-info/RECORD +89 -0
  45. lesspython-0.2.0.dist-info/RECORD +0 -84
  46. {lesspython-0.2.0.dist-info → lesspython-0.3.0.dist-info}/WHEEL +0 -0
  47. {lesspython-0.2.0.dist-info → lesspython-0.3.0.dist-info}/licenses/LICENSE +0 -0
  48. {lesspython-0.2.0.dist-info → lesspython-0.3.0.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, hier_plotly,
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 chart_stats, resolve_quiet
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 (no y, no by)
84
- to sum to 1, the faceted stat_x="proportion". Used by the
85
- faceted radar and bubble paths."""
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
- return (t.reindex(index=by_order, columns=x_order)
112
- .fillna(0))
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 == "bubble":
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
- if show:
221
- # the table lists the items top of the chart first
222
- print("\n".join(_items_print(
223
- frq[order[::-1]], wm[order[::-1]], numeric)))
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 show:
272
- if w_lv is None:
273
- w_lv = pd.Series(np.nan, index=items)
274
- print("\n".join(_items_print(
275
- f_lv, w_lv, numeric,
276
- f"Frequencies of Responses by Variable, "
277
- f"{facet_name}: {lv}")))
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
- tbls[lv] = t[items[::-1]] # first item at the top
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,85 @@ 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 (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
300
473
 
301
474
 
302
475
  def _items_print(frq, wm, numeric, title=None):
@@ -305,6 +478,15 @@ def _items_print(frq, wm, numeric, title=None):
305
478
  numeric)
306
479
 
307
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
+
308
490
  def _profile_fill(theme):
309
491
  """One hue per series from the theme, as R's profile does:
310
492
  .color_range(.get_fill(theme), n), the qualitative hues for the
@@ -352,7 +534,7 @@ def _dot_diff_lines(cats, ydf, x_name):
352
534
 
353
535
 
354
536
  def _dot_paired(cats, ydf, sort, sort_miss, origin_x, show_diff,
355
- x_name, **render):
537
+ x_name, quiet=None, **render):
356
538
  """Order a multi-series dot plot and draw it. Two series are a
357
539
  pair, read for the gap between them, so by default they are
358
540
  ordered by that difference, ascending so the largest positive
@@ -370,8 +552,23 @@ def _dot_paired(cats, ydf, sort, sort_miss, origin_x, show_diff,
370
552
  kind="stable")
371
553
  cats = [cats[i] for i in order]
372
554
  ydf = ydf.iloc[order]
373
- if dif_pair and show_diff:
374
- print("\n".join(_dot_diff_lines(cats, ydf, x_name)))
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))
375
572
  origin, gridT = _dot_origin_grid(
376
573
  ydf.to_numpy(dtype=float).ravel(),
377
574
  origin_in=0 if (dif_pair and origin_x is None) else origin_x)
@@ -396,7 +593,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
396
593
  theme=None,
397
594
  labels=None, labels_position=None,
398
595
  labels_color=None, labels_size=None, labels_decimals=None,
399
- labels_cut=None, segments=None,
596
+ labels_cut=None, segments=None, one_plot=None,
400
597
  legend_title=None, legend_position=None,
401
598
  legend_labels=None, legend_horiz=False,
402
599
  legend_size=None, legend_abbrev=None, legend_adjust=0,
@@ -453,6 +650,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
453
650
 
454
651
  Variables are strings naming columns of the DataFrame `data`.
455
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.
456
657
  """
457
658
 
458
659
  # ----- validate parameters ------------------------------------
@@ -566,6 +767,13 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
566
767
  raise ValueError('labels_position must be "in" or "out"')
567
768
  if axis_fmt not in ("K", ",", ".", ""):
568
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"
569
777
  if stack100:
570
778
  if form != "bar":
571
779
  raise ValueError('stack100 rescales the bars of a bar '
@@ -662,9 +870,6 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
662
870
  if sort != "0":
663
871
  raise ValueError("Sort not applicable to Trellis "
664
872
  "(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
873
 
669
874
  # ----- resolve variables and filter ---------------------------
670
875
  if filter is not None:
@@ -692,7 +897,8 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
692
897
  stat=stat, sort=sort, sort_miss=sort_miss,
693
898
  stack100=stack100, horiz=horiz, fill=fill, theme=theme,
694
899
  labels=labels, labels_size=labels_size, gap=gap,
695
- labels_cut=labels_cut,
900
+ labels_cut=labels_cut, power=power, radius=radius,
901
+ one_plot=one_plot,
696
902
  xlab=xlab, ylab=ylab, main=main,
697
903
  legend_title=legend_title, n_col=n_col, n_row=n_row,
698
904
  axis_fmt=axis_fmt, axis_x_pre=axis_x_pre,
@@ -737,7 +943,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
737
943
  x_lab_dot = (xlab if xlab else ylab if ylab else val_lab)
738
944
  return _dot_paired(
739
945
  cats, ydf.reset_index(drop=True), sort, sort_miss,
740
- origin_x, show_diff=not resolve_quiet(quiet), x_name=x,
946
+ origin_x, show_diff=True, quiet=quiet, x_name=x,
741
947
  fill=fill, border=color, pt_size=pt_size,
742
948
  x_lab=x_lab_dot, y_lab=x,
743
949
  digits_d=2 if digits_d is None else digits_d,
@@ -814,12 +1020,53 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
814
1020
  facet_order = (_category_order(facet_ser)
815
1021
  if facet_ser is not None else None)
816
1022
 
817
- # theme=: a sequential palette in the theme's hue over the fill
818
- # elements (the by groups if present, else the x categories)
819
- if theme is not None and fill is None:
820
- n_fill = len(by_order) if by_order is not None \
821
- else len(x_order)
822
- fill = _theme_fill(theme, n_fill)
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))
823
1070
 
824
1071
  # explicit panel-grid layout: n_col wins, else derive from
825
1072
  # n_row; None keeps each renderer's own default (single-column
@@ -915,35 +1162,50 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
915
1162
  f"have a numerical variable y='{y}', so specify "
916
1163
  'stat to define the aggregation, e.g., stat="mean"')
917
1164
 
918
- # accompanying statistics: frequencies, or the stat of y
919
- if not resolve_quiet(quiet):
920
- y_out = (y_ser if y_ser is not None
921
- and stat in STAT_FUN else None)
922
- if by_more_sers and (y_ser is None or y_out is not None):
923
- from .stats_out import nested_stats
924
- print("\n".join(nested_stats(
925
- [x_ser, by_ser] + by_more_sers, [x, by] + by_more,
926
- y_out, stat, y,
927
- 2 if digits_d is None else digits_d)))
928
- elif y_ser is None or y_out is not None:
929
- var_lbl = data.attrs.get("variable_labels", {}) or {}
930
- print("\n".join(chart_stats(
931
- x_ser, by_ser, y_out, stat, x, by, y,
932
- 2 if digits_d is None else digits_d,
933
- stack100=stack100, n_miss=n_miss_x,
934
- x_lbl=var_lbl.get(x), y_lbl=var_lbl.get(y),
935
- facet_ser=facet_ser, facet_name=facet_name,
936
- form=form)))
937
-
938
- # labels default for aggregated data; for counts leave labels
939
- # None so bc_plotly shows the value with % lines in hover
940
- # R analog: Chart.R line ~716
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()
941
1201
  if labels is None and (stat is not None or
942
1202
  (is_agg and y_ser is not None)):
943
1203
  # the bars carry a statistic, so the label is its value;
944
1204
  # beside only arranges the bars, and a percentage of a sum
945
1205
  # of means is not a quantity
946
1206
  labels = "input"
1207
+ elif labels is None and form == "bar" and y_ser is None:
1208
+ labels = "%"
947
1209
 
948
1210
  # ----- hierarchical forms aggregate per nesting level ---------
949
1211
  # from the raw columns, not from the flat table built below
@@ -962,6 +1224,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
962
1224
  facet_order=facet_order)
963
1225
  fill_vec = hier_color_resolve(x_order, fill, x=x_ser,
964
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))
965
1231
  if main is None:
966
1232
  main = build_title(x, by_name=by_label, y_name=y,
967
1233
  stat=stat, facet_name=facet_name)
@@ -974,6 +1240,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
974
1240
  # keep the inner rings meaningful.
975
1241
  return hier_plotly(
976
1242
  agg, fill_vec, type=hier_type,
1243
+ fill_vec_byfac=fill_vec_byfac,
977
1244
  x_name=x, by_name=by, facet_name=facet_name,
978
1245
  main=main, border=color, digits_d=digits_d,
979
1246
  labels="%" if labels is None else labels,
@@ -1021,6 +1288,11 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1021
1288
  columns=x_order)
1022
1289
  if y_ser is None:
1023
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
1024
1296
  if stack100:
1025
1297
  # each bar scaled to its own total compares
1026
1298
  # composition rather than magnitude
@@ -1048,7 +1320,9 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1048
1320
  elif y_ser is not None:
1049
1321
  val_name = y
1050
1322
  elif by_ser is not None:
1051
- val_name = "Count"
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
1052
1326
  else:
1053
1327
  val_name = None # Count/Proportion of x
1054
1328
  return bc_facet_plotly(
@@ -1070,9 +1344,6 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1070
1344
  )
1071
1345
 
1072
1346
  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
1347
  facets = {
1077
1348
  str(lv): _facet_table(
1078
1349
  x_ser[facet_ser == lv],
@@ -1174,6 +1445,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1174
1445
  g = df.groupby(["f", "g", "x"], observed=True)["y"]
1175
1446
  t = (g.sum() if y_ser is None
1176
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")
1177
1452
  cells = {}
1178
1453
  for (f, gg, xx), v in t.items():
1179
1454
  nm = None if by_ser is None else gg
@@ -1194,8 +1469,12 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1194
1469
  digits_d = 0 if y_ser is None else 2
1195
1470
  from .plotly_utils import axis_format
1196
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")
1197
1474
  else "Count" if y_ser is None
1198
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
1199
1478
  return profile_facet_plotly(
1200
1479
  cells, x_lv, series, y_tick,
1201
1480
  axis_format(y_tick, digits_d, axis_fmt, axis_y_pre),
@@ -1220,9 +1499,6 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1220
1499
  agg_y = not is_agg and stat is not None
1221
1500
  cats_l, vals_l, fac_l = [], [], []
1222
1501
  if by_ser is not None:
1223
- if prop:
1224
- raise ValueError(
1225
- 'stat_x="proportion" with by= is not yet ported')
1226
1502
  rows = []
1227
1503
  for lv in facet_order:
1228
1504
  m = facet_ser == lv
@@ -1236,6 +1512,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1236
1512
  wide = (t.unstack("by")
1237
1513
  .reindex(index=[str(c) for c in x_order],
1238
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)
1239
1519
  rows.append(wide)
1240
1520
  cats_l += list(wide.index)
1241
1521
  fac_l += [str(lv)] * len(wide)
@@ -1266,8 +1546,8 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1266
1546
  # an aggregated value is labeled by its statistic, as the
1267
1547
  # unfaceted chart is: "Mean of Salary", not "Salary"
1268
1548
  if y_ser is None:
1269
- val_lab = ("Count" if by_ser is not None
1270
- else f"Proportion of {x}" if prop
1549
+ val_lab = (f"Proportion of {x}" if prop
1550
+ else "Count" if by_ser is not None
1271
1551
  else f"Count of {x}")
1272
1552
  else:
1273
1553
  val_lab = f"{STAT_LBL[stat]} of {y}" if agg_y else y
@@ -1317,9 +1597,6 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1317
1597
 
1318
1598
  # ----- build the table the renderer receives ------------------
1319
1599
  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
1600
 
1324
1601
  if by_ser is None:
1325
1602
  tbl = (x_ser.groupby(x_ser).size()
@@ -1339,11 +1616,20 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1339
1616
  tbl = pd.crosstab(by_ser, x_ser)
1340
1617
  tbl = tbl.reindex(index=by_order, columns=x_order,
1341
1618
  fill_value=0)
1342
- y_name = "Count"
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"
1343
1629
  if ylab is None:
1344
- ylab = f"Count of {x}"
1630
+ ylab = f"{y_name} of {x}"
1345
1631
  if digits_d is None:
1346
- digits_d = 0
1632
+ digits_d = 0 if stat_x == "count" else 2
1347
1633
 
1348
1634
  elif stat == "deviation": # R analog: .agg()
1349
1635
  if by_ser is not None:
@@ -1468,6 +1754,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1468
1754
  labels_decimals=labels_decimals,
1469
1755
  labels_size=0.90 if labels_size is None
1470
1756
  else labels_size,
1757
+ shares=(y_ser is None and stat_x == "proportion"),
1471
1758
  )
1472
1759
 
1473
1760
  if form == "radar":
@@ -1564,11 +1851,13 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1564
1851
  ydf.columns = [str(c) for c in ydf.columns]
1565
1852
  cats = [str(c) for c in ydf.index]
1566
1853
  val_lab = (xlab if xlab else ylab_user if ylab_user
1567
- else "Count" if y_ser is None else ylab)
1854
+ else ylab if (y_ser is not None
1855
+ or stat_x == "proportion")
1856
+ else "Count")
1568
1857
  return _dot_paired(
1569
1858
  cats, ydf.reset_index(drop=True), sort, sort_miss,
1570
1859
  origin_x,
1571
- show_diff=not quiet_use and y_ser is None,
1860
+ show_diff=y_ser is None, quiet=quiet,
1572
1861
  x_name=x,
1573
1862
  fill=fill, border=color, pt_size=pt_size,
1574
1863
  x_lab=val_lab, y_lab=x, digits_d=digits_d,
@@ -1650,8 +1939,16 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1650
1939
  if (fill_split is not None or fill_scaled) \
1651
1940
  and isinstance(tbl, pd.Series):
1652
1941
  from .getColors import bar_fill_colors
1653
- fill = bar_fill_colors(tbl.to_numpy(dtype=float), fill,
1654
- fill_split, fill_scaled, fill_chroma)
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)
1655
1952
 
1656
1953
  fig = bc_plotly(
1657
1954
  tbl,
@@ -1688,4 +1985,5 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1688
1985
 
1689
1986
 
1690
1987
  # font_size= scales all text of the returned figure
1691
- Chart = font_scaled(Chart)
1988
+ from .style import with_theme as _with_theme # noqa: E402
1989
+ Chart = font_scaled(attach_stats(_with_theme(Chart, own_param=True)))