lessPython 0.1.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 (82) hide show
  1. lessPy/ANOVA.py +680 -0
  2. lessPy/Chart.py +1055 -0
  3. lessPy/Correlation.py +236 -0
  4. lessPy/Flows.py +116 -0
  5. lessPy/Logit.py +615 -0
  6. lessPy/Prop_test.py +267 -0
  7. lessPy/Regression.py +1491 -0
  8. lessPy/VariableLabels.py +119 -0
  9. lessPy/X.py +426 -0
  10. lessPy/XY.py +2007 -0
  11. lessPy/__init__.py +60 -0
  12. lessPy/anova_rmd.py +227 -0
  13. lessPy/bc_plotly.py +575 -0
  14. lessPy/bubble_plotly.py +470 -0
  15. lessPy/corCFA.py +316 -0
  16. lessPy/corEFA.py +220 -0
  17. lessPy/corPrint.py +45 -0
  18. lessPy/corProp.py +73 -0
  19. lessPy/corRead.py +48 -0
  20. lessPy/corReflect.py +72 -0
  21. lessPy/corReorder.py +161 -0
  22. lessPy/corScree.py +87 -0
  23. lessPy/data/Anova_1way.csv +25 -0
  24. lessPy/data/Anova_2way.csv +49 -0
  25. lessPy/data/Anova_rb.csv +8 -0
  26. lessPy/data/Anova_rbf.csv +49 -0
  27. lessPy/data/Anova_sp.csv +57 -0
  28. lessPy/data/BodyMeas.csv +341 -0
  29. lessPy/data/Cars93.csv +94 -0
  30. lessPy/data/Employee.csv +38 -0
  31. lessPy/data/Employee_lbl.csv +9 -0
  32. lessPy/data/FreqTable99.csv +5 -0
  33. lessPy/data/Jackets.csv +1026 -0
  34. lessPy/data/Learn.csv +35 -0
  35. lessPy/data/Mach4.csv +352 -0
  36. lessPy/data/Mach4_lbl.csv +21 -0
  37. lessPy/data/Reading.csv +101 -0
  38. lessPy/data/StockPrice.csv +1489 -0
  39. lessPy/data/WeightLoss.csv +11 -0
  40. lessPy/datasets.py +46 -0
  41. lessPy/date_infer.py +112 -0
  42. lessPy/details.py +314 -0
  43. lessPy/dn_plotly.py +495 -0
  44. lessPy/dot_plotly.py +385 -0
  45. lessPy/freq_poly_plotly.py +324 -0
  46. lessPy/getColors.py +399 -0
  47. lessPy/hier_plotly.py +352 -0
  48. lessPy/hs_plotly.py +395 -0
  49. lessPy/logit_rmd.py +410 -0
  50. lessPy/order_by.py +94 -0
  51. lessPy/pie_plotly.py +292 -0
  52. lessPy/pivot.py +158 -0
  53. lessPy/plotly_utils.py +787 -0
  54. lessPy/plt_add.py +129 -0
  55. lessPy/plt_contour.py +192 -0
  56. lessPy/plt_contour_facet.py +194 -0
  57. lessPy/plt_forecast.py +677 -0
  58. lessPy/plt_mat_plotly.py +201 -0
  59. lessPy/plt_plotly.py +216 -0
  60. lessPy/plt_smooth.py +170 -0
  61. lessPy/plt_time.py +143 -0
  62. lessPy/prob_norm.py +111 -0
  63. lessPy/prob_tcut.py +131 -0
  64. lessPy/prob_znorm.py +110 -0
  65. lessPy/radar_plotly.py +201 -0
  66. lessPy/reg_rmd.py +754 -0
  67. lessPy/rename.py +33 -0
  68. lessPy/reshape.py +95 -0
  69. lessPy/showColors.py +130 -0
  70. lessPy/simCImean.py +165 -0
  71. lessPy/simCLT.py +265 -0
  72. lessPy/simFlips.py +104 -0
  73. lessPy/simMeans.py +146 -0
  74. lessPy/stats_out.py +189 -0
  75. lessPy/ttest.py +641 -0
  76. lessPy/utils.py +235 -0
  77. lessPy/vbs_plotly.py +545 -0
  78. lesspython-0.1.0.dist-info/METADATA +93 -0
  79. lesspython-0.1.0.dist-info/RECORD +82 -0
  80. lesspython-0.1.0.dist-info/WHEEL +5 -0
  81. lesspython-0.1.0.dist-info/licenses/LICENSE +338 -0
  82. lesspython-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,470 @@
1
+ # bubble_plotly.py — analog of bubble.plotly.R
2
+ #
3
+ # Renders bubbles from ALREADY-TABULATED input:
4
+ # 1-D pandas Series: index = categories -> one row of bubbles
5
+ # along the x-axis, sized by value, with stems below
6
+ # 2-D pandas DataFrame: rows = `by` levels, columns = x
7
+ # categories -> bubble matrix (balloon plot), one bubble
8
+ # per cell, sized by the cell value
9
+ #
10
+ # Bubble diameter: max_px * v^power / max(v^power), with
11
+ # max_px = 2 * radius * dpi — size encodes magnitude, power
12
+ # tempers the visual dominance of large values.
13
+ #
14
+ # facet_tbls= renders the faceted 1-D grid: an ordered dict
15
+ # {facet level: 1-D Series on the full category set}, one panel
16
+ # per level, with bubble sizes and category hues normalized
17
+ # globally so the panels stay comparable.
18
+
19
+ import numpy as np
20
+ import pandas as pd
21
+ import plotly.graph_objects as go
22
+
23
+ from .utils import fmt, get_option
24
+ from .plotly_utils import (
25
+ BASE_COLORS, contrast_text_for_hex, facet_layout, make_trans,
26
+ plotly_style, to_hex,
27
+ )
28
+
29
+ _LABEL_VALUES = ("%", "input", "prop", "off")
30
+
31
+
32
+ def _diam_builder(all_values, power, radius, dpi):
33
+ """Diameter function normalized to the global max.
34
+ R analog: bubble.diam.builder()"""
35
+ v = np.maximum(0, np.asarray(all_values, dtype=float))
36
+ with np.errstate(invalid="ignore"):
37
+ max_ru = np.nanmax(v ** power) if v.size else np.nan
38
+ max_px_diam = 2 * radius * dpi
39
+
40
+ def diam(vals):
41
+ vals = np.maximum(0, np.asarray(vals, dtype=float))
42
+ if not np.isfinite(max_ru) or max_ru <= 0:
43
+ return np.zeros(len(vals))
44
+ return max_px_diam * (vals ** power) / max_ru
45
+ return diam
46
+
47
+
48
+ def _label_text(vals, share_tot, labels, digits_d, lbl_d):
49
+ if labels == "input":
50
+ return [fmt(v, digits_d) for v in vals]
51
+ if labels == "%":
52
+ return [f"{100 * s:.{lbl_d}f}%" for s in share_tot]
53
+ return [f"{s:.{lbl_d}f}" for s in share_tot] # "prop"
54
+
55
+
56
+ def bubble_plotly(x=None, x_name=None, y_name=None, by_name=None,
57
+ x_lab=None, y_lab=None, main=None,
58
+ fill=None, border="black", opacity=None,
59
+ power=0.5, radius=0.50,
60
+ digits_d=0,
61
+ labels=None, labels_position="in",
62
+ labels_color=None, labels_size=0.90,
63
+ labels_decimals=None,
64
+ label_min_px=26, label_autocontrast=True,
65
+ facet_tbls=None, facet_name=None, n_col=None,
66
+ style_opts=None):
67
+
68
+ if labels is None:
69
+ labels = "%" # R analog: match.arg default
70
+ if labels not in _LABEL_VALUES:
71
+ raise ValueError(f"labels must be one of {_LABEL_VALUES}")
72
+
73
+ if facet_tbls is not None: # panels replace the x table
74
+ x = next(iter(facet_tbls.values()))
75
+ two_d = facet_tbls is None and isinstance(x, pd.DataFrame)
76
+ if not two_d and not isinstance(x, pd.Series):
77
+ raise TypeError("x must be a pandas Series (1-D) or "
78
+ "DataFrame (2-D, rows = by levels)")
79
+
80
+ if x_name is None:
81
+ x_name = (x.columns.name if two_d else x.index.name) or "x"
82
+ if y_name is None:
83
+ y_name = "Count" if two_d else (x.name or "Count")
84
+ if two_d and by_name is None:
85
+ by_name = x.index.name or "by"
86
+ if style_opts is None:
87
+ style_opts = plotly_style()
88
+ if fill is None:
89
+ fill = BASE_COLORS
90
+
91
+ title_size = round(16 * get_option("main_size", 1))
92
+ dpi = get_option("plotly_dpi", 96)
93
+
94
+ alpha_fill = 0.85 if opacity is None else float(opacity)
95
+ if not np.isfinite(alpha_fill):
96
+ alpha_fill = 0.85
97
+ alpha_fill = max(0.0, min(1.0, alpha_fill))
98
+
99
+ # "prop" plots proportions, which digits_d (0 for counts)
100
+ # would flatten to "0"
101
+ lbl_d = (int(labels_decimals) if labels_decimals is not None
102
+ else (2 if labels == "prop"
103
+ else max(0, int(digits_d))))
104
+ trace_mode = "markers" if labels == "off" else "markers+text"
105
+ # None = the default (R's labels_position %||% "in")
106
+ txt_pos = ("middle center"
107
+ if labels_position is None
108
+ or str(labels_position).lower() == "in"
109
+ else "top center")
110
+ txt_size = round(14 * labels_size * get_option("axis_size", 0.9))
111
+
112
+ lab_font = round(15 * get_option("lab_size", 1))
113
+ tick_font = round(16 * get_option("axis_size", 0.9))
114
+
115
+ def label_colors(base_hex, n):
116
+ if labels_color is not None:
117
+ return [to_hex(labels_color)] * n
118
+ if label_autocontrast:
119
+ return [contrast_text_for_hex(h) for h in base_hex]
120
+ return ["black"] * n
121
+
122
+ def title_layout(fig):
123
+ if main:
124
+ fig.update_layout(
125
+ title=dict(text=main, x=0.5, xanchor="center",
126
+ y=0.98, yanchor="top",
127
+ font=dict(size=title_size,
128
+ color=to_hex(get_option(
129
+ "lab_color", "black")))),
130
+ margin=dict(t=round(title_size * 2.2),
131
+ r=25, b=30, l=25))
132
+
133
+ # ---------- faceted 1-D grid ------------------------------------
134
+ # One 1-D bubble panel per facet level; sizes and category hues
135
+ # are normalized globally so the panels stay comparable.
136
+ # R analog: bubble.plotly.R faceted 1-D path
137
+ if facet_tbls is not None:
138
+ fac_levels = [str(k) for k in facet_tbls.keys()]
139
+ tabs = list(facet_tbls.values())
140
+
141
+ all_vals = np.concatenate(
142
+ [t.to_numpy(dtype=float) for t in tabs])
143
+ diam_fun = _diam_builder(all_vals, power, radius, dpi)
144
+ max_diam = float(np.nanmax(diam_fun(all_vals))) \
145
+ if all_vals.size else 1.0
146
+ if not np.isfinite(max_diam) or max_diam <= 0:
147
+ max_diam = 1.0
148
+
149
+ # color keyed by category name, hues shared across panels
150
+ cats_all = [str(c) for c in tabs[0].index]
151
+ cat_hex_map = {c: to_hex(fill[i % len(fill)]
152
+ if isinstance(fill, (list, tuple))
153
+ else fill)
154
+ for i, c in enumerate(cats_all)}
155
+
156
+ def ann_adj(i, a, d):
157
+ a["y"] = min(d["y"][1] + 0.04, 0.99)
158
+ return a
159
+ domains, anns = facet_layout(
160
+ fac_levels, facet_name or "",
161
+ yanchor="bottom", ann_adjust=ann_adj, n_col=n_col)
162
+
163
+ target_r = 0.35
164
+ y_top = target_r * 1.20
165
+ seg_color = to_hex(style_opts["grid_color"])
166
+ hover = (f"{x_name}: %{{customdata.xcat}}"
167
+ f"<br>{y_name}: %{{hovertext}}"
168
+ f"<br>% of {x_name}: %{{customdata.pct_x:.1%}}"
169
+ "<br>% of total: %{customdata.pct_tot:.1%}"
170
+ "<extra></extra>")
171
+
172
+ fig = go.Figure()
173
+ stems = []
174
+ axes = {}
175
+ for i, (lv, t) in enumerate(zip(fac_levels, tabs)):
176
+ suf = "" if i == 0 else str(i + 1)
177
+ cats = [str(c) for c in t.index]
178
+ vals = t.to_numpy(dtype=float)
179
+ n = len(cats)
180
+
181
+ diam_px = diam_fun(vals)
182
+ total = np.nansum(vals)
183
+ share_tot = (vals / total if total > 0
184
+ else np.zeros(n))
185
+ cat_hex = [cat_hex_map[c] for c in cats]
186
+ fill_rgba = make_trans(cat_hex, alpha_fill)
187
+ txt_cols = label_colors(cat_hex, n)
188
+
189
+ fmt_val = [fmt(v, digits_d) for v in vals]
190
+ if labels == "off":
191
+ label_show = [""] * n
192
+ else:
193
+ txt = _label_text(vals, share_tot, labels,
194
+ digits_d, lbl_d)
195
+ label_show = [tx if np.isfinite(dp)
196
+ and dp >= label_min_px else ""
197
+ for tx, dp in zip(txt, diam_px)]
198
+ customdata = [dict(xcat=c, bycat="", pct_x=float(s),
199
+ pct_tot=float(s))
200
+ for c, s in zip(cats, share_tot)]
201
+
202
+ fig.add_trace(go.Scatter(
203
+ mode=trace_mode,
204
+ x=cats,
205
+ y=[0] * n,
206
+ xaxis=f"x{suf}",
207
+ yaxis=f"y{suf}",
208
+ hovertext=fmt_val,
209
+ hovertemplate=hover,
210
+ customdata=customdata,
211
+ marker=dict(
212
+ symbol="circle",
213
+ size=diam_px.tolist(),
214
+ sizemode="diameter",
215
+ color=fill_rgba,
216
+ line=dict(color=to_hex(border), width=1),
217
+ sizemin=12,
218
+ ),
219
+ text=label_show,
220
+ textposition=txt_pos,
221
+ textfont=dict(size=txt_size, color=txt_cols),
222
+ cliponaxis=False,
223
+ showlegend=False,
224
+ ))
225
+
226
+ # stems from the panel bottom to the base of each bubble
227
+ radius_data = (diam_px / max_diam) * target_r
228
+ stems += [dict(type="line",
229
+ xref=f"x{suf}", yref=f"y{suf}",
230
+ x0=k, x1=k,
231
+ y0=-1, y1=-float(radius_data[k]),
232
+ line=dict(color=seg_color, width=1),
233
+ layer="below")
234
+ for k in range(n)]
235
+
236
+ axes[f"xaxis{suf}"] = dict(
237
+ type="category",
238
+ domain=list(domains[i]["x"]),
239
+ anchor=f"y{suf}",
240
+ title=dict(text=x_lab if x_lab is not None
241
+ else x_name,
242
+ standoff=10,
243
+ font=dict(size=lab_font)),
244
+ showgrid=False, zeroline=False, showline=True,
245
+ ticks="outside",
246
+ tickfont=dict(size=tick_font),
247
+ )
248
+ axes[f"yaxis{suf}"] = dict(
249
+ visible=False, fixedrange=True,
250
+ domain=list(domains[i]["y"]),
251
+ anchor=f"x{suf}",
252
+ range=[-1, y_top],
253
+ )
254
+
255
+ fig.update_layout(
256
+ hoverlabel=dict(align="left"),
257
+ annotations=anns,
258
+ shapes=stems,
259
+ margin=dict(t=round(title_size * 2.4) if main else 40,
260
+ b=8, l=20, r=20),
261
+ showlegend=False,
262
+ template=None,
263
+ plot_bgcolor=to_hex(style_opts["panel_fill"]),
264
+ paper_bgcolor=to_hex(style_opts["window_fill"]),
265
+ **axes,
266
+ )
267
+ if main:
268
+ fig.update_layout(title=dict(
269
+ text=main, x=0.5, xanchor="center",
270
+ y=0.98, yanchor="top",
271
+ font=dict(size=title_size,
272
+ color=to_hex(get_option("lab_color",
273
+ "black")))))
274
+ return fig
275
+
276
+ # ---------- single 1-D chart ------------------------------------
277
+ if not two_d:
278
+ cats = [str(c) for c in x.index]
279
+ vals = x.to_numpy(dtype=float)
280
+ n = len(cats)
281
+
282
+ diam_fun = _diam_builder(vals, power, radius, dpi)
283
+ diam_px = diam_fun(vals)
284
+ max_diam = float(np.nanmax(diam_px)) if n else 1.0
285
+ if not np.isfinite(max_diam) or max_diam <= 0:
286
+ max_diam = 1.0
287
+
288
+ total = np.nansum(vals)
289
+ share_tot = vals / total if total > 0 else np.zeros(n)
290
+
291
+ # one palette color per category, as in the bar chart
292
+ cat_hex = to_hex([fill[i % len(fill)]
293
+ if isinstance(fill, (list, tuple))
294
+ else fill for i in range(n)])
295
+ fill_rgba = make_trans(cat_hex, alpha_fill)
296
+ txt_cols = label_colors(cat_hex, n)
297
+
298
+ fmt_val = [fmt(v, digits_d) for v in vals]
299
+ if labels == "off":
300
+ label_show = [""] * n
301
+ else:
302
+ txt = _label_text(vals, share_tot, labels, digits_d,
303
+ lbl_d)
304
+ label_show = [t if np.isfinite(dp) and dp >= label_min_px
305
+ else "" for t, dp in zip(txt, diam_px)]
306
+
307
+ customdata = [dict(xcat=c, bycat="", pct_x=float(s),
308
+ pct_tot=float(s))
309
+ for c, s in zip(cats, share_tot)]
310
+ hover = (f"{x_name}: %{{customdata.xcat}}"
311
+ f"<br>{y_name}: %{{hovertext}}"
312
+ f"<br>% of {x_name}: %{{customdata.pct_x:.1%}}"
313
+ "<br>% of total: %{customdata.pct_tot:.1%}"
314
+ "<extra></extra>")
315
+
316
+ fig = go.Figure(go.Scatter(
317
+ mode=trace_mode,
318
+ x=cats,
319
+ y=[0] * n,
320
+ hovertext=fmt_val,
321
+ hovertemplate=hover,
322
+ customdata=customdata,
323
+ marker=dict(
324
+ symbol="circle",
325
+ size=diam_px.tolist(),
326
+ sizemode="diameter",
327
+ color=fill_rgba,
328
+ line=dict(color=to_hex(border), width=1),
329
+ sizemin=12,
330
+ ),
331
+ text=label_show,
332
+ textposition=txt_pos,
333
+ textfont=dict(size=txt_size, color=txt_cols),
334
+ cliponaxis=False,
335
+ showlegend=False,
336
+ ))
337
+
338
+ # stems from the plot bottom to the base of each bubble
339
+ target_r = 0.35
340
+ radius_data = (diam_px / max_diam) * target_r
341
+ y_top = target_r * 1.20
342
+ seg_color = to_hex(style_opts["grid_color"])
343
+ stems = [dict(type="line", xref="x", yref="y",
344
+ x0=i, x1=i, y0=-1, y1=-float(radius_data[i]),
345
+ line=dict(color=seg_color, width=1),
346
+ layer="below")
347
+ for i in range(n)]
348
+
349
+ fig.update_layout(
350
+ hoverlabel=dict(align="left"),
351
+ xaxis=dict(
352
+ type="category",
353
+ automargin=True,
354
+ title=dict(text=x_lab if x_lab is not None
355
+ else x_name,
356
+ standoff=10,
357
+ font=dict(size=lab_font)),
358
+ showgrid=False, zeroline=False, showline=True,
359
+ ticks="outside",
360
+ tickfont=dict(size=tick_font),
361
+ ),
362
+ yaxis=dict(visible=False, fixedrange=True,
363
+ range=[-1, y_top]),
364
+ shapes=stems,
365
+ template=None,
366
+ plot_bgcolor=to_hex(style_opts["panel_fill"]),
367
+ paper_bgcolor=to_hex(style_opts["window_fill"]),
368
+ )
369
+ title_layout(fig)
370
+ return fig
371
+
372
+ # ---------- 2-D bubble matrix ------------------------------------
373
+ groups = [str(g) for g in x.index]
374
+ cats = [str(c) for c in x.columns]
375
+ mat = x.to_numpy(dtype=float)
376
+ n_g = len(groups)
377
+
378
+ total = np.nansum(mat)
379
+ share_tot = mat / total if total > 0 else np.zeros(mat.shape)
380
+ col_tot = np.nansum(mat, axis=0)
381
+ with np.errstate(invalid="ignore", divide="ignore"):
382
+ share_x = np.where(col_tot > 0, mat / col_tot, 0.0)
383
+
384
+ diam_fun = _diam_builder(mat.ravel(), power, radius, dpi)
385
+
386
+ # one color per by group
387
+ grp_hex = to_hex([fill[i % len(fill)]
388
+ if isinstance(fill, (list, tuple)) else fill
389
+ for i in range(n_g)])
390
+ grp_rgba = make_trans(grp_hex, alpha_fill)
391
+ grp_txt = label_colors(grp_hex, n_g)
392
+
393
+ hover = (f"{x_name}: %{{customdata.xcat}}"
394
+ f"<br>{by_name}: %{{customdata.bycat}}"
395
+ f"<br>{y_name}: %{{hovertext}}"
396
+ f"<br>% of {x_name}: %{{customdata.pct_x:.1%}}"
397
+ "<br>% of total: %{customdata.pct_tot:.1%}"
398
+ "<extra></extra>")
399
+
400
+ fig = go.Figure()
401
+ for i, g in enumerate(groups):
402
+ row = mat[i]
403
+ diam_px = diam_fun(row)
404
+ fmt_val = [fmt(v, digits_d) for v in row]
405
+ if labels == "off":
406
+ label_show = [""] * len(row)
407
+ else:
408
+ txt = _label_text(row, share_tot[i], labels, digits_d,
409
+ lbl_d)
410
+ label_show = [t if np.isfinite(dp) and dp >= label_min_px
411
+ else "" for t, dp in zip(txt, diam_px)]
412
+ customdata = [dict(xcat=c, bycat=g, pct_x=float(px),
413
+ pct_tot=float(pt))
414
+ for c, px, pt in zip(cats, share_x[i],
415
+ share_tot[i])]
416
+ fig.add_trace(go.Scatter(
417
+ mode=trace_mode,
418
+ x=cats,
419
+ y=[g] * len(cats),
420
+ hovertext=fmt_val,
421
+ hovertemplate=hover,
422
+ customdata=customdata,
423
+ marker=dict(
424
+ symbol="circle",
425
+ size=diam_px.tolist(),
426
+ sizemode="diameter",
427
+ color=grp_rgba[i],
428
+ line=dict(color=to_hex(border), width=1),
429
+ ),
430
+ text=label_show,
431
+ textposition=txt_pos,
432
+ textfont=dict(size=txt_size, color=grp_txt[i]),
433
+ cliponaxis=False,
434
+ showlegend=False,
435
+ ))
436
+
437
+ fig.update_layout(
438
+ hoverlabel=dict(align="left"),
439
+ xaxis=dict(
440
+ type="category",
441
+ automargin=True,
442
+ title=dict(text=x_lab if x_lab is not None else x_name,
443
+ font=dict(size=lab_font)),
444
+ showgrid=True,
445
+ gridcolor=to_hex(style_opts["grid_color"]),
446
+ gridwidth=1,
447
+ zeroline=False,
448
+ tickfont=dict(size=tick_font),
449
+ ),
450
+ yaxis=dict(
451
+ type="category",
452
+ automargin=True,
453
+ title=dict(text=y_lab if y_lab is not None else by_name,
454
+ font=dict(size=lab_font)),
455
+ showgrid=False, zeroline=False,
456
+ categoryorder="array",
457
+ categoryarray=list(reversed(groups)),
458
+ tickfont=dict(size=tick_font),
459
+ ),
460
+ template=None,
461
+ shapes=[dict(type="rect", xref="paper", yref="paper",
462
+ x0=0, x1=1, y0=0, y1=1,
463
+ line=dict(color=to_hex(
464
+ style_opts["panel_border"]), width=1),
465
+ layer="below")],
466
+ plot_bgcolor=to_hex(style_opts["panel_fill"]),
467
+ paper_bgcolor=to_hex(style_opts["window_fill"]),
468
+ )
469
+ title_layout(fig)
470
+ return fig