lessPython 0.2.0__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. lessPy/ANOVA.py +74 -15
  2. lessPy/Chart.py +431 -88
  3. lessPy/Correlation.py +40 -11
  4. lessPy/Flows.py +3 -3
  5. lessPy/Logit.py +15 -11
  6. lessPy/Regression.py +27 -21
  7. lessPy/X.py +280 -42
  8. lessPy/XY.py +1332 -239
  9. lessPy/__init__.py +12 -2
  10. lessPy/_themes.py +5007 -0
  11. lessPy/bc_items_plotly.py +33 -20
  12. lessPy/bc_plotly.py +27 -10
  13. lessPy/bubble_plotly.py +33 -17
  14. lessPy/corCFA.py +9 -5
  15. lessPy/corProp.py +8 -4
  16. lessPy/corReflect.py +8 -4
  17. lessPy/corReorder.py +8 -4
  18. lessPy/corScree.py +12 -8
  19. lessPy/data/StockPrice.csv +1401 -1386
  20. lessPy/details.py +17 -4
  21. lessPy/dn_plotly.py +74 -54
  22. lessPy/dot_plotly.py +9 -8
  23. lessPy/freq_poly_plotly.py +18 -11
  24. lessPy/getColors.py +247 -20
  25. lessPy/hier_plotly.py +41 -14
  26. lessPy/hs_plotly.py +23 -11
  27. lessPy/pie_plotly.py +1 -1
  28. lessPy/pivot.py +39 -9
  29. lessPy/plotly_utils.py +162 -29
  30. lessPy/plt_add.py +2 -7
  31. lessPy/plt_contour.py +6 -6
  32. lessPy/plt_contour_facet.py +3 -3
  33. lessPy/plt_mat_plotly.py +85 -12
  34. lessPy/plt_plotly.py +34 -18
  35. lessPy/plt_smooth.py +4 -4
  36. lessPy/plt_time.py +10 -3
  37. lessPy/prob_norm.py +7 -3
  38. lessPy/prob_tcut.py +7 -3
  39. lessPy/prob_znorm.py +7 -1
  40. lessPy/profile_plotly.py +5 -4
  41. lessPy/radar_plotly.py +4 -3
  42. lessPy/rcolors.py +461 -0
  43. lessPy/simCImean.py +7 -3
  44. lessPy/simCLT.py +7 -3
  45. lessPy/simFlips.py +7 -3
  46. lessPy/simMeans.py +7 -3
  47. lessPy/stats_out.py +257 -11
  48. lessPy/style.py +167 -0
  49. lessPy/sunflower_plotly.py +141 -0
  50. lessPy/ttest.py +17 -13
  51. lessPy/utils.py +146 -6
  52. lessPy/vbs_plotly.py +39 -27
  53. lessPy/x_console.py +763 -0
  54. {lesspython-0.2.0.dist-info → lesspython-0.3.1.dist-info}/METADATA +13 -2
  55. lesspython-0.3.1.dist-info/RECORD +89 -0
  56. {lesspython-0.2.0.dist-info → lesspython-0.3.1.dist-info}/WHEEL +1 -1
  57. lesspython-0.2.0.dist-info/RECORD +0 -84
  58. {lesspython-0.2.0.dist-info → lesspython-0.3.1.dist-info}/licenses/LICENSE +0 -0
  59. {lesspython-0.2.0.dist-info → lesspython-0.3.1.dist-info}/top_level.txt +0 -0
lessPy/Chart.py CHANGED
@@ -35,14 +35,18 @@ from .plt_add import plt_add
35
35
  from .bubble_plotly import bubble_plotly
36
36
  from .dot_plotly import dot_plotly
37
37
  from .hier_plotly import (
38
- hier_aggregate, hier_color_resolve, 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,86 @@ def _chart_items(data, items, y, by, facet, form, stat, sort,
296
391
  rotate_x=rotate_x, rotate_y=rotate_y, main=main,
297
392
  by_tbls=tbls, by_name=leg, beside=False,
298
393
  val_name=("Proportion" if stack100 else "Count"),
299
- legend_title=leg)
394
+ legend_title=leg, horiz=horiz)
395
+
396
+
397
+ def _chart_per_variable(data, items, cols, fill, xlab, main, quiet):
398
+ """One bar chart of counts per categorical variable, as panels of
399
+ one figure, each followed at the console by its frequencies and
400
+ chi-square test. Numeric variables are passed over, as are a
401
+ variable that identifies each case and one with a single value.
402
+ R analog: bc.data.frame()"""
403
+ from plotly.subplots import make_subplots
404
+ import plotly.graph_objects as go
405
+ from .plotly_utils import (
406
+ as_plotly_color, BASE_COLORS, auto_text_color,
407
+ axis_format, plotly_style, to_hex)
408
+ from .stats_out import _counts_1d
409
+ show = not resolve_quiet(quiet)
410
+ keep = []
411
+ for nm, c in zip(items, cols):
412
+ if is_numeric_dtype(c) and not isinstance(
413
+ c.dtype, pd.CategoricalDtype):
414
+ continue # R: .is.num.cat(x, 0) FALSE
415
+ v = c.dropna()
416
+ if v.nunique() >= len(v): # identifies each case
417
+ continue
418
+ if v.nunique() == 1:
419
+ if show:
420
+ print(f"\nVariable {nm} has only one value.\n"
421
+ " No bar chart produced.\n")
422
+ continue
423
+ keep.append((nm, c))
424
+ if not keep:
425
+ raise ValueError(
426
+ "No categorical variables, so no bar charts.\n\n"
427
+ "If you have integer variables that are categorical, "
428
+ "convert them to pandas Categorical, e.g.\n"
429
+ " d['Plan'] = d['Plan'].astype('category')")
430
+
431
+ n = len(keep)
432
+ n_col = min(3, n)
433
+ n_row = math.ceil(n / n_col)
434
+ fig = make_subplots(rows=n_row, cols=n_col,
435
+ subplot_titles=[nm for nm, _ in keep],
436
+ horizontal_spacing=0.08,
437
+ vertical_spacing=max(0.08, 0.3 / n_row))
438
+ st = plotly_style()
439
+ rep_lines, per_var = [], {}
440
+ for k, (nm, c) in enumerate(keep):
441
+ rep_lines += _counts_1d(c.dropna(), nm,
442
+ int(c.isna().sum())) + [""]
443
+ per_var[nm] = chart_stats_data(c.dropna(), None, None, None,
444
+ n_miss=int(c.isna().sum()))
445
+ order = _category_order(c.dropna())
446
+ cnt = c.value_counts().reindex(order, fill_value=0)
447
+ f = (fill if isinstance(fill, (list, tuple))
448
+ else [fill] if fill is not None else BASE_COLORS)
449
+ cols_k = [to_hex(f[i % len(f)]) for i in range(len(cnt))]
450
+ r, cc = k // n_col + 1, k % n_col + 1
451
+ fig.add_trace(go.Bar(
452
+ x=[str(v) for v in cnt.index], y=cnt.to_numpy(),
453
+ marker=dict(color=cols_k), text=[str(v) for v in cnt],
454
+ textposition="inside", showlegend=False,
455
+ textfont=dict(color=auto_text_color(cols_k)),
456
+ hovertemplate=f"{nm}: %{{x}}<br>Count: %{{y}}"
457
+ "<extra></extra>"), row=r, col=cc)
458
+ ticks = pretty(0.0, float(cnt.max()))
459
+ fig.update_yaxes(row=r, col=cc, tickvals=ticks,
460
+ ticktext=axis_format(ticks, 0),
461
+ range=[0, ticks[-1] * 1.04],
462
+ gridcolor="#E5E5E5",
463
+ title_text="Count" if cc == 1 else "")
464
+ fig.update_xaxes(row=r, col=cc, type="category",
465
+ title_text=xlab or "")
466
+ _set_stats(ChartStats(n_dim=1, variables=per_var), rep_lines,
467
+ quiet)
468
+ fig.update_layout(
469
+ template=None, height=140 + 260 * n_row,
470
+ plot_bgcolor=as_plotly_color(st["panel_fill"]),
471
+ paper_bgcolor=as_plotly_color(st["window_fill"]),
472
+ title=(dict(text=main, x=0.5) if main else None))
473
+ return fig
300
474
 
301
475
 
302
476
  def _items_print(frq, wm, numeric, title=None):
@@ -305,6 +479,15 @@ def _items_print(frq, wm, numeric, title=None):
305
479
  numeric)
306
480
 
307
481
 
482
+ def _set_stats(st, lines, quiet):
483
+ """Keep the statistics of this call, with the report as their
484
+ text, and display the report unless quiet."""
485
+ st["text"] = "\n".join(lines)
486
+ CALL_STATS.set(st)
487
+ if not resolve_quiet(quiet):
488
+ print(st["text"])
489
+
490
+
308
491
  def _profile_fill(theme):
309
492
  """One hue per series from the theme, as R's profile does:
310
493
  .color_range(.get_fill(theme), n), the qualitative hues for the
@@ -352,7 +535,7 @@ def _dot_diff_lines(cats, ydf, x_name):
352
535
 
353
536
 
354
537
  def _dot_paired(cats, ydf, sort, sort_miss, origin_x, show_diff,
355
- x_name, **render):
538
+ x_name, quiet=None, **render):
356
539
  """Order a multi-series dot plot and draw it. Two series are a
357
540
  pair, read for the gap between them, so by default they are
358
541
  ordered by that difference, ascending so the largest positive
@@ -370,8 +553,23 @@ def _dot_paired(cats, ydf, sort, sort_miss, origin_x, show_diff,
370
553
  kind="stable")
371
554
  cats = [cats[i] for i in order]
372
555
  ydf = ydf.iloc[order]
373
- if dif_pair and show_diff:
374
- print("\n".join(_dot_diff_lines(cats, ydf, x_name)))
556
+ if dif_pair:
557
+ # the difference the display presents, kept with the figure; it
558
+ # is reported where R lists it (show_diff), unless quiet
559
+ dif = pd.Series(
560
+ (ydf.iloc[:, 1] - ydf.iloc[:, 0]).to_numpy(dtype=float),
561
+ index=cats, name=f"{ydf.columns[1]} - {ydf.columns[0]}")
562
+ st = CALL_STATS.get()
563
+ if st is None:
564
+ st = ChartStats(n_dim=2, values=ydf.set_axis(cats), text="")
565
+ CALL_STATS.set(st)
566
+ st["diff"] = dif
567
+ if show_diff:
568
+ lines = _dot_diff_lines(cats, ydf, x_name)
569
+ st["text"] = "\n".join(
570
+ [t for t in (st.get("text", ""), "\n".join(lines)) if t])
571
+ if not resolve_quiet(quiet):
572
+ print("\n".join(lines))
375
573
  origin, gridT = _dot_origin_grid(
376
574
  ydf.to_numpy(dtype=float).ravel(),
377
575
  origin_in=0 if (dif_pair and origin_x is None) else origin_x)
@@ -396,7 +594,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
396
594
  theme=None,
397
595
  labels=None, labels_position=None,
398
596
  labels_color=None, labels_size=None, labels_decimals=None,
399
- labels_cut=None, segments=None,
597
+ labels_cut=None, segments=None, one_plot=None,
400
598
  legend_title=None, legend_position=None,
401
599
  legend_labels=None, legend_horiz=False,
402
600
  legend_size=None, legend_abbrev=None, legend_adjust=0,
@@ -453,6 +651,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
453
651
 
454
652
  Variables are strings naming columns of the DataFrame `data`.
455
653
  Returns a plotly Figure; call .show() to display from a script.
654
+ The statistics behind the chart come with it as fig.stats, read
655
+ as attributes or by name: fig.stats.freq, fig.stats.p_value,
656
+ fig.stats.text (the console report, kept when quiet=True), named
657
+ as R's Chart() names its returned list.
456
658
  """
457
659
 
458
660
  # ----- validate parameters ------------------------------------
@@ -566,6 +768,13 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
566
768
  raise ValueError('labels_position must be "in" or "out"')
567
769
  if axis_fmt not in ("K", ",", ".", ""):
568
770
  raise ValueError('axis_fmt must be "K", ",", "." or ""')
771
+ # proportions of a bar chart with by are taken within each bar,
772
+ # the 100% stacked chart. R analog: .bc.main()
773
+ # if (!is.null(by) && prop) stack100 <- TRUE
774
+ if (stat_x == "proportion" and by is not None and form == "bar"
775
+ and facet is None and y is None):
776
+ stack100 = True
777
+ stat_x = "count"
569
778
  if stack100:
570
779
  if form != "bar":
571
780
  raise ValueError('stack100 rescales the bars of a bar '
@@ -662,9 +871,6 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
662
871
  if sort != "0":
663
872
  raise ValueError("Sort not applicable to Trellis "
664
873
  "(faceted bar) charts")
665
- if stat_x == "proportion" and by is not None:
666
- raise NotImplementedError(
667
- 'stat_x="proportion" with by= is not yet ported')
668
874
 
669
875
  # ----- resolve variables and filter ---------------------------
670
876
  if filter is not None:
@@ -692,7 +898,8 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
692
898
  stat=stat, sort=sort, sort_miss=sort_miss,
693
899
  stack100=stack100, horiz=horiz, fill=fill, theme=theme,
694
900
  labels=labels, labels_size=labels_size, gap=gap,
695
- labels_cut=labels_cut,
901
+ labels_cut=labels_cut, power=power, radius=radius,
902
+ one_plot=one_plot,
696
903
  xlab=xlab, ylab=ylab, main=main,
697
904
  legend_title=legend_title, n_col=n_col, n_row=n_row,
698
905
  axis_fmt=axis_fmt, axis_x_pre=axis_x_pre,
@@ -737,7 +944,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
737
944
  x_lab_dot = (xlab if xlab else ylab if ylab else val_lab)
738
945
  return _dot_paired(
739
946
  cats, ydf.reset_index(drop=True), sort, sort_miss,
740
- origin_x, show_diff=not resolve_quiet(quiet), x_name=x,
947
+ origin_x, show_diff=True, quiet=quiet, x_name=x,
741
948
  fill=fill, border=color, pt_size=pt_size,
742
949
  x_lab=x_lab_dot, y_lab=x,
743
950
  digits_d=2 if digits_d is None else digits_d,
@@ -814,12 +1021,53 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
814
1021
  facet_order = (_category_order(facet_ser)
815
1022
  if facet_ser is not None else None)
816
1023
 
817
- # theme=: 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)
1024
+ # a theme set by style(), or by this call's theme=, which the
1025
+ # wrapper has applied for the call
1026
+ if theme is None:
1027
+ theme = get_option("theme")
1028
+
1029
+ # an ordered category is ordinal, so its default fill is the
1030
+ # theme's sequential palette, light to dark in the order of the
1031
+ # levels, not distinct hues. Fill encodes x without by and by
1032
+ # with it, so the ordering of that variable is the one that
1033
+ # applies. R analog: Chart.R is.ordered() fill
1034
+ if (form == "bar" and fill is None and facet_ser is None
1035
+ and fill_split is None and not fill_scaled):
1036
+ enc = by_ser if by_ser is not None else x_ser
1037
+ if (isinstance(enc.dtype, pd.CategoricalDtype)
1038
+ and enc.cat.ordered):
1039
+ fill = _color_range(
1040
+ _THEME_PALETTE.get(theme, "blues"),
1041
+ len(by_order) if by_order is not None
1042
+ else len(x_order))
1043
+
1044
+ # theme=: a bar chart without by is drawn in the theme's single
1045
+ # bar color, which fill_scaled then varies in luminance; with
1046
+ # by, the levels span the theme's sequential palette, darkest
1047
+ # first. The default theme keeps the qualitative hues.
1048
+ # R analog: Chart.R fill block (theme.seq) and .bc.main()
1049
+ if (theme is not None and fill is None
1050
+ and theme not in ("colors", "hues")):
1051
+ if theme not in _THEME_PALETTE:
1052
+ raise ValueError(
1053
+ f"unknown theme '{theme}'; one of: "
1054
+ f"{', '.join(sorted(_THEME_PALETTE))}")
1055
+ if by_order is not None:
1056
+ fill = list(reversed(_color_range(_THEME_PALETTE[theme],
1057
+ len(by_order))))
1058
+ elif form == "bar":
1059
+ fill = [_THEME_BAR_FILL.get(theme, "#595959E6")] \
1060
+ * len(x_order)
1061
+ else:
1062
+ fill = _theme_fill(theme, len(x_order))
1063
+
1064
+ # a palette name in fill, such as "reds", names a range of colors,
1065
+ # one per bar, or per level of by. R analog: .bc.main()
1066
+ # .color_range(fill, n.levels)
1067
+ if (form == "bar" and isinstance(fill, str) and not fill_scaled
1068
+ and fill in _palette_names()):
1069
+ fill = _color_range(fill, len(by_order) if by_order is not None
1070
+ else len(x_order))
823
1071
 
824
1072
  # explicit panel-grid layout: n_col wins, else derive from
825
1073
  # n_row; None keeps each renderer's own default (single-column
@@ -915,35 +1163,50 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
915
1163
  f"have a numerical variable y='{y}', so specify "
916
1164
  'stat to define the aggregation, e.g., stat="mean"')
917
1165
 
918
- # accompanying statistics: frequencies, or the stat of y
919
- 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
1166
+ # accompanying statistics: frequencies, or the stat of y. They are
1167
+ # computed whether or not displayed, and returned with the figure
1168
+ # as fig.stats, as R's Chart() returns its list
1169
+ y_out = (y_ser if y_ser is not None
1170
+ and stat in STAT_FUN else None)
1171
+ if by_more_sers and (y_ser is None or y_out is not None):
1172
+ from .stats_out import nested_stats
1173
+ lines = nested_stats(
1174
+ [x_ser, by_ser] + by_more_sers, [x, by] + by_more,
1175
+ y_out, stat, y, 2 if digits_d is None else digits_d)
1176
+ cols_n = [x_ser, by_ser] + by_more_sers
1177
+ df_n = pd.DataFrame({nm: c for nm, c in
1178
+ zip([x, by] + by_more, cols_n)})
1179
+ _set_stats(ChartStats(
1180
+ n_dim=len(cols_n),
1181
+ freq=df_n.groupby([x, by] + by_more, observed=True).size()
1182
+ if y_out is None else None),
1183
+ lines, quiet)
1184
+ elif y_ser is None or y_out is not None:
1185
+ var_lbl = data.attrs.get("variable_labels", {}) or {}
1186
+ lines = chart_stats(
1187
+ x_ser, by_ser, y_out, stat, x, by, y,
1188
+ 2 if digits_d is None else digits_d,
1189
+ stack100=stack100, n_miss=n_miss_x,
1190
+ x_lbl=var_lbl.get(x), y_lbl=var_lbl.get(y),
1191
+ facet_ser=facet_ser, facet_name=facet_name,
1192
+ form=form)
1193
+ _set_stats(chart_stats_data(x_ser, by_ser, y_out, stat,
1194
+ facet_ser=facet_ser,
1195
+ n_miss=n_miss_x),
1196
+ lines, quiet)
1197
+
1198
+ # labels default for aggregated data, the value of the statistic;
1199
+ # bars of counts are labeled with their percentages, as lessR's
1200
+ # do (getOption("values"), "%"): of the total, or of each bar for
1201
+ # the 100% stacked chart. R analog: Chart.R ~716, .bc.main()
941
1202
  if labels is None and (stat is not None or
942
1203
  (is_agg and y_ser is not None)):
943
1204
  # the bars carry a statistic, so the label is its value;
944
1205
  # beside only arranges the bars, and a percentage of a sum
945
1206
  # of means is not a quantity
946
1207
  labels = "input"
1208
+ elif labels is None and form == "bar" and y_ser is None:
1209
+ labels = "%"
947
1210
 
948
1211
  # ----- hierarchical forms aggregate per nesting level ---------
949
1212
  # from the raw columns, not from the flat table built below
@@ -962,6 +1225,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
962
1225
  facet_order=facet_order)
963
1226
  fill_vec = hier_color_resolve(x_order, fill, x=x_ser,
964
1227
  y=y_ser, stat=stat)
1228
+ fill_vec_byfac = (None if facet_ser is None else
1229
+ hier_color_resolve_byfac(
1230
+ x_order, fill, x_ser, y_ser, stat,
1231
+ facet_ser, facet_order))
965
1232
  if main is None:
966
1233
  main = build_title(x, by_name=by_label, y_name=y,
967
1234
  stat=stat, facet_name=facet_name)
@@ -974,6 +1241,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
974
1241
  # keep the inner rings meaningful.
975
1242
  return hier_plotly(
976
1243
  agg, fill_vec, type=hier_type,
1244
+ fill_vec_byfac=fill_vec_byfac,
977
1245
  x_name=x, by_name=by, facet_name=facet_name,
978
1246
  main=main, border=color, digits_d=digits_d,
979
1247
  labels="%" if labels is None else labels,
@@ -1021,6 +1289,11 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1021
1289
  columns=x_order)
1022
1290
  if y_ser is None:
1023
1291
  t = t.fillna(0)
1292
+ if stat_x == "proportion" and not stack100:
1293
+ # every cell of the panel as a share of the panel,
1294
+ # as lattice's prop.table(margin=panel)
1295
+ tot = t.to_numpy().sum()
1296
+ t = t / tot if tot > 0 else t
1024
1297
  if stack100:
1025
1298
  # each bar scaled to its own total compares
1026
1299
  # composition rather than magnitude
@@ -1048,7 +1321,9 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1048
1321
  elif y_ser is not None:
1049
1322
  val_name = y
1050
1323
  elif by_ser is not None:
1051
- val_name = "Count"
1324
+ # named for x, as lattice's "Count of" / "Proportion of"
1325
+ val_name = ("Proportion of " if stat_x == "proportion"
1326
+ else "Count of ") + x
1052
1327
  else:
1053
1328
  val_name = None # Count/Proportion of x
1054
1329
  return bc_facet_plotly(
@@ -1070,9 +1345,6 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1070
1345
  )
1071
1346
 
1072
1347
  if facet_ser is not None and form == "radar":
1073
- if stat_x == "proportion" and by_ser is not None:
1074
- raise NotImplementedError(
1075
- 'stat_x="proportion" with by= is not yet ported')
1076
1348
  facets = {
1077
1349
  str(lv): _facet_table(
1078
1350
  x_ser[facet_ser == lv],
@@ -1174,6 +1446,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1174
1446
  g = df.groupby(["f", "g", "x"], observed=True)["y"]
1175
1447
  t = (g.sum() if y_ser is None
1176
1448
  else STAT_FUN[stat](g) if agg_y else g.mean())
1449
+ if y_ser is None and stat_x == "proportion":
1450
+ # each series is its group's distribution over x in that
1451
+ # panel; without by, the panel's own distribution
1452
+ t = t / t.groupby(level=["f", "g"]).transform("sum")
1177
1453
  cells = {}
1178
1454
  for (f, gg, xx), v in t.items():
1179
1455
  nm = None if by_ser is None else gg
@@ -1194,8 +1470,12 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1194
1470
  digits_d = 0 if y_ser is None else 2
1195
1471
  from .plotly_utils import axis_format
1196
1472
  y_lab = (ylab if ylab is not None
1473
+ else f"Proportion of {x}" if (y_ser is None
1474
+ and stat_x == "proportion")
1197
1475
  else "Count" if y_ser is None
1198
1476
  else f"{STAT_LBL[stat]} of {y}" if agg_y else y)
1477
+ if y_ser is None and stat_x == "proportion" and digits_d == 0:
1478
+ digits_d = 2
1199
1479
  return profile_facet_plotly(
1200
1480
  cells, x_lv, series, y_tick,
1201
1481
  axis_format(y_tick, digits_d, axis_fmt, axis_y_pre),
@@ -1220,9 +1500,6 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1220
1500
  agg_y = not is_agg and stat is not None
1221
1501
  cats_l, vals_l, fac_l = [], [], []
1222
1502
  if by_ser is not None:
1223
- if prop:
1224
- raise ValueError(
1225
- 'stat_x="proportion" with by= is not yet ported')
1226
1503
  rows = []
1227
1504
  for lv in facet_order:
1228
1505
  m = facet_ser == lv
@@ -1236,6 +1513,10 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1236
1513
  wide = (t.unstack("by")
1237
1514
  .reindex(index=[str(c) for c in x_order],
1238
1515
  columns=[str(b) for b in by_order]))
1516
+ if prop and y_ser is None:
1517
+ # each group's distribution over x in this panel
1518
+ tot = wide.sum(axis=0)
1519
+ wide = wide.div(tot.where(tot != 0), axis=1)
1239
1520
  rows.append(wide)
1240
1521
  cats_l += list(wide.index)
1241
1522
  fac_l += [str(lv)] * len(wide)
@@ -1266,8 +1547,8 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1266
1547
  # an aggregated value is labeled by its statistic, as the
1267
1548
  # unfaceted chart is: "Mean of Salary", not "Salary"
1268
1549
  if y_ser is None:
1269
- val_lab = ("Count" if by_ser is not None
1270
- else f"Proportion of {x}" if prop
1550
+ val_lab = (f"Proportion of {x}" if prop
1551
+ else "Count" if by_ser is not None
1271
1552
  else f"Count of {x}")
1272
1553
  else:
1273
1554
  val_lab = f"{STAT_LBL[stat]} of {y}" if agg_y else y
@@ -1317,9 +1598,6 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1317
1598
 
1318
1599
  # ----- build the table the renderer receives ------------------
1319
1600
  if y_ser is None: # counts of x
1320
- if stat_x == "proportion" and by_ser is not None:
1321
- raise NotImplementedError(
1322
- 'stat_x="proportion" with by= is not yet ported')
1323
1601
 
1324
1602
  if by_ser is None:
1325
1603
  tbl = (x_ser.groupby(x_ser).size()
@@ -1339,11 +1617,20 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1339
1617
  tbl = pd.crosstab(by_ser, x_ser)
1340
1618
  tbl = tbl.reindex(index=by_order, columns=x_order,
1341
1619
  fill_value=0)
1342
- y_name = "Count"
1620
+ if stat_x == "proportion":
1621
+ # each group's distribution over x, so the groups are
1622
+ # compared whatever their sizes: every by row sums
1623
+ # to 1 (the bar chart took its own path above, the
1624
+ # 100% stacked chart within each bar)
1625
+ tot = tbl.sum(axis=1)
1626
+ tbl = tbl.div(tot.where(tot != 0), axis=0).fillna(0)
1627
+ y_name = "Proportion"
1628
+ else:
1629
+ y_name = "Count"
1343
1630
  if ylab is None:
1344
- ylab = f"Count of {x}"
1631
+ ylab = f"{y_name} of {x}"
1345
1632
  if digits_d is None:
1346
- digits_d = 0
1633
+ digits_d = 0 if stat_x == "count" else 2
1347
1634
 
1348
1635
  elif stat == "deviation": # R analog: .agg()
1349
1636
  if by_ser is not None:
@@ -1468,6 +1755,7 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1468
1755
  labels_decimals=labels_decimals,
1469
1756
  labels_size=0.90 if labels_size is None
1470
1757
  else labels_size,
1758
+ shares=(y_ser is None and stat_x == "proportion"),
1471
1759
  )
1472
1760
 
1473
1761
  if form == "radar":
@@ -1564,11 +1852,13 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1564
1852
  ydf.columns = [str(c) for c in ydf.columns]
1565
1853
  cats = [str(c) for c in ydf.index]
1566
1854
  val_lab = (xlab if xlab else ylab_user if ylab_user
1567
- else "Count" if y_ser is None else ylab)
1855
+ else ylab if (y_ser is not None
1856
+ or stat_x == "proportion")
1857
+ else "Count")
1568
1858
  return _dot_paired(
1569
1859
  cats, ydf.reset_index(drop=True), sort, sort_miss,
1570
1860
  origin_x,
1571
- show_diff=not quiet_use and y_ser is None,
1861
+ show_diff=y_ser is None, quiet=quiet,
1572
1862
  x_name=x,
1573
1863
  fill=fill, border=color, pt_size=pt_size,
1574
1864
  x_lab=val_lab, y_lab=x, digits_d=digits_d,
@@ -1627,6 +1917,26 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1627
1917
  facet_name=facet_ttl, y_name=y_name, stat=stat)
1628
1918
  elif main == "":
1629
1919
  main = None
1920
+ # a palette name in fill names the palette, not a color, as
1921
+ # for the bar chart. A sequential palette shades each slice
1922
+ # by its value off the palette's ramp, larger darker, as
1923
+ # for the treemap. R analog: Chart.R pie palette branch
1924
+ if isinstance(fill, str) and fill in _palette_names():
1925
+ from .hier_plotly import _HIER_SEQ
1926
+ slices = list(tbl.columns if isinstance(tbl, pd.DataFrame)
1927
+ else tbl.index)
1928
+ vals = (tbl.sum(axis=0) if isinstance(tbl, pd.DataFrame)
1929
+ else tbl).to_numpy(dtype=float)
1930
+ if (fill.lower() in _HIER_SEQ and len(slices) > 1
1931
+ and np.ptp(vals) > 0):
1932
+ from .getColors import getColors
1933
+ ramp = [c[:7] for c in getColors(fill.lower(), n=256,
1934
+ quiet=True)]
1935
+ pos = (vals - vals.min()) / np.ptp(vals)
1936
+ fill = [ramp[i] for i in
1937
+ np.round(pos * (len(ramp) - 1)).astype(int)]
1938
+ else:
1939
+ fill = _color_range(fill, len(slices))
1630
1940
  return pie_plotly(
1631
1941
  tbl,
1632
1942
  x_name=x, y_name=y_name, by_name=by, main=main,
@@ -1650,8 +1960,16 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1650
1960
  if (fill_split is not None or fill_scaled) \
1651
1961
  and isinstance(tbl, pd.Series):
1652
1962
  from .getColors import bar_fill_colors
1653
- fill = bar_fill_colors(tbl.to_numpy(dtype=float), fill,
1654
- fill_split, fill_scaled, fill_chroma)
1963
+ # one fill repeated over the bars is a single color; more
1964
+ # than two leave the hue to the theme, as R's .bc.main()
1965
+ f_in = fill
1966
+ if isinstance(fill, (list, tuple)):
1967
+ uniq = list(dict.fromkeys(fill))
1968
+ f_in = (uniq[0] if len(uniq) == 1
1969
+ else None if len(uniq) > 2 else uniq)
1970
+ fill = bar_fill_colors(tbl.to_numpy(dtype=float), f_in,
1971
+ fill_split, fill_scaled, fill_chroma,
1972
+ theme=theme)
1655
1973
 
1656
1974
  fig = bc_plotly(
1657
1975
  tbl,
@@ -1687,5 +2005,30 @@ def Chart(x, y=None, data=None, filter=None, by=None, facet=None,
1687
2005
  return fig
1688
2006
 
1689
2007
 
2008
+ def _with_height(func):
2009
+ """height= in inches, as lessR's Chart(height=), converted at
2010
+ 96 pixels per inch, so pdf_file= also writes that height.
2011
+ None keeps the figure's own height."""
2012
+ import functools
2013
+
2014
+ @functools.wraps(func)
2015
+ def wrapper(*args, height=None, **kwargs):
2016
+ if height is not None and (isinstance(height, bool) or
2017
+ not isinstance(height, (int, float))
2018
+ or height <= 0):
2019
+ raise ValueError(
2020
+ f"height is in inches, a positive number such as "
2021
+ f"height=4, not {height!r}")
2022
+ fig = func(*args, **kwargs)
2023
+ if height is not None and hasattr(fig, "layout"):
2024
+ fig.update_layout(height=round(96 * height))
2025
+ return fig
2026
+
2027
+ return wrapper
2028
+
2029
+
1690
2030
  # font_size= scales all text of the returned figure
1691
- Chart = font_scaled(Chart)
2031
+ from .style import with_theme as _with_theme # noqa: E402
2032
+ from .utils import file_output # noqa: E402
2033
+ Chart = file_output(_with_height(font_scaled(attach_stats(
2034
+ _with_theme(Chart, own_param=True)))))