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/X.py CHANGED
@@ -49,13 +49,16 @@ from scipy import stats as sps
49
49
  from .dn_plotly import dn_plotly
50
50
  from .freq_poly_plotly import freq_poly_plotly
51
51
  from .hs_plotly import hs_plotly
52
- from .plotly_utils import BASE_COLORS, axis_format, font_scaled
52
+ from .plotly_utils import (
53
+ BASE_COLORS, axis_format, by_colors, font_scaled,
54
+ )
53
55
  from .plt_add import plt_add
54
- from .stats_out import resolve_quiet, x_stats
56
+ from . import x_console as xc
57
+ from .stats_out import CALL_STATS, record_stats, resolve_quiet, x_stats
55
58
  from .vbs_plotly import vbs_plotly
56
59
  from .utils import (
57
- bw_nrd0, category_order, get_column, get_option, pretty,
58
- resolve_facet,
60
+ band_width, category_order, get_column, get_option, pretty,
61
+ resolve_facet, set_option,
59
62
  )
60
63
 
61
64
  # the VBS family: single-element v/b/s, and the composites. "vb"
@@ -63,7 +66,20 @@ from .utils import (
63
66
  # (deprecated) vbs_plot= subset string; here they are just forms.
64
67
  _VBS_FORMS = ("violin", "box", "strip", "vb", "vs", "bs", "vbs")
65
68
  _FORMS = ("histogram", "freq_poly", "density") + _VBS_FORMS
66
- _STATS = ("count", "proportion", "density")
69
+ _STATS = ("count", "proportion")
70
+
71
+
72
+ def _norm_form(form):
73
+ """Any one to three of v b s, in any order and any case, names
74
+ those VBS layers: normalize to the canonical order v b s, a
75
+ lone letter to the full name of its layer (X.R)."""
76
+ if isinstance(form, str):
77
+ ltr = list(form.strip().lower())
78
+ if (1 <= len(ltr) <= 3 and set(ltr) <= set("vbs")
79
+ and len(set(ltr)) == len(ltr)):
80
+ f = "".join(c for c in "vbs" if c in ltr)
81
+ return {"v": "violin", "b": "box", "s": "strip"}.get(f, f)
82
+ return form
67
83
 
68
84
 
69
85
  def _breaks_from_args(fx, bin_start, bin_width, bin_end, breaks):
@@ -102,7 +118,7 @@ def _x_finish(fig, rotate_x, rotate_y, scale_x, axis_fmt,
102
118
  angles. R analogs: scale_x, rotate_x, rotate_y."""
103
119
  if scale_x is not None:
104
120
  tv = np.linspace(float(scale_x[0]), float(scale_x[1]),
105
- int(scale_x[2]))
121
+ int(scale_x[2]) + 1) # n intervals, as R
106
122
  fig.update_xaxes(
107
123
  tickvals=tv,
108
124
  ticktext=axis_format(tv, digits_d, axis_fmt,
@@ -117,7 +133,7 @@ def _x_finish(fig, rotate_x, rotate_y, scale_x, axis_fmt,
117
133
 
118
134
  def X(x, by=None, facet=None, data=None, filter=None,
119
135
  form="histogram",
120
- stat="count",
136
+ stat=None,
121
137
  fill=None, color=None,
122
138
  bin_start=None, bin_width=None, bin_end=None,
123
139
  breaks="Sturges",
@@ -142,23 +158,44 @@ def X(x, by=None, facet=None, data=None, filter=None,
142
158
  quiet=None):
143
159
  """Analytic view of the distribution of one numerical variable,
144
160
  optionally grouped (by=). Variables are strings naming columns
145
- of the DataFrame `data`. Returns a plotly Figure.
161
+ of the DataFrame `data`. Returns a plotly Figure, or a list of
162
+ Figures, one per variable, when x is a list of names.
146
163
  """
147
164
 
165
+ form = _norm_form(form)
148
166
  if form not in _FORMS:
149
- raise ValueError(f"form must be one of {_FORMS}")
150
- if stat not in _STATS:
151
- raise ValueError(f"stat must be one of {_STATS}")
167
+ raise ValueError(
168
+ "form must be one of: "
169
+ + ", ".join(f'"{f}"' for f in _FORMS) + "\n\n"
170
+ "Any subset of the violin, box, and strip layers is a "
171
+ "form, named\nby its letters in any order and any case, "
172
+ 'as in form="vb" or\nform="SV". A single layer may also '
173
+ 'be named in full, as in\nform="violin".')
174
+ if stat is not None:
175
+ if stat not in _STATS:
176
+ raise ValueError(f"stat must be one of {_STATS}")
177
+ # stat names a frequency of the binned data, which only the
178
+ # forms that plot a frequency have (X.R)
179
+ if form not in ("histogram", "freq_poly"):
180
+ why = ("A density is an area, already scaled to a "
181
+ "proportion,\nso a count and a proportion do not "
182
+ "distinguish two\ndisplays."
183
+ if form == "density" else
184
+ f'form="{form}" plots the values themselves and\n'
185
+ "the quantiles of their distribution, not a "
186
+ "frequency,\nso a count and a proportion do not "
187
+ "distinguish two\ndisplays.")
188
+ raise ValueError(
189
+ "stat sets the plotted value to a count of the cases\n"
190
+ "in each bin or to their proportion, so it applies to "
191
+ "the forms\nthat plot a frequency: form=\"histogram\" "
192
+ 'and\nform="freq_poly".\n\n' + why)
193
+ else:
194
+ stat = "count"
152
195
  if data is None:
153
196
  raise ValueError(
154
197
  "data= is required: a pandas DataFrame containing the "
155
198
  "named columns")
156
- if stat == "density":
157
- if form == "freq_poly":
158
- stat = "count" # density is not a freq poly stat (X.R)
159
- else:
160
- form = "density" # R analog: X.R stat="density"
161
-
162
199
  if cumulate not in ("off", "on", "both"):
163
200
  raise ValueError('cumulate: "off", "on", or "both"')
164
201
  if kind not in ("general", "normal", "both"):
@@ -171,16 +208,41 @@ def X(x, by=None, facet=None, data=None, filter=None,
171
208
  if cumulate != "off" and (by is not None
172
209
  or facet is not None):
173
210
  raise ValueError(
174
- "cumulate applies to a single histogram: "
175
- "no by= or facet=")
211
+ "cumulate applies to a single histogram or frequency "
212
+ "polygon: no by= or facet=")
213
+ if cumulate != "off" and form not in ("histogram", "freq_poly"):
214
+ raise ValueError(
215
+ "cumulate accumulates the bin counts of a histogram or "
216
+ f'frequency polygon, which form="{form}" does not plot')
176
217
  if counts and (by is not None or facet is not None):
177
218
  raise ValueError(
178
219
  "counts labels apply to a single histogram: "
179
220
  "no by= or facet=")
180
- if (kind != "general" or rug) and by is not None:
221
+ if rug and by is not None:
222
+ raise ValueError(
223
+ "rug draws the data values of a single series: no by=")
224
+ if kind != "general" and by is not None and facet is not None:
225
+ raise ValueError(
226
+ "kind with by= fits a normal curve to each group of a "
227
+ "single panel: no facet=")
228
+ # by overlays the groups within a panel, marked on the points
229
+ # of the strip layer; a violin or box drawn over another is
230
+ # not readable, so without a strip there is no layer to carry
231
+ # the groups, and facet separates them (X.R)
232
+ vbs_ltr = {"violin": "v", "box": "b",
233
+ "strip": "s"}.get(form, form)
234
+ if (form in _VBS_FORMS and by is not None
235
+ and "s" not in vbs_ltr):
181
236
  raise ValueError(
182
- "kind and rug draw per-panel curves of a single "
183
- "series: no by=")
237
+ "The groups of by are overlaid within a panel, marked "
238
+ "on the\npoints of the strip layer, which "
239
+ f'form="{form}" does\nnot include. A violin or a box '
240
+ "drawn over another is not\nreadable, so there is no "
241
+ "layer here to carry the groups.\n\n"
242
+ "To mark the groups, add the strip layer:\n"
243
+ f' X("{x}", by="{by}", form="{vbs_ltr}s")\n\n'
244
+ f"To draw a separate {form} for each group:\n"
245
+ f' X("{x}", facet="{by}", form="{form}")')
184
246
  if (out_cut > 0 or box_adj) and form not in _VBS_FORMS:
185
247
  raise ValueError(
186
248
  "out_cut and box_adj apply to the VBS forms: "
@@ -211,6 +273,18 @@ def X(x, by=None, facet=None, data=None, filter=None,
211
273
  "the plotly VBS draws facet levels as bands on one "
212
274
  "panel, so n_row and n_col do not apply")
213
275
 
276
+ if isinstance(by, (list, tuple)):
277
+ raise ValueError(
278
+ "Only one by variable is permitted, but more than one "
279
+ f"specified:\n by = {list(by)}\n\n"
280
+ "The groups of by are overlaid within the panel, "
281
+ "distinguished by\ncolor and optionally symbol. That one "
282
+ "channel is carried by the\nfirst variable, so a second "
283
+ "has no encoding left by which to\nseparate its levels.\n"
284
+ "To stratify by a second variable, use facet, which draws "
285
+ "each of\nits groups in a panel of its own.")
286
+
287
+ data_in = data # the caller's, for suggestions
214
288
  if filter is not None:
215
289
  data = data.query(filter)
216
290
 
@@ -224,6 +298,15 @@ def X(x, by=None, facet=None, data=None, filter=None,
224
298
  (facet_ser, facet_name,
225
299
  facet2_ser, facet2_name) = resolve_facet(data, facet, "X")
226
300
 
301
+ # before casewise deletion: the one-variable summary counts the
302
+ # missing values of x, the panel table those within each panel
303
+ x_raw = x_ser.to_numpy(dtype=float)
304
+ grp_ok = ~pd.concat([s for s in (by_ser, facet_ser, facet2_ser)
305
+ if s is not None] or [x_ser.notna()],
306
+ axis=1).isna().any(axis=1).to_numpy()
307
+ by_raw = by_ser.to_numpy() if by_ser is not None else None
308
+ f_raw = facet_ser.to_numpy() if facet_ser is not None else None
309
+
227
310
  used = [s for s in (x_ser, by_ser, facet_ser, facet2_ser)
228
311
  if s is not None]
229
312
  keep = ~pd.concat(used, axis=1).isna().any(axis=1)
@@ -240,24 +323,60 @@ def X(x, by=None, facet=None, data=None, filter=None,
240
323
  if len(fx) == 0:
241
324
  raise ValueError(f"'{x}' has no finite values")
242
325
 
243
- if not resolve_quiet(quiet): # accompanying statistics
244
- print("\n".join(x_stats(
245
- x_ser.to_numpy(dtype=float), x,
246
- 2 if digits_d is None else digits_d)))
247
- if form == "density" and kind in ("normal", "both"):
248
- if 2 < len(fx) < 5000: # R dn.main.R range
249
- W, p = sps.shapiro(fx)
250
- print("\nNull hypothesis is a normal population")
251
- print("Shapiro-Wilk normality test: "
252
- f"W = {W:.4f}, p-value = {p:.4f}")
253
- else:
254
- print("\nSample size out of range for the "
255
- "Shapiro-Wilk normality test")
326
+ # the statistics for fig.stats; the console report, ports of
327
+ # R's printers (x_console), follows the display it accompanies
328
+ x_stats(x_ser.to_numpy(dtype=float), x,
329
+ 2 if digits_d is None else digits_d)
330
+ show = not resolve_quiet(quiet)
331
+ pre, dname = xc.call_names(data_in)
332
+ x_lbl = ((getattr(data_in, "attrs", {}) or {})
333
+ .get("variable_labels", {}) or {}).get(x)
334
+ # parameters named in the call, which the suggestions skip
335
+ given = {nm for nm, v in (
336
+ ("bin_width", bin_width), ("bin_start", bin_start),
337
+ ("bin_end", bin_end), ("out_cut", out_cut or None),
338
+ ("fences", fences or None), ("vbs_mean", vbs_mean or None),
339
+ ("box_adj", box_adj or None)) if v is not None}
340
+
341
+ def say(*comps):
342
+ if show:
343
+ print(xc.report(*comps))
344
+
345
+ def one_var(sug):
346
+ """suggestions, summary, outliers: the single-group report"""
347
+ return [sug, xc.ss_numeric(x_raw, x, x_lbl, digits_d),
348
+ xc.outliers(fx, 2)]
349
+
350
+ def panel_table():
351
+ """the statistics of each panel (one facet variable)"""
352
+ ok = grp_ok
353
+ cells = f_raw[ok].astype(str)
354
+ name = facet_name
355
+ d = 2
356
+ if by_raw is not None:
357
+ cells = np.char.add(np.char.add(cells, ", "),
358
+ by_raw[ok].astype(str))
359
+ name = f"{facet_name}, {by}"
360
+ d = max(xc.max_dd(fx) + 1, 2) if digits_d is None \
361
+ else digits_d
362
+ return xc.facet_table(x_raw[ok], cells, x, name, d)
363
+
364
+ def bins(edges):
365
+ cnt = pd.cut(pd.Series(fx), edges, right=True,
366
+ include_lowest=True).value_counts(sort=False)
367
+ return xc.bin_table(edges, cnt.to_numpy(), len(fx),
368
+ xc.max_dd(fx) if digits_d is None
369
+ else digits_d)
256
370
 
257
371
  # ----- violin / box / strip (VBS) ----------------------------
258
372
  if form in _VBS_FORMS:
259
373
  vbs_plot = {"violin": "v", "box": "b",
260
374
  "strip": "s"}.get(form, form)
375
+ # the violin's smoothing is bw, the density curve's is
376
+ # bandwidth; both name the same quantity, so a bandwidth
377
+ # given without a bw is honored here (X.R)
378
+ if bw is None and bandwidth is not None:
379
+ bw = bandwidth
261
380
  fin = np.isfinite(x_ser.to_numpy(dtype=float))
262
381
  by_arr = by_order = None
263
382
  facet_arr = facet_order = None
@@ -279,8 +398,7 @@ def X(x, by=None, facet=None, data=None, filter=None,
279
398
  by_arr = by_ser.to_numpy()[fin]
280
399
  by_order = category_order(by_ser)
281
400
  n_g = len(by_order)
282
- pt_fill = [BASE_COLORS[i % len(BASE_COLORS)]
283
- for i in range(n_g)]
401
+ pt_fill = by_colors(n_g) # theme's palette, as R
284
402
  pt_color = pt_fill
285
403
  pt_trans = get_option("trans_pt_fill", 0.10)
286
404
  # strip-point colors per vbs_pt_fill, X.R VBS section
@@ -303,7 +421,7 @@ def X(x, by=None, facet=None, data=None, filter=None,
303
421
  get_column(data, ID, "ID")[keep]
304
422
  if ID is not None
305
423
  else x_ser.index).astype(str)[fin]
306
- return _x_finish(vbs_plotly(
424
+ fig = _x_finish(vbs_plotly(
307
425
  fx, x_name=x, vbs_plot=vbs_plot,
308
426
  by=by_arr, by_order=by_order, by_name=by,
309
427
  facet=facet_arr, facet_order=facet_order,
@@ -325,6 +443,49 @@ def X(x, by=None, facet=None, data=None, filter=None,
325
443
  ), rotate_x, rotate_y, scale_x, axis_fmt, axis_x_pre,
326
444
  2 if digits_d is None else digits_d)
327
445
 
446
+ # report, ~ .param.VBS() and the pivot tables of X.R, from
447
+ # the data as read rather than as jittered for the strip
448
+ prm = CALL_STATS.get() or {}
449
+ comps = [xc.suggest_vbs(
450
+ x, pre, dname, given,
451
+ n_by=len(by_order) if by_order is not None else 0,
452
+ by_name=by,
453
+ n_facet=len(facet_order) if facet_order is not None else 0,
454
+ facet_name=facet_name)]
455
+ ids_out = (np.asarray(get_column(data, ID, "ID")[keep])
456
+ .astype(str)[fin] if ID is not None else None)
457
+ if by is None and facet is None:
458
+ comps += [
459
+ xc.box_summary(fx, x, x_lbl, digits_d, k_iqr=k,
460
+ box_adj=box_adj, a=a, b=b),
461
+ xc.outliers(fx, digits_d, ids=ids_out, k_iqr=k,
462
+ box_adj=box_adj, a=a, b=b),
463
+ xc.dup_values(fx)]
464
+ elif facet is not None:
465
+ comps.append(xc.dup_values(fx, facet_arr, facet_order))
466
+ else:
467
+ comps.append(xc.dup_values(fx, by_arr, by_order))
468
+ comps.append(xc.vbs_params(
469
+ vbs_plot, prm,
470
+ show_bw=prm.get("bw") is not None))
471
+ sd = max(xc.max_dd(fx), 2) # integer data still needs
472
+ summ = ["", f"---------- Summary Statistics for {x}"]
473
+ if by is not None:
474
+ summ += [""] + xc.vbs_summary(fx, by_arr, by, sd,
475
+ order=by_order)
476
+ if facet_arr is not None:
477
+ summ += [""] + xc.vbs_summary(fx, facet_arr, facet_name,
478
+ sd, order=facet_order)
479
+ if facet2_arr is not None:
480
+ summ += [""] + xc.vbs_summary(fx, facet2_arr,
481
+ facet2_name, sd,
482
+ order=facet2_order)
483
+ if by is None and facet is None:
484
+ summ += [""] + xc.vbs_summary(fx, None, x, sd)
485
+ comps.append(summ)
486
+ say(*comps)
487
+ return fig
488
+
328
489
  # facet for histogram/density: aligned array + level order
329
490
  facet_arr = (facet_ser.to_numpy()
330
491
  if facet_ser is not None else None)
@@ -348,13 +509,14 @@ def X(x, by=None, facet=None, data=None, filter=None,
348
509
  if form == "freq_poly":
349
510
  edges = _breaks_from_args(fx, bin_start, bin_width,
350
511
  bin_end, breaks)
351
- return _x_finish(freq_poly_plotly(
512
+ fig = _x_finish(freq_poly_plotly(
352
513
  x_ser, by=by_ser, x_name=x, by_name=by,
353
514
  facet=facet_arr, facet_order=f_order,
354
515
  facet_name=facet_name,
355
516
  facet2=facet2_arr, facet2_order=f2_order,
356
517
  facet2_name=facet2_name,
357
518
  breaks=edges, proportion=stat == "proportion",
519
+ cumulate=cumulate != "off",
358
520
  fill=fill,
359
521
  fill_area=area_fill not in ("off", "transparent"),
360
522
  x_lab=x if xlab is None else xlab, y_lab=ylab,
@@ -364,12 +526,22 @@ def X(x, by=None, facet=None, data=None, filter=None,
364
526
  digits_d=3 if digits_d is None else digits_d,
365
527
  ), rotate_x, rotate_y, scale_x, axis_fmt, axis_x_pre,
366
528
  3 if digits_d is None else digits_d)
529
+ if facet_ser is not None:
530
+ if facet2_ser is None:
531
+ say(panel_table())
532
+ elif by_ser is not None: # X.R: summary and outliers
533
+ say(*one_var(None)[1:])
534
+ else:
535
+ say(*one_var(xc.suggest_freq_poly(x, pre, dname, given)),
536
+ bins(edges))
537
+ return fig
367
538
 
368
539
  # ----- density ---------------------------------------------
369
540
  if form == "density":
370
- bw = bandwidth if bandwidth is not None else bw_nrd0(fx)
541
+ # lessR's .band.width(): bw.nrd0 widened toward one peak
542
+ bw = bandwidth if bandwidth is not None else band_width(fx, 25)
371
543
  show_h = show_histogram and by is None
372
- return _x_finish(dn_plotly(
544
+ fig = _x_finish(dn_plotly(
373
545
  x_ser, by=by_ser, x_name=x, by_name=by,
374
546
  facet=facet_arr, facet_order=f_order,
375
547
  facet_name=facet_name,
@@ -393,6 +565,30 @@ def X(x, by=None, facet=None, data=None, filter=None,
393
565
  axis_fmt=axis_fmt, axis_x_pre=axis_x_pre,
394
566
  ), rotate_x, rotate_y, scale_x, axis_fmt, axis_x_pre,
395
567
  3 if digits_d is None else digits_d)
568
+ if facet_ser is not None:
569
+ if facet2_ser is None:
570
+ say(panel_table())
571
+ elif by_ser is not None:
572
+ say(xc.by_table(x_ser.to_numpy(dtype=float),
573
+ by_ser.to_numpy(), x, by, pre, dname,
574
+ order=category_order(by_ser)))
575
+ else: # dn.main.R
576
+ st = [f"--- Bandwidth --- for general curve: "
577
+ f"{bw:.4f}", ""]
578
+ if kind in ("normal", "both"):
579
+ if 2 < len(fx) < 5000:
580
+ W, p = sps.shapiro(fx)
581
+ record_stats(shapiro_W=float(W),
582
+ shapiro_p=float(p))
583
+ st += [" ", "Null hypothesis is a normal population",
584
+ f"Shapiro-Wilk normality test: W = {W:.4f},"
585
+ f" p-value = {p:.4f}"]
586
+ else:
587
+ st.append("Sample size out of range for "
588
+ "Shapiro-Wilk normality test.")
589
+ sug, ss, otl = one_var(xc.suggest_density(x, pre, dname))
590
+ say(sug, st, ss, otl)
591
+ return fig
396
592
 
397
593
  # ----- histogram ---------------------------------------------
398
594
  edges = _breaks_from_args(fx, bin_start, bin_width, bin_end,
@@ -419,8 +615,50 @@ def X(x, by=None, facet=None, data=None, filter=None,
419
615
  axis_x_pre, 2 if digits_d is None else digits_d)
420
616
  if add is not None:
421
617
  plt_add(fig, add, x1=x1, x2=x2, y1=y1, y2=y2)
618
+ if facet_ser is not None:
619
+ if facet2_ser is None: # as .bar.lattice()
620
+ say(panel_table())
621
+ elif by_ser is not None:
622
+ say(xc.by_table(x_ser.to_numpy(dtype=float), by_ser.to_numpy(),
623
+ x, by, pre, dname,
624
+ order=category_order(by_ser)))
625
+ else:
626
+ say(*one_var(xc.suggest_hist(x, pre, dname, given)),
627
+ bins(edges))
422
628
  return fig
423
629
 
424
630
 
631
+ def _per_variable(func):
632
+ """A list of x variables is one display per variable, as in R,
633
+ returned as a list of Figures; suggestions are not repeated for
634
+ each, as R gives none for several variables."""
635
+ import functools
636
+
637
+ @functools.wraps(func)
638
+ def wrapper(*args, **kwargs):
639
+ xs = kwargs["x"] if "x" in kwargs else (args[0] if args
640
+ else None)
641
+ if not isinstance(xs, (list, tuple)):
642
+ return func(*args, **kwargs)
643
+ sug = get_option("suggest", True)
644
+ set_option("suggest", False)
645
+ try:
646
+ figs = []
647
+ for xi in xs:
648
+ if "x" in kwargs:
649
+ figs.append(func(*args, **{**kwargs, "x": xi}))
650
+ else:
651
+ figs.append(func(xi, *args[1:], **kwargs))
652
+ return figs
653
+ finally:
654
+ set_option("suggest", sug)
655
+
656
+ return wrapper
657
+
658
+
425
659
  # font_size= scales all text of the returned figure
426
- X = font_scaled(X)
660
+ from .stats_out import attach_stats as _attach_stats # noqa: E402
661
+ from .style import with_theme as _with_theme # noqa: E402
662
+ from .utils import file_output # noqa: E402
663
+ X = file_output(_per_variable(font_scaled(_attach_stats(
664
+ _with_theme(X), capture=True))))