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.
- lessPy/ANOVA.py +5 -5
- lessPy/Chart.py +386 -88
- lessPy/Correlation.py +2 -2
- lessPy/Logit.py +2 -2
- lessPy/Regression.py +6 -4
- lessPy/X.py +279 -42
- lessPy/XY.py +814 -158
- lessPy/__init__.py +12 -2
- lessPy/_themes.py +5007 -0
- lessPy/bc_items_plotly.py +31 -18
- lessPy/bc_plotly.py +23 -7
- lessPy/bubble_plotly.py +17 -2
- lessPy/corCFA.py +3 -3
- lessPy/corProp.py +2 -2
- lessPy/corReflect.py +2 -2
- lessPy/corReorder.py +2 -2
- lessPy/corScree.py +3 -3
- lessPy/data/StockPrice.csv +1401 -1386
- lessPy/details.py +17 -4
- lessPy/dn_plotly.py +65 -45
- lessPy/freq_poly_plotly.py +12 -5
- lessPy/getColors.py +247 -20
- lessPy/hier_plotly.py +36 -9
- lessPy/hs_plotly.py +16 -4
- lessPy/plotly_utils.py +78 -9
- lessPy/plt_mat_plotly.py +74 -2
- lessPy/plt_time.py +10 -3
- lessPy/prob_norm.py +3 -3
- lessPy/prob_tcut.py +3 -3
- lessPy/prob_znorm.py +3 -1
- lessPy/rcolors.py +461 -0
- lessPy/simCImean.py +3 -3
- lessPy/simCLT.py +3 -3
- lessPy/simFlips.py +3 -3
- lessPy/simMeans.py +3 -3
- lessPy/stats_out.py +257 -11
- lessPy/style.py +160 -0
- lessPy/sunflower_plotly.py +140 -0
- lessPy/ttest.py +5 -5
- lessPy/utils.py +43 -6
- lessPy/vbs_plotly.py +16 -5
- lessPy/x_console.py +763 -0
- {lesspython-0.2.0.dist-info → lesspython-0.3.0.dist-info}/METADATA +6 -2
- lesspython-0.3.0.dist-info/RECORD +89 -0
- lesspython-0.2.0.dist-info/RECORD +0 -84
- {lesspython-0.2.0.dist-info → lesspython-0.3.0.dist-info}/WHEEL +0 -0
- {lesspython-0.2.0.dist-info → lesspython-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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"{
|
|
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
|
|
65
|
-
fill =
|
|
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
|
|
95
|
-
fill =
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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=
|
|
221
|
+
hovertext=[pre + t for t in hover(d)],
|
|
213
222
|
showlegend=G > 1,
|
|
214
223
|
))
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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",
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
lessPy/freq_poly_plotly.py
CHANGED
|
@@ -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
|
|
94
|
-
fill =
|
|
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
|
-
|
|
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
|
|
225
|
-
fill =
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
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,
|
|
167
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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"
|
|
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)
|