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.
- lessPy/ANOVA.py +680 -0
- lessPy/Chart.py +1055 -0
- lessPy/Correlation.py +236 -0
- lessPy/Flows.py +116 -0
- lessPy/Logit.py +615 -0
- lessPy/Prop_test.py +267 -0
- lessPy/Regression.py +1491 -0
- lessPy/VariableLabels.py +119 -0
- lessPy/X.py +426 -0
- lessPy/XY.py +2007 -0
- lessPy/__init__.py +60 -0
- lessPy/anova_rmd.py +227 -0
- lessPy/bc_plotly.py +575 -0
- lessPy/bubble_plotly.py +470 -0
- lessPy/corCFA.py +316 -0
- lessPy/corEFA.py +220 -0
- lessPy/corPrint.py +45 -0
- lessPy/corProp.py +73 -0
- lessPy/corRead.py +48 -0
- lessPy/corReflect.py +72 -0
- lessPy/corReorder.py +161 -0
- lessPy/corScree.py +87 -0
- lessPy/data/Anova_1way.csv +25 -0
- lessPy/data/Anova_2way.csv +49 -0
- lessPy/data/Anova_rb.csv +8 -0
- lessPy/data/Anova_rbf.csv +49 -0
- lessPy/data/Anova_sp.csv +57 -0
- lessPy/data/BodyMeas.csv +341 -0
- lessPy/data/Cars93.csv +94 -0
- lessPy/data/Employee.csv +38 -0
- lessPy/data/Employee_lbl.csv +9 -0
- lessPy/data/FreqTable99.csv +5 -0
- lessPy/data/Jackets.csv +1026 -0
- lessPy/data/Learn.csv +35 -0
- lessPy/data/Mach4.csv +352 -0
- lessPy/data/Mach4_lbl.csv +21 -0
- lessPy/data/Reading.csv +101 -0
- lessPy/data/StockPrice.csv +1489 -0
- lessPy/data/WeightLoss.csv +11 -0
- lessPy/datasets.py +46 -0
- lessPy/date_infer.py +112 -0
- lessPy/details.py +314 -0
- lessPy/dn_plotly.py +495 -0
- lessPy/dot_plotly.py +385 -0
- lessPy/freq_poly_plotly.py +324 -0
- lessPy/getColors.py +399 -0
- lessPy/hier_plotly.py +352 -0
- lessPy/hs_plotly.py +395 -0
- lessPy/logit_rmd.py +410 -0
- lessPy/order_by.py +94 -0
- lessPy/pie_plotly.py +292 -0
- lessPy/pivot.py +158 -0
- lessPy/plotly_utils.py +787 -0
- lessPy/plt_add.py +129 -0
- lessPy/plt_contour.py +192 -0
- lessPy/plt_contour_facet.py +194 -0
- lessPy/plt_forecast.py +677 -0
- lessPy/plt_mat_plotly.py +201 -0
- lessPy/plt_plotly.py +216 -0
- lessPy/plt_smooth.py +170 -0
- lessPy/plt_time.py +143 -0
- lessPy/prob_norm.py +111 -0
- lessPy/prob_tcut.py +131 -0
- lessPy/prob_znorm.py +110 -0
- lessPy/radar_plotly.py +201 -0
- lessPy/reg_rmd.py +754 -0
- lessPy/rename.py +33 -0
- lessPy/reshape.py +95 -0
- lessPy/showColors.py +130 -0
- lessPy/simCImean.py +165 -0
- lessPy/simCLT.py +265 -0
- lessPy/simFlips.py +104 -0
- lessPy/simMeans.py +146 -0
- lessPy/stats_out.py +189 -0
- lessPy/ttest.py +641 -0
- lessPy/utils.py +235 -0
- lessPy/vbs_plotly.py +545 -0
- lesspython-0.1.0.dist-info/METADATA +93 -0
- lesspython-0.1.0.dist-info/RECORD +82 -0
- lesspython-0.1.0.dist-info/WHEEL +5 -0
- lesspython-0.1.0.dist-info/licenses/LICENSE +338 -0
- lesspython-0.1.0.dist-info/top_level.txt +1 -0
lessPy/plotly_utils.py
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
# plotly_utils.py — analog of zzz_plotly.R
|
|
2
|
+
#
|
|
3
|
+
# Only the helpers needed by the render functions ported so far.
|
|
4
|
+
# Function names drop the leading "." of their R counterparts.
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from .utils import get_option
|
|
12
|
+
|
|
13
|
+
# R analog: .plotly_base_colors()
|
|
14
|
+
BASE_COLORS = [
|
|
15
|
+
"#4398D0", "#B28B2A", "#5FA140", "#D57388",
|
|
16
|
+
"#9A84D6", "#00A898", "#C97E5B", "#909711",
|
|
17
|
+
"#00A3BA", "#D26FAF", "#00A76F", "#BD76CB",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# minimal named-color table; R's col2rgb() knows all R names, but
|
|
21
|
+
# plotly accepts CSS names directly so unknowns pass through as-is
|
|
22
|
+
_NAMED = {
|
|
23
|
+
"black": "#000000",
|
|
24
|
+
"white": "#FFFFFF",
|
|
25
|
+
"red": "#FF0000",
|
|
26
|
+
"blue": "#0000FF",
|
|
27
|
+
"green": "#00FF00",
|
|
28
|
+
"gray": "#BEBEBE",
|
|
29
|
+
"grey": "#BEBEBE",
|
|
30
|
+
"darkred": "#8B0000",
|
|
31
|
+
"darkblue": "#00008B",
|
|
32
|
+
"steelblue": "#4682B4",
|
|
33
|
+
"transparent": "#FFFFFF00",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _gray_level(name):
|
|
38
|
+
m = re.fullmatch(r"gr[ae]y(\d{1,3})", name)
|
|
39
|
+
if m is None:
|
|
40
|
+
return None
|
|
41
|
+
lvl = int(m.group(1))
|
|
42
|
+
if lvl > 100:
|
|
43
|
+
return None
|
|
44
|
+
v = round(255 * lvl / 100)
|
|
45
|
+
return f"#{v:02X}{v:02X}{v:02X}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def to_hex(col):
|
|
49
|
+
"""Convert a color or list of colors to hex. R analog: .to_hex()"""
|
|
50
|
+
if col is None:
|
|
51
|
+
return None
|
|
52
|
+
if isinstance(col, (list, tuple)):
|
|
53
|
+
return [to_hex(c) for c in col]
|
|
54
|
+
|
|
55
|
+
c1 = str(col).strip()
|
|
56
|
+
if c1.lower() == "off":
|
|
57
|
+
c1 = "transparent"
|
|
58
|
+
|
|
59
|
+
# rgb()/rgba(): retain alpha if present
|
|
60
|
+
m = re.fullmatch(r"rgba?\(([^)]+)\)", c1)
|
|
61
|
+
if m:
|
|
62
|
+
parts = [float(p) for p in m.group(1).split(",")]
|
|
63
|
+
r, g, b = (int(round(max(0, min(255, p)))) for p in parts[:3])
|
|
64
|
+
if len(parts) == 4:
|
|
65
|
+
a = int(round(max(0, min(1, parts[3])) * 255))
|
|
66
|
+
return f"#{r:02X}{g:02X}{b:02X}{a:02X}"
|
|
67
|
+
return f"#{r:02X}{g:02X}{b:02X}"
|
|
68
|
+
|
|
69
|
+
if re.fullmatch(r"#[0-9A-Fa-f]{6}", c1):
|
|
70
|
+
return c1
|
|
71
|
+
if re.fullmatch(r"#[0-9A-Fa-f]{3}", c1):
|
|
72
|
+
r, g, b = c1[1], c1[2], c1[3]
|
|
73
|
+
return f"#{r}{r}{g}{g}{b}{b}"
|
|
74
|
+
if re.fullmatch(r"#[0-9A-Fa-f]{8}", c1):
|
|
75
|
+
return c1
|
|
76
|
+
|
|
77
|
+
lower = c1.lower()
|
|
78
|
+
if lower in _NAMED:
|
|
79
|
+
return _NAMED[lower]
|
|
80
|
+
gray = _gray_level(lower)
|
|
81
|
+
if gray is not None:
|
|
82
|
+
return gray
|
|
83
|
+
return c1 # CSS name unknown to us; plotly handles it
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def as_plotly_color(col):
|
|
87
|
+
"""plotly.py rejects #RRGGBBAA hex; rewrite it as rgba().
|
|
88
|
+
R analog: .as_plotly_rgba()"""
|
|
89
|
+
if col is None:
|
|
90
|
+
return None
|
|
91
|
+
if isinstance(col, (list, tuple)):
|
|
92
|
+
return [as_plotly_color(c) for c in col]
|
|
93
|
+
h = to_hex(col)
|
|
94
|
+
if isinstance(h, str) and re.fullmatch(r"#[0-9A-Fa-f]{8}", h):
|
|
95
|
+
r, g, b = int(h[1:3], 16), int(h[3:5], 16), int(h[5:7], 16)
|
|
96
|
+
a = int(h[7:9], 16) / 255
|
|
97
|
+
return f"rgba({r},{g},{b},{a:.3f})"
|
|
98
|
+
return h
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def auto_opacity(k, mode="overlay"):
|
|
102
|
+
"""Fill opacity by number of overlapping series.
|
|
103
|
+
R analog: .auto_opacity()"""
|
|
104
|
+
if mode not in ("overlay", "fill", "lines", "stack"):
|
|
105
|
+
raise ValueError("bad auto_opacity mode")
|
|
106
|
+
k = max(1, int(k))
|
|
107
|
+
base = 1.00 if k == 1 else 0.65 - 0.058 * k
|
|
108
|
+
lo, hi = {"overlay": (0.28, 0.80), "fill": (0.20, 0.65),
|
|
109
|
+
"lines": (0.40, 0.90), "stack": (1.00, 1.00)}[mode]
|
|
110
|
+
return max(lo, min(hi, base))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def make_trans(col, alpha):
|
|
114
|
+
"""Color(s) + alpha -> rgba() string(s).
|
|
115
|
+
R analog: .maketrans_plotly()"""
|
|
116
|
+
if isinstance(col, (list, tuple)):
|
|
117
|
+
return [make_trans(c, alpha) for c in col]
|
|
118
|
+
a = max(0.0, min(1.0, float(alpha)))
|
|
119
|
+
s = "" if col is None else str(col).strip().lower()
|
|
120
|
+
if s in ("", "na", "transparent", "off"):
|
|
121
|
+
return "rgba(0,0,0,0)"
|
|
122
|
+
h = to_hex(col)
|
|
123
|
+
if not str(h).startswith("#"):
|
|
124
|
+
return h # unknown CSS name; pass through opaque
|
|
125
|
+
r, g, b = hex_to_rgb3(h)
|
|
126
|
+
return f"rgba({r},{g},{b},{a:.3f})"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def auto_text_color(cols, bg="white", threshold=0.5):
|
|
130
|
+
"""Black or white text per fill, from relative luminance; rgba
|
|
131
|
+
fills fall back to bg when fully transparent.
|
|
132
|
+
R analog: .auto_text_color() in zzz.R"""
|
|
133
|
+
def one(col):
|
|
134
|
+
s = str(col)
|
|
135
|
+
m = re.fullmatch(r"rgba?\(([^)]+)\)", s)
|
|
136
|
+
if m:
|
|
137
|
+
parts = [float(p) for p in m.group(1).split(",")]
|
|
138
|
+
if len(parts) == 4 and parts[3] == 0:
|
|
139
|
+
s = bg # fully transparent: use background
|
|
140
|
+
m = None
|
|
141
|
+
else:
|
|
142
|
+
r, g, b = parts[:3]
|
|
143
|
+
if not m:
|
|
144
|
+
h = to_hex(s)
|
|
145
|
+
if not str(h).startswith("#"):
|
|
146
|
+
h = to_hex(bg)
|
|
147
|
+
r, g, b = hex_to_rgb3(h)
|
|
148
|
+
lin = (_srgb_to_lin(r), _srgb_to_lin(g), _srgb_to_lin(b))
|
|
149
|
+
lum = 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]
|
|
150
|
+
return "white" if lum < threshold else "black"
|
|
151
|
+
if isinstance(cols, (list, tuple)):
|
|
152
|
+
return [one(c) for c in cols]
|
|
153
|
+
return one(cols)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def build_title(x_name, by_name=None, y_name=None, stat=None,
|
|
157
|
+
facet_name=None):
|
|
158
|
+
"""Auto-build a chart title from the variable names.
|
|
159
|
+
R analog: .plotly_build_title()"""
|
|
160
|
+
cap = {"mean": "Mean", "sum": "Sum", "median": "Median",
|
|
161
|
+
"min": "Min", "max": "Max", "sd": "SD",
|
|
162
|
+
"deviation": "Mean Deviation"}
|
|
163
|
+
if y_name == "Count":
|
|
164
|
+
y_name = None
|
|
165
|
+
if y_name is None and stat is None:
|
|
166
|
+
ttl = f"Count of {x_name}"
|
|
167
|
+
elif stat is None:
|
|
168
|
+
ttl = f"{y_name} by {x_name}"
|
|
169
|
+
else:
|
|
170
|
+
ttl = f"{cap.get(stat, stat.upper())} of {y_name} " \
|
|
171
|
+
f"by {x_name}"
|
|
172
|
+
if by_name:
|
|
173
|
+
ttl += f" by {by_name}"
|
|
174
|
+
if facet_name:
|
|
175
|
+
ttl += f" across {facet_name}"
|
|
176
|
+
return ttl
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# where a named position puts the legend: (x, y, xanchor, yanchor).
|
|
180
|
+
# "right_margin" is the default, outside the panel; the rest are the
|
|
181
|
+
# base-R legend() keywords, placed inside the panel as they are there.
|
|
182
|
+
LEGEND_POSITIONS = {
|
|
183
|
+
"right_margin": (1.02, 0.5, "left", "middle"),
|
|
184
|
+
"topleft": (0.02, 0.98, "left", "top"),
|
|
185
|
+
"top": (0.50, 0.98, "center", "top"),
|
|
186
|
+
"topright": (0.98, 0.98, "right", "top"),
|
|
187
|
+
"left": (0.02, 0.50, "left", "middle"),
|
|
188
|
+
"center": (0.50, 0.50, "center", "middle"),
|
|
189
|
+
"right": (0.98, 0.50, "right", "middle"),
|
|
190
|
+
"bottomleft": (0.02, 0.02, "left", "bottom"),
|
|
191
|
+
"bottom": (0.50, 0.02, "center", "bottom"),
|
|
192
|
+
"bottomright": (0.98, 0.02, "right", "bottom"),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def abbrev(txt, n):
|
|
197
|
+
"""Truncate to at most n characters. R analog: legend_abbrev"""
|
|
198
|
+
if n is None or txt is None:
|
|
199
|
+
return txt
|
|
200
|
+
n = int(n)
|
|
201
|
+
return txt if len(txt) <= n else txt[:n]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def legend_style(by_name, style_opts, title=None, position=None,
|
|
205
|
+
horiz=False, size=None, adjust=0, abbrev_n=None):
|
|
206
|
+
"""Legend styling for a by= grouping, shared by the histogram,
|
|
207
|
+
density, and bar renderers (the VBS composite draws its own
|
|
208
|
+
horizontal lattice-style key instead).
|
|
209
|
+
|
|
210
|
+
title overrides the by-variable name, position names a slot in
|
|
211
|
+
LEGEND_POSITIONS, horiz lays the keys out in a row, size is a
|
|
212
|
+
character expansion factor on the text, adjust shifts the legend
|
|
213
|
+
horizontally (paper units), and abbrev_n truncates the title and
|
|
214
|
+
the key labels. R analog: the legend_* parameters of Chart()."""
|
|
215
|
+
if position is not None and position not in LEGEND_POSITIONS:
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"legend_position must be one of "
|
|
218
|
+
f"{', '.join(LEGEND_POSITIONS)}")
|
|
219
|
+
|
|
220
|
+
mult = 1.0 if size is None else float(size)
|
|
221
|
+
text = by_name or "" if title is None else title
|
|
222
|
+
leg = dict(
|
|
223
|
+
title=dict(text=abbrev(text, abbrev_n) or "",
|
|
224
|
+
font=dict(size=round(
|
|
225
|
+
15 * get_option("lab_size", 1) * mult))),
|
|
226
|
+
font=dict(size=round(
|
|
227
|
+
16 * get_option("axis_size", 0.9) * mult)),
|
|
228
|
+
bgcolor=to_hex(style_opts["window_fill"]),
|
|
229
|
+
bordercolor=to_hex(style_opts["legend_border"]),
|
|
230
|
+
borderwidth=1,
|
|
231
|
+
)
|
|
232
|
+
# placement stays plotly's own unless asked for, so the callers
|
|
233
|
+
# that only style the legend keep the layout they already had
|
|
234
|
+
if horiz:
|
|
235
|
+
leg["orientation"] = "h"
|
|
236
|
+
# "right_margin" is plotly's own outside-right placement, so
|
|
237
|
+
# leave the layout alone there unless the legend is nudged
|
|
238
|
+
if (position not in (None, "right_margin")) or adjust:
|
|
239
|
+
x, y, xanchor, yanchor = LEGEND_POSITIONS[
|
|
240
|
+
position or "right_margin"]
|
|
241
|
+
leg.update(x=x + float(adjust or 0), xanchor=xanchor,
|
|
242
|
+
y=y, yanchor=yanchor)
|
|
243
|
+
return leg
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def hex_to_rgb3(h):
|
|
247
|
+
"""R analog: .hex_to_rgb3()"""
|
|
248
|
+
h = h.replace("#", "").upper()
|
|
249
|
+
if len(h) in (3, 4):
|
|
250
|
+
h = h[0] * 2 + h[1] * 2 + h[2] * 2
|
|
251
|
+
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _srgb_to_lin(c):
|
|
255
|
+
c = c / 255
|
|
256
|
+
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def contrast_text_for_hex(hex_col):
|
|
260
|
+
"""White or black text, whichever contrasts more with the fill.
|
|
261
|
+
R analog: .contrast_text_for_hex()"""
|
|
262
|
+
hex_col = to_hex(hex_col)
|
|
263
|
+
if not str(hex_col).startswith("#"):
|
|
264
|
+
return "#000000"
|
|
265
|
+
r, g, b = hex_to_rgb3(hex_col)
|
|
266
|
+
lin = (_srgb_to_lin(r), _srgb_to_lin(g), _srgb_to_lin(b))
|
|
267
|
+
lum = 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]
|
|
268
|
+
white = 1.05 / (lum + 0.05)
|
|
269
|
+
black = (lum + 0.05) / 0.05
|
|
270
|
+
return "#FFFFFF" if white > black else "#000000"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def is_integer_valued(v):
|
|
274
|
+
"""True if every non-missing value is a whole number."""
|
|
275
|
+
vals = [x for x in v if x == x] # drop NaN
|
|
276
|
+
return all(float(x).is_integer() for x in vals)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def get_tick_fmt(v, digits_d=2):
|
|
280
|
+
"""d3-format string for hover/tick values. R analog: .get.tick.fmt()"""
|
|
281
|
+
if digits_d is None:
|
|
282
|
+
digits_d = 2
|
|
283
|
+
return "" if is_integer_valued(v) else f".{digits_d}f"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def axis_format(vals, digits_d=2, axis_fmt="K", prefix=""):
|
|
287
|
+
"""Tick labels under the lessR axis_fmt policies. "K" (the
|
|
288
|
+
default): billions go scientific; multiples of 100 all past
|
|
289
|
+
1000 render as thousands ("60K"); otherwise a comma
|
|
290
|
+
separator past 9999. ",": comma separator. ".": European
|
|
291
|
+
(period separator). "": plain. prefix prepends (e.g. "$").
|
|
292
|
+
R analog: .axis.format()"""
|
|
293
|
+
v = np.asarray(list(vals), dtype=float)
|
|
294
|
+
if len(v) == 0:
|
|
295
|
+
return []
|
|
296
|
+
fmtd = get_tick_fmt(v, digits_d)
|
|
297
|
+
base = [f"{u:.{digits_d}f}" if fmtd else f"{u:g}"
|
|
298
|
+
for u in v]
|
|
299
|
+
nz = np.abs(v)[np.abs(v) > 0]
|
|
300
|
+
if axis_fmt == "K" and not fmtd:
|
|
301
|
+
if np.any(np.abs(v) >= 1e9):
|
|
302
|
+
base = [f"{u:g}" for u in v]
|
|
303
|
+
elif (np.all(np.abs(v / 100 - np.round(v / 100))
|
|
304
|
+
< 1e-10)
|
|
305
|
+
and len(nz) > 0 and np.all(nz > 1000)):
|
|
306
|
+
base = [f"{u / 1000:g}K" for u in v]
|
|
307
|
+
elif np.any(v > 9999):
|
|
308
|
+
base = [f"{u:,.0f}" for u in v]
|
|
309
|
+
elif axis_fmt == ",":
|
|
310
|
+
base = [f"{u:,.0f}" for u in v]
|
|
311
|
+
elif axis_fmt == ".":
|
|
312
|
+
base = [f"{u:,.0f}".replace(",", ".") for u in v]
|
|
313
|
+
if prefix:
|
|
314
|
+
base = [prefix + b for b in base]
|
|
315
|
+
return base
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def grid_style():
|
|
319
|
+
"""R analog: .grid_style()"""
|
|
320
|
+
return {
|
|
321
|
+
"color": to_hex(get_option("grid_col", "gray85")),
|
|
322
|
+
"width": get_option("grid_lwd", 0.5),
|
|
323
|
+
"dash": get_option("grid_lty", None),
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _grid_lines(vals, vertical):
|
|
328
|
+
if vals is None or len(vals) == 0:
|
|
329
|
+
return []
|
|
330
|
+
st = grid_style()
|
|
331
|
+
line = {"color": st["color"], "width": st["width"]}
|
|
332
|
+
if st["dash"] is not None:
|
|
333
|
+
line["dash"] = st["dash"]
|
|
334
|
+
shapes = []
|
|
335
|
+
for v in vals:
|
|
336
|
+
if vertical:
|
|
337
|
+
pos = {"xref": "x", "yref": "paper",
|
|
338
|
+
"x0": v, "x1": v, "y0": 0, "y1": 1}
|
|
339
|
+
else:
|
|
340
|
+
pos = {"xref": "paper", "yref": "y",
|
|
341
|
+
"x0": 0, "x1": 1, "y0": v, "y1": v}
|
|
342
|
+
shapes.append({"type": "line", "layer": "below",
|
|
343
|
+
"line": dict(line), **pos})
|
|
344
|
+
return shapes
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def x_grid(x_vals):
|
|
348
|
+
"""Vertical grid lines at given x values. R analog: x_grid()"""
|
|
349
|
+
return _grid_lines(x_vals, vertical=True)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def y_grid(y_vals):
|
|
353
|
+
"""Horizontal grid lines at given y values. R analog: y_grid()"""
|
|
354
|
+
return _grid_lines(y_vals, vertical=False)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def axis_base():
|
|
358
|
+
"""R analog: axis_base()"""
|
|
359
|
+
return {
|
|
360
|
+
"zeroline": False,
|
|
361
|
+
"showline": True,
|
|
362
|
+
"linecolor": to_hex(get_option("axis_color", "black")),
|
|
363
|
+
"linewidth": get_option("axis_lwd", 1),
|
|
364
|
+
"ticks": "outside",
|
|
365
|
+
"ticklen": 4,
|
|
366
|
+
"automargin": True,
|
|
367
|
+
"tickfont": {
|
|
368
|
+
"color": to_hex(get_option("axis_color", "black")),
|
|
369
|
+
"size": 16 * get_option("axis_size", 0.9),
|
|
370
|
+
},
|
|
371
|
+
"title": {
|
|
372
|
+
"font": {
|
|
373
|
+
"color": to_hex(get_option("lab_color", "black")),
|
|
374
|
+
"size": 15 * get_option("lab_size", 1),
|
|
375
|
+
},
|
|
376
|
+
"standoff": 10,
|
|
377
|
+
},
|
|
378
|
+
"gridcolor": to_hex(get_option("grid_color", "gray90")),
|
|
379
|
+
"gridwidth": get_option("grid_lwd", 1),
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def axis_num(title_txt, tickvals, ticktext):
|
|
384
|
+
"""Numeric axis with explicit ticks. R analog: axis_num()"""
|
|
385
|
+
ax = axis_base()
|
|
386
|
+
ax["title"]["text"] = title_txt
|
|
387
|
+
ax.update(tickmode="array", tickvals=tickvals,
|
|
388
|
+
ticktext=ticktext, showgrid=False)
|
|
389
|
+
return ax
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def axis_cat(title_txt):
|
|
393
|
+
"""Categorical axis, grid off. R analog: axis_cat()"""
|
|
394
|
+
ax = axis_base()
|
|
395
|
+
ax["title"]["text"] = title_txt
|
|
396
|
+
ax["showgrid"] = False
|
|
397
|
+
return ax
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def apply_font_size(fig, mult):
|
|
401
|
+
"""Scale every text size in the figure by mult -- the per-call
|
|
402
|
+
font_size. Covers the global font, the title, each (sub)plot's
|
|
403
|
+
axis title and tick font, the legend, annotations, and trace
|
|
404
|
+
text. Text without an explicit size inherits the global font,
|
|
405
|
+
so scaling layout.font carries it. R analog: font_size."""
|
|
406
|
+
if not mult or mult == 1:
|
|
407
|
+
return fig
|
|
408
|
+
|
|
409
|
+
def scale(obj, attr, default=None):
|
|
410
|
+
if obj is None:
|
|
411
|
+
return
|
|
412
|
+
try:
|
|
413
|
+
cur = getattr(obj, attr)
|
|
414
|
+
except Exception:
|
|
415
|
+
return
|
|
416
|
+
try:
|
|
417
|
+
if cur is not None:
|
|
418
|
+
setattr(obj, attr, max(1, round(cur * mult)))
|
|
419
|
+
elif default is not None:
|
|
420
|
+
setattr(obj, attr, max(1, round(default * mult)))
|
|
421
|
+
except Exception:
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
lay = fig.layout
|
|
425
|
+
scale(lay.font, "size", 12) # inherited default
|
|
426
|
+
scale(lay.title.font, "size")
|
|
427
|
+
scale(lay.legend.font, "size")
|
|
428
|
+
for ax in list(fig.select_xaxes()) + list(fig.select_yaxes()):
|
|
429
|
+
scale(ax.title.font, "size")
|
|
430
|
+
scale(ax.tickfont, "size")
|
|
431
|
+
for ann in lay.annotations:
|
|
432
|
+
scale(ann.font, "size")
|
|
433
|
+
for tr in fig.data:
|
|
434
|
+
for a in ("textfont", "insidetextfont", "outsidetextfont"):
|
|
435
|
+
scale(getattr(tr, a, None), "size")
|
|
436
|
+
return fig
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def font_scaled(func):
|
|
440
|
+
"""Wrap a plot function so a font_size= keyword scales the text
|
|
441
|
+
of the returned figure (font_size=1 is a no-op)."""
|
|
442
|
+
import functools
|
|
443
|
+
|
|
444
|
+
@functools.wraps(func)
|
|
445
|
+
def wrapper(*args, font_size=1, **kwargs):
|
|
446
|
+
fig = func(*args, **kwargs)
|
|
447
|
+
try:
|
|
448
|
+
if font_size and font_size != 1 and hasattr(fig, "layout"):
|
|
449
|
+
apply_font_size(fig, font_size)
|
|
450
|
+
except Exception:
|
|
451
|
+
pass
|
|
452
|
+
return fig
|
|
453
|
+
|
|
454
|
+
return wrapper
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def sym_at(shape, i):
|
|
458
|
+
"""Per-group marker symbol. A scalar shape is used for every
|
|
459
|
+
group (by= differentiates by color only, the default); a list/
|
|
460
|
+
tuple gives one symbol per group, recycled if short."""
|
|
461
|
+
if isinstance(shape, (list, tuple)):
|
|
462
|
+
return shape[i % len(shape)]
|
|
463
|
+
return shape
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def square_layout(main=False, side=460):
|
|
467
|
+
"""Figure width/height/margin giving a square plot area
|
|
468
|
+
(side x side px), matching R's square default device. Setting
|
|
469
|
+
width = side + left + right and height = side + top + bottom
|
|
470
|
+
makes the plotting box square regardless of tick-label width."""
|
|
471
|
+
left, right, bottom = 78, 28, 56
|
|
472
|
+
top = 60 if main else 30
|
|
473
|
+
return dict(width=side + left + right,
|
|
474
|
+
height=side + top + bottom,
|
|
475
|
+
margin=dict(l=left, r=right, t=top, b=bottom))
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def facet_fig(n_rows, n_cols=1):
|
|
479
|
+
"""Facet panel grid on shared axes; row n_rows is the BOTTOM
|
|
480
|
+
row, where the first facet levels draw (the lattice as.table
|
|
481
|
+
default, as in the faceted VBS)."""
|
|
482
|
+
from plotly.subplots import make_subplots
|
|
483
|
+
# plotly caps spacing at 1/(n-1); keep a 10% margin under it so
|
|
484
|
+
# many-panel grids (e.g. 25 facet levels) still lay out
|
|
485
|
+
v_space = max(0.055, min(0.16, 0.32 / n_rows))
|
|
486
|
+
if n_rows > 1:
|
|
487
|
+
v_space = min(v_space, 0.9 / (n_rows - 1))
|
|
488
|
+
h_space = 0.06 if n_cols > 1 else 0.2
|
|
489
|
+
if n_cols > 1:
|
|
490
|
+
h_space = min(h_space, 0.9 / (n_cols - 1))
|
|
491
|
+
return make_subplots(
|
|
492
|
+
rows=n_rows, cols=n_cols, shared_xaxes=True,
|
|
493
|
+
shared_yaxes=n_cols > 1,
|
|
494
|
+
vertical_spacing=v_space, horizontal_spacing=h_space)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def facet_pos(i, n_lvl, n_col):
|
|
498
|
+
"""Subplot (row, col) of facet level i: the lattice fill
|
|
499
|
+
order — bottom row first, left to right, then upward, so the
|
|
500
|
+
bottom row is always full and carries the x ticks."""
|
|
501
|
+
n_row = math.ceil(n_lvl / max(1, n_col))
|
|
502
|
+
return n_row - i // n_col, i % n_col + 1
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def facet_cell_label(name, level):
|
|
506
|
+
"""Panel-strip label Var = "level" (quote non-numeric levels
|
|
507
|
+
only, so an integer level reads as Year = 2020, not
|
|
508
|
+
Year = "2020"). R analog: .lab.lv() in .plt.dist.facet /
|
|
509
|
+
.plt.contour.facet"""
|
|
510
|
+
try:
|
|
511
|
+
float(level)
|
|
512
|
+
q = str(level)
|
|
513
|
+
except (TypeError, ValueError):
|
|
514
|
+
q = f'"{level}"'
|
|
515
|
+
return f"{name} = {q}"
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def facet_cells2(f1_order, f2_order, f1_name, f2_name):
|
|
519
|
+
"""Cell order for the two-facet grid: panel rows = facet2
|
|
520
|
+
levels (first level on the top row), panel columns = facet1
|
|
521
|
+
levels, filled row-major — the layout of R's faceted
|
|
522
|
+
dist/contour renderers and the lattice facet1 * facet2
|
|
523
|
+
conditioning. Returns (labels, pos, n_row, n_col) for
|
|
524
|
+
finish_facet(pos=...)."""
|
|
525
|
+
labels, pos = [], []
|
|
526
|
+
for i2, l2 in enumerate(f2_order):
|
|
527
|
+
for i1, l1 in enumerate(f1_order):
|
|
528
|
+
labels.append(facet_cell_label(f1_name, l1) + ", "
|
|
529
|
+
+ facet_cell_label(f2_name, l2))
|
|
530
|
+
pos.append((i2 + 1, i1 + 1))
|
|
531
|
+
return labels, pos, len(f2_order), len(f1_order)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def facet_panels(facet, facet_order, facet2=None,
|
|
535
|
+
facet2_order=None, facet_name=None,
|
|
536
|
+
facet2_name=None, n_col=1):
|
|
537
|
+
"""Panel plan shared by the faceted renderers. One facet:
|
|
538
|
+
the established bottom-up fill (facet_pos) with plain level
|
|
539
|
+
strip labels. Two facets: the facet_cells2 top-down grid
|
|
540
|
+
(rows = facet2, cols = facet1) with Var = "level" cell
|
|
541
|
+
labels; n_col is then fixed by the facet1 levels. Returns
|
|
542
|
+
(labels, pos, sel, n_row, n_col): sel[i] is the boolean
|
|
543
|
+
row mask of panel i."""
|
|
544
|
+
import numpy as np
|
|
545
|
+
f1 = np.asarray(facet)
|
|
546
|
+
if facet2 is None:
|
|
547
|
+
n = len(facet_order)
|
|
548
|
+
labels = [str(lv) for lv in facet_order]
|
|
549
|
+
pos = [facet_pos(i, n, n_col) for i in range(n)]
|
|
550
|
+
sel = [f1 == lv for lv in facet_order]
|
|
551
|
+
return labels, pos, sel, math.ceil(n / max(1, n_col)), \
|
|
552
|
+
max(1, n_col)
|
|
553
|
+
labels, pos, n_row, n_c = facet_cells2(
|
|
554
|
+
facet_order, facet2_order, facet_name, facet2_name)
|
|
555
|
+
f2 = np.asarray(facet2)
|
|
556
|
+
sel = [(f1 == l1) & (f2 == l2)
|
|
557
|
+
for l2 in facet2_order for l1 in facet_order]
|
|
558
|
+
return labels, pos, sel, n_row, n_c
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def finish_facet(fig, levels, ax, x_lab, y_lab, gridT1,
|
|
562
|
+
style_opts=None, y_cat=None, n_col=1, pos=None,
|
|
563
|
+
height=None, width=None):
|
|
564
|
+
"""Facet styling shared by the faceted renderers: per-panel y
|
|
565
|
+
axes on a common scale (y label on the middle panel of the
|
|
566
|
+
first column), x ticks on the bottom row only, vertical grid
|
|
567
|
+
and a frame per panel, and a shaded strip label above each
|
|
568
|
+
panel. levels fill bottom-up, left to right (facet_pos):
|
|
569
|
+
levels[0] draws bottom-left. n_col > 1 lays the panels out
|
|
570
|
+
as a grid (the lattice n_col). y_cat: category labels for
|
|
571
|
+
panels whose y axis is categorical (the faceted bar chart);
|
|
572
|
+
ax then needs only axT1/axL1. pos: explicit (row, col) per
|
|
573
|
+
level — the two-facet grid (facet_cells2), which fills
|
|
574
|
+
top-down."""
|
|
575
|
+
if style_opts is None:
|
|
576
|
+
style_opts = plotly_style()
|
|
577
|
+
n = len(levels)
|
|
578
|
+
if pos is not None:
|
|
579
|
+
n_row = max(r for r, _ in pos)
|
|
580
|
+
n_col = max(c for _, c in pos)
|
|
581
|
+
else:
|
|
582
|
+
n_row = math.ceil(n / max(1, n_col))
|
|
583
|
+
mid_r = (n_row + 1) // 2
|
|
584
|
+
mid_c = (n_col + 1) // 2
|
|
585
|
+
gs = grid_style()
|
|
586
|
+
shapes = list(fig.layout.shapes or [])
|
|
587
|
+
anns = list(fig.layout.annotations or [])
|
|
588
|
+
s_fill = as_plotly_color(get_option("strip_fill",
|
|
589
|
+
"#7F7F7F37"))
|
|
590
|
+
s_line = to_hex(get_option("strip_color", "gray40"))
|
|
591
|
+
s_text = to_hex(get_option("strip_text_color", "gray15"))
|
|
592
|
+
strip_h = max(0.10, min(0.30, 0.05 * n_row))
|
|
593
|
+
# strip font: shrink so the longest label fits its panel
|
|
594
|
+
# (the two-facet Var = "level" cell labels run long)
|
|
595
|
+
s_size = round(14 * get_option("axis_size", 0.9))
|
|
596
|
+
panel_px = (700 - 90) / max(1, n_col)
|
|
597
|
+
max_len = max(len(str(lv)) for lv in levels)
|
|
598
|
+
if max_len > 0:
|
|
599
|
+
s_size = max(8, min(s_size,
|
|
600
|
+
int(panel_px / (0.58 * max_len))))
|
|
601
|
+
|
|
602
|
+
for i, lvl in enumerate(levels):
|
|
603
|
+
r, c = pos[i] if pos is not None else facet_pos(i, n,
|
|
604
|
+
n_col)
|
|
605
|
+
k_ax = (r - 1) * n_col + c
|
|
606
|
+
suf = "" if k_ax == 1 else str(k_ax)
|
|
607
|
+
show_ylab = y_lab if (c == 1 and r == mid_r) else ""
|
|
608
|
+
if y_cat is None:
|
|
609
|
+
ay = axis_num(show_ylab, ax["axT2"], ax["axL2"])
|
|
610
|
+
ay.update(showgrid=True,
|
|
611
|
+
gridcolor=to_hex(style_opts["grid_col"]),
|
|
612
|
+
gridwidth=1, griddash="dot",
|
|
613
|
+
showticklabels=c == 1,
|
|
614
|
+
range=[0, float(ax["axT2"][-1]) * 1.04])
|
|
615
|
+
else:
|
|
616
|
+
ay = axis_cat(show_ylab)
|
|
617
|
+
ay.update(categoryorder="array",
|
|
618
|
+
categoryarray=list(y_cat),
|
|
619
|
+
showticklabels=c == 1)
|
|
620
|
+
fig.update_yaxes(row=r, col=c, **ay)
|
|
621
|
+
if r == n_row:
|
|
622
|
+
fig.update_xaxes(
|
|
623
|
+
row=r, col=c,
|
|
624
|
+
**axis_num(x_lab if c == mid_c else "",
|
|
625
|
+
ax["axT1"], ax["axL1"]))
|
|
626
|
+
else:
|
|
627
|
+
fig.update_xaxes(
|
|
628
|
+
row=r, col=c, showline=True, ticks="",
|
|
629
|
+
linecolor=to_hex(get_option("axis_color",
|
|
630
|
+
"black")))
|
|
631
|
+
for v in (gridT1 or []): # vertical panel grid
|
|
632
|
+
shapes.append({
|
|
633
|
+
"type": "line", "layer": "below",
|
|
634
|
+
"xref": f"x{suf}", "yref": f"y{suf} domain",
|
|
635
|
+
"x0": v, "x1": v, "y0": 0, "y1": 1,
|
|
636
|
+
"line": {"color": gs["color"],
|
|
637
|
+
"width": gs["width"]}})
|
|
638
|
+
shapes.append({ # panel frame
|
|
639
|
+
"type": "rect",
|
|
640
|
+
"xref": f"x{suf} domain", "yref": f"y{suf} domain",
|
|
641
|
+
"x0": 0, "x1": 1, "y0": 0, "y1": 1,
|
|
642
|
+
"line": {"color": to_hex("gray75"), "width": 1},
|
|
643
|
+
"fillcolor": "rgba(0,0,0,0)"})
|
|
644
|
+
shapes.append({ # strip label band
|
|
645
|
+
"type": "rect",
|
|
646
|
+
"xref": f"x{suf} domain", "yref": f"y{suf} domain",
|
|
647
|
+
"x0": 0, "x1": 1, "y0": 1.0, "y1": 1.0 + strip_h,
|
|
648
|
+
"fillcolor": s_fill,
|
|
649
|
+
"line": {"color": s_line, "width": 1}})
|
|
650
|
+
anns.append(dict(
|
|
651
|
+
xref=f"x{suf} domain", yref=f"y{suf} domain",
|
|
652
|
+
x=0.5, y=1.0 + strip_h / 2, text=str(lvl),
|
|
653
|
+
xanchor="center", yanchor="middle",
|
|
654
|
+
showarrow=False,
|
|
655
|
+
font=dict(size=s_size, color=s_text)))
|
|
656
|
+
|
|
657
|
+
used = (set(pos) if pos is not None
|
|
658
|
+
else {facet_pos(i, n, n_col) for i in range(n)})
|
|
659
|
+
for r in range(1, n_row + 1): # ragged top row: hide
|
|
660
|
+
for c in range(1, n_col + 1): # axes of empty cells
|
|
661
|
+
if (r, c) not in used:
|
|
662
|
+
fig.update_xaxes(row=r, col=c, visible=False)
|
|
663
|
+
fig.update_yaxes(row=r, col=c, visible=False)
|
|
664
|
+
|
|
665
|
+
layout_kw = dict(
|
|
666
|
+
shapes=shapes, annotations=anns, template=None,
|
|
667
|
+
plot_bgcolor=to_hex(style_opts["panel_fill"]),
|
|
668
|
+
paper_bgcolor=to_hex(style_opts["window_fill"]),
|
|
669
|
+
height=height if height is not None else 140 + 160 * n_row,
|
|
670
|
+
)
|
|
671
|
+
if width is not None:
|
|
672
|
+
layout_kw["width"] = width
|
|
673
|
+
fig.update_layout(**layout_kw)
|
|
674
|
+
return fig
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def facet_domains_grid(n_fac, n_col_max=3, gap_x=0.04, gap_y=0.11,
|
|
678
|
+
facet_size=1.0, n_col=None):
|
|
679
|
+
"""Equal-size grid of paper-coordinate panel domains, filled
|
|
680
|
+
row-major from the TOP row down (plotly domain y runs
|
|
681
|
+
bottom-up). Each domain: dict(x=(x0, x1), y=(y0, y1), row,
|
|
682
|
+
col), rows/cols 1-based. facet_size < 1 shrinks each panel
|
|
683
|
+
about its own center. An explicit n_col (the lattice n_col)
|
|
684
|
+
overrides the n_col_max default.
|
|
685
|
+
R analog: .plotly_make_domains_grid()"""
|
|
686
|
+
if n_col is None:
|
|
687
|
+
n_col = min(n_col_max, n_fac)
|
|
688
|
+
n_col = max(1, min(int(n_col), n_fac))
|
|
689
|
+
n_row = math.ceil(n_fac / n_col)
|
|
690
|
+
|
|
691
|
+
width = max(0.01, (1 - (n_col + 1) * gap_x) / n_col)
|
|
692
|
+
height = max(0.01, (1 - (n_row + 1) * gap_y) / n_row)
|
|
693
|
+
|
|
694
|
+
domains = []
|
|
695
|
+
for k in range(n_fac):
|
|
696
|
+
r, c = divmod(k, n_col)
|
|
697
|
+
x0 = gap_x + c * (width + gap_x)
|
|
698
|
+
x1 = x0 + width
|
|
699
|
+
y1 = 1 - r * (height + gap_y)
|
|
700
|
+
y0 = y1 - height
|
|
701
|
+
|
|
702
|
+
cx, cy = (x0 + x1) / 2, (y0 + y1) / 2
|
|
703
|
+
half_w = (x1 - x0) * facet_size / 2
|
|
704
|
+
half_h = (y1 - y0) * facet_size / 2
|
|
705
|
+
domains.append(dict(
|
|
706
|
+
x=(max(0.0, cx - half_w), min(1.0, cx + half_w)),
|
|
707
|
+
y=(max(0.0, cy - half_h), min(1.0, cy + half_h)),
|
|
708
|
+
row=r + 1, col=c + 1))
|
|
709
|
+
return domains, n_row, n_col
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def facet_layout(facet_levels, facet_name="",
|
|
713
|
+
n_col_max=3, gap_x=0.04, gap_y=0.11,
|
|
714
|
+
facet_size=1.0, domains=None, domain_adjust=None,
|
|
715
|
+
reverse_domains=False, y_base=0.054,
|
|
716
|
+
y_row_shift=-0.002, yanchor="top",
|
|
717
|
+
font_size=None, ann_adjust=None, n_col=None):
|
|
718
|
+
"""Panel-grid domains plus a per-panel title annotation, shared
|
|
719
|
+
by the faceted hier/radar/dot/bubble renderers. Supply domains
|
|
720
|
+
to skip the grid computation (panels laid out elsewhere).
|
|
721
|
+
Hooks: domain_adjust(d) tweaks each domain, reverse_domains
|
|
722
|
+
flips their order (treemap row fix), ann_adjust(i, ann, d)
|
|
723
|
+
finalizes each annotation. An empty facet_name gives bare
|
|
724
|
+
level labels. An explicit n_col (the lattice n_col) overrides
|
|
725
|
+
the n_col_max grid default. R analog: .plotly_facet_layout()"""
|
|
726
|
+
if domains is None:
|
|
727
|
+
domains, _, _ = facet_domains_grid(
|
|
728
|
+
len(facet_levels), n_col_max, gap_x, gap_y, facet_size,
|
|
729
|
+
n_col=n_col)
|
|
730
|
+
if domain_adjust is not None:
|
|
731
|
+
domains = [domain_adjust(dict(d)) for d in domains]
|
|
732
|
+
if reverse_domains:
|
|
733
|
+
domains = list(reversed(domains))
|
|
734
|
+
if font_size is None:
|
|
735
|
+
font_size = round(14 * get_option("lab_size", 1))
|
|
736
|
+
|
|
737
|
+
anns = []
|
|
738
|
+
for i, d in enumerate(domains):
|
|
739
|
+
y_title = (d["y"][1] + y_base
|
|
740
|
+
+ y_row_shift * (d.get("row", 1) - 1))
|
|
741
|
+
ann = dict(
|
|
742
|
+
text=(f"{facet_name}: {facet_levels[i]}" if facet_name
|
|
743
|
+
else str(facet_levels[i])),
|
|
744
|
+
x=(d["x"][0] + d["x"][1]) / 2, y=y_title,
|
|
745
|
+
xref="paper", yref="paper",
|
|
746
|
+
xanchor="center", yanchor=yanchor,
|
|
747
|
+
showarrow=False, font=dict(size=font_size))
|
|
748
|
+
if ann_adjust is not None:
|
|
749
|
+
ann = ann_adjust(i, ann, d)
|
|
750
|
+
anns.append(ann)
|
|
751
|
+
return domains, anns
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def plot_border(top=True, right=True, color=None, width=None):
|
|
755
|
+
"""Top/right frame lines of the panel. R analog: plot_border()"""
|
|
756
|
+
if color is None:
|
|
757
|
+
color = get_option("panel_border", "#808080")
|
|
758
|
+
if width is None:
|
|
759
|
+
width = get_option("panel_lwd", 1)
|
|
760
|
+
if isinstance(color, str) and re.fullmatch(r"gray\d+", color):
|
|
761
|
+
color = "gray75" # make a gray a little lighter for plotly
|
|
762
|
+
|
|
763
|
+
line = {"color": to_hex(color), "width": width}
|
|
764
|
+
out = []
|
|
765
|
+
if top:
|
|
766
|
+
out.append({"type": "line", "xref": "paper", "yref": "paper",
|
|
767
|
+
"x0": 0, "x1": 1, "y0": 1, "y1": 1, "line": line})
|
|
768
|
+
if right:
|
|
769
|
+
out.append({"type": "line", "xref": "paper", "yref": "paper",
|
|
770
|
+
"x0": 1, "x1": 1, "y0": 0, "y1": 1, "line": line})
|
|
771
|
+
return out
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def plotly_style():
|
|
775
|
+
"""Current style settings for the render functions.
|
|
776
|
+
R analog: .plotly_style()"""
|
|
777
|
+
pb = get_option("panel_border", "#808080")
|
|
778
|
+
return {
|
|
779
|
+
"grid_color": get_option("grid_color", "gray85"),
|
|
780
|
+
"grid_col": get_option("grid_col", "gray85"),
|
|
781
|
+
"lab_color": get_option("lab_color", "black"),
|
|
782
|
+
"legend_border": get_option("legend_border", pb),
|
|
783
|
+
"panel_border": pb,
|
|
784
|
+
"panel_fill": get_option("panel_fill", "white"),
|
|
785
|
+
"window_fill": get_option("window_fill", "white"),
|
|
786
|
+
"segment_color": get_option("segment_color", "gray40"),
|
|
787
|
+
}
|