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/details.py CHANGED
@@ -129,6 +129,11 @@ def details(data=None, n_mcut=1, max_lines=30, miss_show=30,
129
129
 
130
130
  names = list(data.columns)
131
131
  types = [_type_label(data[c]) for c in names]
132
+ # whole numbers that pandas stores as float64 (a pandas integer
133
+ # column cannot hold NaN): reported as integer, as lessR does,
134
+ # but flagged so the report states what pandas actually stores
135
+ stored_float = [t == "integer" and pdt.is_float_dtype(data[c].dtype)
136
+ for c, t in zip(names, types)]
132
137
  n_val = [int(data[c].notna().sum()) for c in names]
133
138
  n_miss = [int(data[c].isna().sum()) for c in names]
134
139
  n_uniq = [int(data[c].dropna().nunique()) for c in names]
@@ -137,7 +142,8 @@ def details(data=None, n_mcut=1, max_lines=30, miss_show=30,
137
142
  _print_types(types)
138
143
  first_last = [_first_last(data[c], n_obs) for c in names]
139
144
  maybe_id, id_col = _print_table(names, types, n_val, n_miss,
140
- n_uniq, first_last, n_var)
145
+ n_uniq, first_last, n_var,
146
+ stored_float)
141
147
 
142
148
  if maybe_id is not None and not var_labels:
143
149
  _print_id_note(maybe_id, id_col)
@@ -196,10 +202,13 @@ def _print_types(types):
196
202
 
197
203
 
198
204
  def _print_table(names, types, n_val, n_miss, n_uniq, first_last,
199
- n_var):
205
+ n_var, stored_float=None):
206
+ if stored_float is None:
207
+ stored_float = [False] * n_var
208
+ shown = [t + "*" if f else t for t, f in zip(types, stored_float)]
200
209
  w_num = max(2, len(str(n_var)))
201
210
  w_nam = max(len("Variable"), *(len(x) for x in names))
202
- w_typ = max(len("Type"), *(len(t) for t in types))
211
+ w_typ = max(len("Type"), *(len(t) for t in shown))
203
212
  w_val = max(len("Values"), *(len(str(x)) for x in n_val))
204
213
  w_mis = max(len("Missing"), *(len(str(x)) for x in n_miss))
205
214
  w_uni = max(len("Unique"), *(len(str(x)) for x in n_uniq))
@@ -215,7 +224,7 @@ def _print_table(names, types, n_val, n_miss, n_uniq, first_last,
215
224
  maybe_id, id_col = None, 0
216
225
  for i in range(n_var):
217
226
  print(f"{i + 1:>{w_num}} {names[i]:<{w_nam}} "
218
- f"{types[i]:<{w_typ}} {n_val[i]:>{w_val}} "
227
+ f"{shown[i]:<{w_typ}} {n_val[i]:>{w_val}} "
219
228
  f"{n_miss[i]:>{w_mis}} {n_uniq[i]:>{w_uni}} "
220
229
  f"{first_last[i]}")
221
230
  if (n_uniq[i] == n_val[i]
@@ -223,6 +232,10 @@ def _print_table(names, types, n_val, n_miss, n_uniq, first_last,
223
232
  "character")):
224
233
  maybe_id, id_col = names[i], i + 1
225
234
  _dash(len(hdr))
235
+ if any(stored_float):
236
+ print("* Whole numbers stored by Pandas as float64 (double), "
237
+ "usually because\n the column had missing values, NaN, "
238
+ "which a Pandas integer column\n cannot hold.")
226
239
  return maybe_id, id_col
227
240
 
228
241
 
lessPy/dn_plotly.py CHANGED
@@ -58,11 +58,13 @@ def dn_plotly(x, by=None, x_name=None, by_name=None,
58
58
  from .utils import bw_nrd0
59
59
  bw = bw_nrd0(x)
60
60
  h = bw * adjust
61
+ from .stats_out import record_stats
62
+ record_stats(bw=float(h))
61
63
 
62
64
  if facet is not None:
63
65
  if fill_miss and by is not None:
64
- from .plotly_utils import BASE_COLORS
65
- fill = BASE_COLORS
66
+ from .plotly_utils import by_colors
67
+ fill = by_colors(len(pd.Series(by).dropna().unique()))
66
68
  return _dn_facet(x, facet, facet_order, x_name,
67
69
  facet_name, fill, x_lab, y_lab, main,
68
70
  h, n, fill_area, style_opts,
@@ -91,8 +93,8 @@ def dn_plotly(x, by=None, x_name=None, by_name=None,
91
93
 
92
94
  # groups need distinguishable hues: default to the palette
93
95
  if fill_miss and G > 1:
94
- from .plotly_utils import BASE_COLORS
95
- fill = BASE_COLORS
96
+ from .plotly_utils import by_colors
97
+ fill = by_colors(G)
96
98
 
97
99
  def group_x(gname):
98
100
  xg = x if by is None else x[(by == gname).to_numpy()]
@@ -124,6 +126,11 @@ def dn_plotly(x, by=None, x_name=None, by_name=None,
124
126
  dx = grid[1] - grid[0]
125
127
 
126
128
  # --- densities per group -------------------------------------
129
+ # R analog: dn.plotly.R — kind "normal"/"both" adds the normal
130
+ # fit to each group by its own mean and sd on the shared grid;
131
+ # a group of identical values has no normal to draw
132
+ do_gen = kind in ("general", "both")
133
+ do_nrm = kind in ("normal", "both")
127
134
  dens = []
128
135
  ymax = 0.0
129
136
  for gname in groups:
@@ -133,19 +140,21 @@ def dn_plotly(x, by=None, x_name=None, by_name=None,
133
140
  [[0], np.cumsum((y[1:] + y[:-1]) * 0.5 * dx)])
134
141
  if cum[-1] > 0:
135
142
  cum = cum / cum[-1]
136
- dens.append(dict(y=y, cum=cum, mn=float(xg.mean())))
137
- ymax = max(ymax, float(y.max()))
138
-
139
- # --- normal curve and background histogram --------------------
140
- # R analog: dn.main.R — kind "normal"/"both" adds the normal
141
- # curve at the sample mean and sd; show_histogram draws a
142
- # faint density-scaled histogram behind the curves
143
- d_nrm = None
144
- if kind in ("normal", "both"):
145
- xg0 = group_x(groups[0])
146
- mu, sd = float(xg0.mean()), float(xg0.std(ddof=1))
147
- z = (grid - mu) / sd
148
- d_nrm = np.exp(-0.5 * z * z) / (sd * np.sqrt(2 * np.pi))
143
+ sd = float(xg.std(ddof=1))
144
+ nrm = None
145
+ if do_nrm and np.isfinite(sd) and sd > 0:
146
+ z = (grid - float(xg.mean())) / sd
147
+ nrm = np.exp(-0.5 * z * z) / (sd * np.sqrt(2 * np.pi))
148
+ dens.append(dict(y=y, cum=cum, mn=float(xg.mean()), nrm=nrm))
149
+ # only a curve that is drawn sets the height of the y-axis
150
+ if do_gen:
151
+ ymax = max(ymax, float(y.max()))
152
+ if nrm is not None:
153
+ ymax = max(ymax, float(nrm.max()))
154
+
155
+ # --- background histogram -------------------------------------
156
+ # show_histogram draws a faint density-scaled histogram behind
157
+ # the curves (dn.main.R)
149
158
  hist_y = None
150
159
  if show_histogram and hist_edges is not None:
151
160
  e = np.asarray(hist_edges, dtype=float)
@@ -155,10 +164,6 @@ def dn_plotly(x, by=None, x_name=None, by_name=None,
155
164
  hist_w = np.diff(e)
156
165
  hist_y = cnt / (tot * hist_w) # density scale
157
166
  hist_mids = (e[:-1] + e[1:]) / 2
158
- if kind == "normal": # general curve not drawn
159
- ymax = 0.0
160
- if d_nrm is not None:
161
- ymax = max(ymax, float(d_nrm.max()))
162
167
  if hist_y is not None:
163
168
  ymax = max(ymax, float(hist_y.max()))
164
169
 
@@ -195,12 +200,16 @@ def dn_plotly(x, by=None, x_name=None, by_name=None,
195
200
  if fill_hist is None else fill_hist),
196
201
  line=dict(width=0)),
197
202
  hoverinfo="skip", showlegend=False))
198
- if kind in ("general", "both"):
199
- for g, gname in enumerate(groups):
200
- d = dens[g]
201
- hv = hover(d)
202
- if G > 1 and by_name:
203
- hv = [f"{by_name}: {gname}<br>{t}" for t in hv]
203
+ # the normal curve: its own color for a single distribution, but
204
+ # the group's color when there are groups to keep apart, dashed
205
+ # beside that group's general curve (dn.plotly.R)
206
+ nrm_fill = (None if fill_normal in (None, "transparent")
207
+ else as_plotly_color(fill_normal))
208
+ for g, gname in enumerate(groups):
209
+ d = dens[g]
210
+ pre = (f"{by_name}: {gname}<br>"
211
+ if G > 1 and by_name else "")
212
+ if do_gen:
204
213
  fig.add_trace(go.Scatter(
205
214
  mode="lines",
206
215
  x=grid, y=d["y"],
@@ -209,29 +218,40 @@ def dn_plotly(x, by=None, x_name=None, by_name=None,
209
218
  fill="tozeroy" if fill_area else "none",
210
219
  fillcolor=fill_rgba[g] if fill_area else None,
211
220
  hoverinfo="text",
212
- hovertext=hv,
221
+ hovertext=[pre + t for t in hover(d)],
213
222
  showlegend=G > 1,
214
223
  ))
215
- # vertical line at the group mean, up to the curve
216
- y_mn = float(np.interp(d["mn"], grid, d["y"]))
217
- mean_line = dict(color=line_rgba[g], dash="dash",
218
- width=1.4 if fill_area else 0.7)
224
+ if d["nrm"] is not None:
225
+ nrm_col = line_rgba[g] if G > 1 else to_hex(color_normal)
226
+ filled = fill_area and G == 1 and nrm_fill is not None
219
227
  fig.add_trace(go.Scatter(
220
- mode="lines",
221
- x=[d["mn"], d["mn"]], y=[0, y_mn],
222
- line=mean_line,
223
- hoverinfo="skip", showlegend=False,
228
+ mode="lines", x=grid, y=d["nrm"],
229
+ name=(None if G == 1 else
230
+ f"{gname} normal" if do_gen else gname),
231
+ showlegend=G > 1 and not do_gen,
232
+ line=dict(color=nrm_col, width=1.4,
233
+ dash="dash" if do_gen and G > 1
234
+ else "solid"),
235
+ fill="tozeroy" if filled else "none",
236
+ fillcolor=nrm_fill if filled else None,
237
+ hoverinfo="text",
238
+ hovertext=[
239
+ f"{pre}{x_name}: {grid[i]:.6g}"
240
+ f"<br>Normal density="
241
+ f"{round(float(d['nrm'][i]), 6):g}"
242
+ for i in range(n)],
224
243
  ))
225
- if d_nrm is not None: # normal curve
226
- nrm_fill = (None if fill_normal in (None, "transparent")
227
- else as_plotly_color(fill_normal))
244
+ # vertical line at the group mean, up to whichever curve
245
+ # is drawn
246
+ curve = d["y"] if do_gen or d["nrm"] is None else d["nrm"]
247
+ y_mn = float(np.interp(d["mn"], grid, curve))
228
248
  fig.add_trace(go.Scatter(
229
- mode="lines", x=grid, y=d_nrm,
230
- line=dict(color=to_hex(color_normal),
231
- width=1.35 if nrm_fill is None else 1),
232
- fill="none" if nrm_fill is None else "tozeroy",
233
- fillcolor=nrm_fill,
234
- hoverinfo="skip", showlegend=False))
249
+ mode="lines",
250
+ x=[d["mn"], d["mn"]], y=[0, y_mn],
251
+ line=dict(color=line_rgba[g], dash="dash",
252
+ width=1.4 if fill_area else 0.7),
253
+ hoverinfo="skip", showlegend=False,
254
+ ))
235
255
  if rug: # ticks at each data value
236
256
  xr, yr = [], []
237
257
  y1r = -0.05 * ymax
@@ -48,6 +48,7 @@ def freq_poly_plotly(x, by=None, x_name=None, by_name=None,
48
48
  facet2=None, facet2_order=None,
49
49
  facet2_name=None,
50
50
  breaks=None, proportion=False,
51
+ cumulate=False,
51
52
  fill=None, fill_area=True,
52
53
  x_lab=None, y_lab=None, main=None,
53
54
  n_col=1, axis_fmt="K", axis_x_pre="",
@@ -65,6 +66,8 @@ def freq_poly_plotly(x, by=None, x_name=None, by_name=None,
65
66
  if y_lab is None:
66
67
  y_lab = (f"Proportion of {x_name}" if proportion
67
68
  else f"Count of {x_name}")
69
+ if cumulate:
70
+ y_lab = f"Cumulative {y_lab}"
68
71
 
69
72
  if facet is not None:
70
73
  return _fp_facet(x, facet, facet_order, x_name,
@@ -90,8 +93,8 @@ def freq_poly_plotly(x, by=None, x_name=None, by_name=None,
90
93
  # groups need distinguishable hues: default to the palette
91
94
  if fill is None:
92
95
  if G > 1:
93
- from .plotly_utils import BASE_COLORS
94
- fill = BASE_COLORS
96
+ from .plotly_utils import by_colors
97
+ fill = by_colors(G)
95
98
  else:
96
99
  fill = get_option("bar_fill_cont", "#96AAC3")
97
100
 
@@ -107,6 +110,8 @@ def freq_poly_plotly(x, by=None, x_name=None, by_name=None,
107
110
  xg = x if by is None else x[(by == gname).to_numpy()]
108
111
  xg = xg[np.isfinite(xg)]
109
112
  yv = _poly_counts(xg, breaks, proportion)
113
+ if cumulate: # running total over the bins
114
+ yv = np.cumsum(yv)
110
115
  polys.append(yv)
111
116
  ymax = max(ymax, float(yv.max()))
112
117
 
@@ -133,7 +138,9 @@ def freq_poly_plotly(x, by=None, x_name=None, by_name=None,
133
138
  for g, gname in enumerate(groups):
134
139
  xx = np.concatenate([[mids[0] - step], mids,
135
140
  [mids[-1] + step]])
136
- yy = np.concatenate([[0], polys[g], [0]])
141
+ # a cumulative polygon (ogive) holds its total at the right
142
+ yy = np.concatenate([[0], polys[g],
143
+ [polys[g][-1] if cumulate else 0]])
137
144
  hover = [
138
145
  (f"{x_name}: {xv:.6g}"
139
146
  f"<br>{y_lab}: {round(float(yv), digits_d):g}")
@@ -221,8 +228,8 @@ def _fp_facet(x, facet, facet_order, x_name, facet_name, breaks,
221
228
 
222
229
  if fill is None:
223
230
  if G > 1:
224
- from .plotly_utils import BASE_COLORS
225
- fill = BASE_COLORS
231
+ from .plotly_utils import by_colors
232
+ fill = by_colors(G)
226
233
  else:
227
234
  fill = get_option("bar_fill_cont", "#96AAC3")
228
235
 
lessPy/getColors.py CHANGED
@@ -5,7 +5,9 @@
5
5
  # The HCL families (hues/qualitative, sequential, divergent) are
6
6
  # reproduced exactly from R's grDevices HCL and colorspace
7
7
  # trajectories; the manual families interpolate in RGB; Okabe-Ito,
8
- # Tableau and "distinct" are fixed lists. The viridis family maps
8
+ # Tableau and "distinct" are fixed lists. The R palettes "rainbow",
9
+ # "heat", "terrain" (grDevices HSV) and their colorspace "_hcl"
10
+ # counterparts are computed exactly as R does. The viridis family maps
9
11
  # to plotly's built-in colorscales (a documented approximation),
10
12
  # and R's wesanderson palettes are not ported (external package).
11
13
 
@@ -37,6 +39,9 @@ _SEQ_HUE = {
37
39
  "magentas": 330, "grays": 0,
38
40
  }
39
41
  _SEQ_NAMES = list(_SEQ_HUE)
42
+ # R's own palettes, grDevices and colorspace. R analog: nmR
43
+ _SEQ_R = ("rainbow", "heat", "terrain",
44
+ "rainbow_hcl", "heat_hcl", "terrain_hcl")
40
45
  _VIRIDIS = {"viridis": "Viridis", "cividis": "Cividis",
41
46
  "plasma": "Plasma", "inferno": "Inferno",
42
47
  "spectral": "Spectral"}
@@ -151,20 +156,63 @@ def _rgb_to_hcl(r, g, b):
151
156
  return math.degrees(math.atan2(V, U)) % 360, math.hypot(U, V), L
152
157
 
153
158
 
159
+ def _xy_to_xyz(x, y):
160
+ return (x / y, 1.0, (1 - x - y) / y)
161
+
162
+
163
+ def _srgb_to_luv_matrix():
164
+ """R's make.rgb() for sRGB: primaries and the D65 white (x, y),
165
+ the matrix scaled so that white maps to white. R's white.points
166
+ table gives D65 as (0.3137, 0.3291), not the CIE (0.3127,
167
+ 0.3290), so that is the white used, as convertColor() uses it."""
168
+ import numpy as np
169
+ prim = np.array([_xy_to_xyz(0.64, 0.33), _xy_to_xyz(0.30, 0.60),
170
+ _xy_to_xyz(0.15, 0.06)]).T
171
+ white = np.array(_xy_to_xyz(0.3137, 0.3291))
172
+ scale = np.linalg.solve(prim, white)
173
+ return prim * scale, white
174
+
175
+
154
176
  def _color_hc(col):
155
- h, c, _ = _rgb_to_hcl(*_to_rgb(col))
156
- return h, c
177
+ """Hue and chroma of a color in CIE Luv, as R's .getHC() reads
178
+ them with grDevices::convertColor(from="sRGB", to="Luv")."""
179
+ import math
180
+ import numpy as np
181
+ r, g, b = (v / 255 for v in _to_rgb(col))
182
+ lin = [v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4
183
+ for v in (r, g, b)]
184
+ M, (Xn, Yn, Zn) = _srgb_to_luv_matrix()
185
+ X, Y, Z = M @ np.array(lin)
186
+ yr = Y / Yn
187
+ eps, kappa = 216 / 24389, 24389 / 27
188
+ L = 116 * yr ** (1 / 3) - 16 if yr > eps else kappa * yr
189
+ den, den_n = X + 15 * Y + 3 * Z, Xn + 15 * Yn + 3 * Zn
190
+ if den == 0:
191
+ return 0.0, 0.0
192
+ u = 13 * L * (4 * X / den - 4 * Xn / den_n)
193
+ v = 13 * L * (9 * Y / den - 9 * Yn / den_n)
194
+ return math.degrees(math.atan2(v, u)) % 360, math.hypot(u, v)
195
+
196
+
197
+ # hue of each theme. R analog: .get.h()
198
+ _THEME_HUE = {"gray": 0, "white": 0, "colors": 240, "lightbronze": 240,
199
+ "dodgerblue": 240, "blue": 240, "gold": 60, "brown": 60,
200
+ "sienna": 60, "orange": 30, "darkred": 0, "red": 0,
201
+ "rose": 0, "slatered": 0, "darkgreen": 120, "green": 120,
202
+ "purple": 300}
157
203
 
158
204
 
159
205
  def bar_fill_colors(values, fill, fill_split, fill_scaled,
160
- fill_chroma=75):
206
+ fill_chroma=75, theme=None):
161
207
  """Per-bar fill colors for a numeric bar chart, keyed to the bar
162
208
  values. fill_split gives a two-color split at that value (below
163
209
  -> first color, above -> second). fill_scaled makes an HCL
164
210
  gradient: chroma and darkness rise with distance from the split
165
211
  (default 0), hue by side. A faithful approximation of R's
166
- .scale.clr, not colorspace-exact. R analog: fill_split /
167
- fill_scaled."""
212
+ .scale.clr: with no fill, the theme's hue at fill_chroma (gray
213
+ for the gray and white themes); with one fill color, that
214
+ color's hue and chroma, the luminance alone varying. R analog:
215
+ fill_split / fill_scaled, .scale.clr()"""
168
216
  import numpy as np
169
217
  from .plotly_utils import to_hex
170
218
  x = np.asarray(values, dtype=float)
@@ -184,12 +232,17 @@ def bar_fill_colors(values, fill, fill_split, fill_scaled,
184
232
  normed = dist / mx
185
233
  lum = 27 + (1 - normed) * 53 # near split light, far dark
186
234
 
187
- if fills is None or len(fills) > 2:
188
- h1 = h2 = 255.0
189
- c1 = c2 = float(fill_chroma)
190
- elif len(fills) == 1:
191
- h1, c1 = _color_hc(fills[0])
192
- h2, c2 = h1, c1
235
+ if fills is None or len(fills) > 2 or len(fills) == 1:
236
+ # one hue and chroma for every bar, only the luminance
237
+ # following the value, as R's .scale.clr()
238
+ if fills is None or len(fills) > 2:
239
+ gray = theme in ("gray", "white")
240
+ hue = 0.0 if gray else float(_THEME_HUE.get(theme or
241
+ "colors", 240))
242
+ chroma = 0.0 if gray else float(fill_chroma)
243
+ else:
244
+ hue, chroma = _color_hc(fills[0])
245
+ return [hcl(hue, chroma, L) for L in lum]
193
246
  else:
194
247
  h1, c1 = _color_hc(fills[0])
195
248
  h2, c2 = _color_hc(fills[1])
@@ -227,18 +280,185 @@ def _ramp(colors, n):
227
280
  return out
228
281
 
229
282
 
283
+ # grDevices .hcl_colors_parameters for the viridis family that lessR
284
+ # draws from hcl.colors(): type, h1 h2 h3, c1 c2 c3, l1 l2 l3,
285
+ # p1 p2 p3 p4, cmax1 cmax2 (None = NA)
286
+ _HCL_PARAMS = {
287
+ "viridis": ("sequential", 300, 75, None, 40, 95, None, 15, 90,
288
+ None, 1.0, 1.1, None, None, None, None),
289
+ "plasma": ("sequential", -100, 100, None, 60, 100, None, 15, 95,
290
+ None, 2.0, 0.9, None, None, None, None),
291
+ "inferno": ("sequential", -100, 85, None, 0, 65, None, 1, 98,
292
+ None, 1.1, 0.9, None, None, 120, None),
293
+ "cividis": ("divergingx", 255, None, 75, 30, 0, 95, 13, 52, 92,
294
+ 1.1, 1.0, 1, None, 47, None),
295
+ "spectral": ("divergingx", 0, 85, 270, 90, 45, 65, 37, 98, 37,
296
+ 1, 1.2, 1, 1.2, 120, None),
297
+ }
298
+
299
+
300
+ def _hcl_seq(i, h1, h2, c1, c2, l1, l2, p1, p2, cmax):
301
+ """grDevices hcl.colors() seqhcl: hue linear, chroma linear or
302
+ triangular through cmax, luminance by power."""
303
+ def lin(t, a, b):
304
+ return b - (b - a) * t
305
+ j = None
306
+ if cmax is not None:
307
+ try:
308
+ j = 1 / (1 + abs(cmax - c1) / abs(cmax - c2))
309
+ except ZeroDivisionError:
310
+ j = None
311
+ if j is not None and (j <= 0 or j >= 1):
312
+ j = None
313
+ out = []
314
+ for t in i:
315
+ tp = t ** p1
316
+ if j is None:
317
+ c = lin(tp, c1, c2)
318
+ elif tp <= j:
319
+ c = c2 - (c2 - cmax) * tp / j
320
+ else:
321
+ c = cmax - (cmax - c1) * abs((tp - j) / (1 - j))
322
+ out.append(hcl(lin(t, h1, h2), c, lin(t ** p2, l1, l2)))
323
+ return out
324
+
325
+
230
326
  def _viridis(name, n):
231
- from plotly.colors import sample_colorscale
232
- xs = [0.0] if n == 1 else [i / (n - 1) for i in range(n)]
233
- cols = sample_colorscale(_VIRIDIS[name], xs, colortype="rgb")
327
+ """n colors of a viridis-family palette exactly as R's
328
+ hcl.colors(n, name). R analog: grDevices::hcl.colors()"""
329
+ (typ, h1, h2, h3, c1, c2, c3, l1, l2, l3,
330
+ p1, p2, p3, p4, cmax1, cmax2) = _HCL_PARAMS[name]
331
+ if typ == "sequential":
332
+ h2 = h1 if h2 is None else h2
333
+ c2 = 0 if c2 is None else c2
334
+ p2 = p1 if p2 is None else p2
335
+ i = [1.0] if n == 1 else [1 - k / (n - 1) for k in range(n)]
336
+ return _hcl_seq(i, h1, h2, c1, c2, l1, l2, p1, p2, cmax1)
337
+ # divergingx: two sequential arms meeting at the center
338
+ p2 = p1 if p2 is None else p2
339
+ p4 = p2 if p4 is None else p4
340
+ n2 = -(-n // 2)
341
+ i = [0.0] if n == 1 else [1 - k * 2 / (n - 1) for k in range(n2)]
342
+ left = _hcl_seq(i, h1, h1 if h2 is None else h2, c1, c2, l1, l2,
343
+ p1, p2, cmax1)
344
+ right = _hcl_seq(i, h3, h3 if h2 is None else h2, c3, c2, l3, l2,
345
+ p3, p4, cmax2)
346
+ col = left + right[::-1]
347
+ if n // 2 < n2:
348
+ del col[n2 - 1]
349
+ return col
350
+
351
+
352
+ # --- R's own palettes: grDevices HSV and colorspace HCL ------------
353
+
354
+ def _hsv(h, s, v):
355
+ """One HSV color as hex, as grDevices hsv(): R's hsv2rgb() and
356
+ its rounding of each channel, (int)(255 * x + 0.5)."""
357
+ if s == 0:
358
+ r = g = b = v
359
+ else:
360
+ t = 6 * math.fmod(h, 1.0)
361
+ i = math.floor(t)
362
+ f = t - i
363
+ p, q, u = v * (1 - s), v * (1 - s * f), v * (1 - s * (1 - f))
364
+ r, g, b = [(v, u, p), (q, v, p), (p, v, u),
365
+ (p, q, v), (u, p, v), (v, p, q)][int(i) % 6]
366
+ return "#{:02X}{:02X}{:02X}".format(
367
+ *(int(255 * x + 0.5) for x in (r, g, b)))
368
+
369
+
370
+ def _seq_int(a, b, n):
371
+ """seq.int(a, b, length.out=n)"""
372
+ if n == 1:
373
+ return [a]
374
+ by = (b - a) / (n - 1)
375
+ return [a + k * by for k in range(n)]
376
+
377
+
378
+ def _rainbow(n, start=0.0, end=None):
379
+ """grDevices rainbow(): HSV hues from start to end."""
380
+ if end is None:
381
+ end = max(1, n - 1) / n
382
+ hs = _seq_int(start, (1 if start > end else 0) + end, n)
383
+ return [_hsv(math.fmod(h, 1.0), 1, 1) for h in hs]
384
+
385
+
386
+ def _heat(n):
387
+ """grDevices heat.colors(): red to yellow, then the yellows
388
+ lighten for the last quarter."""
389
+ j = n // 4
390
+ i = n - j
391
+ cols = _rainbow(i, 0, 1 / 6)
392
+ if j > 0:
393
+ cols += [_hsv(1 / 6, s, 1)
394
+ for s in _seq_int(1 - 1 / (2 * j), 1 / (2 * j), j)]
395
+ return cols
396
+
397
+
398
+ def _terrain(n):
399
+ """grDevices terrain.colors(): green to yellow to near white."""
400
+ k = n // 2
401
+ h, s, v = (4 / 12, 2 / 12, 0 / 12), (1, 1, 0), (0.65, 0.9, 0.95)
402
+ first = [_hsv(a, b, c) for a, b, c in zip(
403
+ _seq_int(h[0], h[1], k), _seq_int(s[0], s[1], k),
404
+ _seq_int(v[0], v[1], k))] if k > 0 else []
405
+ m = n - k + 1
406
+ second = [_hsv(a, b, c) for a, b, c in zip(
407
+ _seq_int(h[1], h[2], m)[1:], _seq_int(s[1], s[2], m)[1:],
408
+ _seq_int(v[1], v[2], m)[1:])]
409
+ return first + second
410
+
411
+
412
+ def _colorspace_seq(n, h, c, l, power):
413
+ """colorspace sequential_hcl() with explicit pairs: h, c, l,
414
+ power each (first, last). Hue moves linearly, chroma and
415
+ luminance by their powers, as colorspace seqhcl()."""
234
416
  out = []
235
- for c in cols:
236
- r, g, b = (round(float(v)) for v in
237
- c[c.find("(") + 1:c.find(")")].split(","))
238
- out.append("#{:02X}{:02X}{:02X}".format(r, g, b))
417
+ for i in _seq_int(1.0, 0.0, n):
418
+ H = h[1] - (h[1] - h[0]) * i
419
+ C = c[1] - (c[1] - c[0]) * i ** power[0]
420
+ L = l[1] - (l[1] - l[0]) * i ** power[1]
421
+ out.append(hcl(H, C, L))
239
422
  return out
240
423
 
241
424
 
425
+ # colorspace's heat_hcl() and terrain_hcl(): h, c., l, power pairs
426
+ _CS_SEQ = {
427
+ "heat_hcl": ((0, 90), (100, 30), (50, 90), (1 / 5, 1)),
428
+ "terrain_hcl": ((130, 0), (80, 0), (60, 95), (1 / 10, 1)),
429
+ }
430
+
431
+
432
+ def _pair(v):
433
+ return tuple(v) if isinstance(v, (tuple, list)) else None
434
+
435
+
436
+ def _seq_r(name, n, c, l):
437
+ """n colors of one of R's own palettes, as lessR's getColors()
438
+ computes them. "heat_hcl" and "terrain_hcl" follow colorspace's
439
+ own chroma and luminance trajectories, replaced only by a c or l
440
+ given: a single c ramps from c to 0, a single l is constant, as
441
+ colorspace reads them. "rainbow_hcl" is lessR's qualitative c=65
442
+ and l=60 unless given. R analog: getColors(), kind seq.R"""
443
+ if name == "rainbow":
444
+ return _rainbow(n)
445
+ if name == "heat":
446
+ return _heat(n)
447
+ if name == "terrain":
448
+ return _terrain(n)
449
+ if name == "rainbow_hcl": # colorspace qualitative_hcl
450
+ c = 65 if c is None else c
451
+ l = 60 if l is None else l
452
+ hs = _seq_int(0, 360 * (n - 1) / n, n)
453
+ return [hcl(h, c, l) for h in hs]
454
+ h, cc, ll, p = _CS_SEQ[name]
455
+ if c is not None:
456
+ cc = _pair(c) or (c, 0)
457
+ if l is not None:
458
+ ll = _pair(l) or (l, l)
459
+ return _colorspace_seq(n, h, cc, ll, p)
460
+
461
+
242
462
  # the fixed mixed hues for a qualitative palette (n <= 24)
243
463
  _MIX = [240, 60, 120, 0, 275, 180, 30, 90, 210, 330, 150, 300]
244
464
 
@@ -251,7 +471,8 @@ def getColors(pal=None, end_pal=None, n=None, h=0, h2=None, c=None,
251
471
 
252
472
  pal: a palette name ("hues", a sequential name such as "blues",
253
473
  "viridis"/"cividis"/"plasma"/"spectral", "Okabe-Ito", "Tableau",
254
- "distinct") or one or more explicit colors. end_pal: an ending
474
+ "distinct", R's "rainbow"/"heat"/"terrain" and their "_hcl"
475
+ versions) or one or more explicit colors. end_pal: an ending
255
476
  color/name for a divergent (two sequential names) or manual
256
477
  (two colors) gradient. h/h2, c, l, power: HCL parameters.
257
478
  R analog: getColors()"""
@@ -284,6 +505,8 @@ def getColors(pal=None, end_pal=None, n=None, h=0, h2=None, c=None,
284
505
  kind = "divergent" if (end_pal is not None and
285
506
  end_pal[0] in _SEQ_NAMES) \
286
507
  else "sequential"
508
+ elif p0 in _SEQ_R:
509
+ kind = "seq.R"
287
510
  elif p0 == "hues":
288
511
  kind = "qualitative"
289
512
  elif p0 in _VIRIDIS:
@@ -350,6 +573,10 @@ def getColors(pal=None, end_pal=None, n=None, h=0, h2=None, c=None,
350
573
  pw = 0.75 if power is None else power
351
574
  pal = _diverging_hcl(n, h, h2, cc, ll, pw, fixup)
352
575
 
576
+ elif kind == "seq.R":
577
+ pal = _seq_r(pal[0], n, None if c_miss else c,
578
+ None if l_miss else l)
579
+
353
580
  elif kind == "viridis":
354
581
  ttl = "Viridis Style Palette: " + pal[0]
355
582
  pal = _viridis(pal[0], n)