matplotly 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.
- matplotly/__init__.py +124 -0
- matplotly/_api.py +984 -0
- matplotly/_code_gen.py +1793 -0
- matplotly/_commands.py +109 -0
- matplotly/_introspect.py +1197 -0
- matplotly/_profiles.py +241 -0
- matplotly/_renderer.py +79 -0
- matplotly/_style_import.py +155 -0
- matplotly/_types.py +31 -0
- matplotly/panels/__init__.py +37 -0
- matplotly/panels/_bar.py +788 -0
- matplotly/panels/_base.py +38 -0
- matplotly/panels/_color_utils.py +221 -0
- matplotly/panels/_distribution.py +1605 -0
- matplotly/panels/_errorbar.py +652 -0
- matplotly/panels/_fill.py +90 -0
- matplotly/panels/_global.py +1507 -0
- matplotly/panels/_heatmap.py +898 -0
- matplotly/panels/_histogram.py +938 -0
- matplotly/panels/_line.py +709 -0
- matplotly/panels/_marginal.py +944 -0
- matplotly/panels/_scatter.py +428 -0
- matplotly/panels/_subplot.py +846 -0
- matplotly-0.1.0.dist-info/METADATA +120 -0
- matplotly-0.1.0.dist-info/RECORD +27 -0
- matplotly-0.1.0.dist-info/WHEEL +4 -0
- matplotly-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Base class for artist-specific control panels."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import abc
|
|
5
|
+
|
|
6
|
+
import ipywidgets as widgets
|
|
7
|
+
|
|
8
|
+
from .._commands import CommandStack
|
|
9
|
+
from .._renderer import CanvasManager
|
|
10
|
+
from .._types import ArtistGroup
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ArtistPanel(abc.ABC):
|
|
14
|
+
"""Abstract base for a panel that controls one ArtistGroup."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, group: ArtistGroup, stack: CommandStack,
|
|
17
|
+
canvas: CanvasManager):
|
|
18
|
+
self._group = group
|
|
19
|
+
self._stack = stack
|
|
20
|
+
self._canvas = canvas
|
|
21
|
+
self._widget: widgets.Widget | None = None
|
|
22
|
+
|
|
23
|
+
@abc.abstractmethod
|
|
24
|
+
def build(self) -> widgets.Widget:
|
|
25
|
+
"""Construct and return the ipywidgets control panel."""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def widget(self) -> widgets.Widget:
|
|
29
|
+
if self._widget is None:
|
|
30
|
+
self._widget = self.build()
|
|
31
|
+
return self._widget
|
|
32
|
+
|
|
33
|
+
# -- helpers -----------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
def _execute_and_redraw(self, cmd) -> None:
|
|
36
|
+
"""Execute a command and refresh the canvas."""
|
|
37
|
+
self._stack.execute(cmd)
|
|
38
|
+
self._canvas.redraw()
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Shared color utilities used by LinePanel, ScatterPanel, and GlobalPanel."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import ipywidgets as widgets
|
|
5
|
+
import matplotlib
|
|
6
|
+
from matplotlib.colors import ListedColormap, to_hex
|
|
7
|
+
|
|
8
|
+
_COLORMAPS = [
|
|
9
|
+
# Qualitative
|
|
10
|
+
"tab10", "tab20", "Set1", "Set2", "Set3", "Paired",
|
|
11
|
+
"Dark2", "Accent", "Pastel1", "Pastel2",
|
|
12
|
+
# Diverging
|
|
13
|
+
"bwr", "coolwarm", "RdBu", "RdYlBu", "RdYlGn",
|
|
14
|
+
"PiYG", "PRGn", "seismic",
|
|
15
|
+
# Sequential
|
|
16
|
+
"viridis", "plasma", "inferno", "magma", "cividis",
|
|
17
|
+
"Blues", "Reds", "Greens", "Oranges", "Purples",
|
|
18
|
+
"YlOrRd", "YlGnBu", "BuGn",
|
|
19
|
+
# Cyclic
|
|
20
|
+
"twilight", "hsv",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
_DW = "48px" # uniform description_width
|
|
24
|
+
_NW = "50px" # uniform number-edit width
|
|
25
|
+
_SN = {"description_width": _DW}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _slider_num(slider, desc_width=None):
|
|
29
|
+
"""Slider (no readout) + linked number edit box (2 dp)."""
|
|
30
|
+
slider.readout = False
|
|
31
|
+
slider.style = {"description_width": desc_width or _DW}
|
|
32
|
+
slider.layout.flex = "1 1 auto"
|
|
33
|
+
if isinstance(slider, widgets.IntSlider):
|
|
34
|
+
num = widgets.IntText(value=slider.value,
|
|
35
|
+
layout=widgets.Layout(width=_NW))
|
|
36
|
+
else:
|
|
37
|
+
num = widgets.BoundedFloatText(
|
|
38
|
+
value=round(slider.value, 2), step=slider.step,
|
|
39
|
+
min=slider.min, max=slider.max,
|
|
40
|
+
layout=widgets.Layout(width=_NW))
|
|
41
|
+
widgets.link((slider, "value"), (num, "value"))
|
|
42
|
+
return widgets.HBox([slider, num],
|
|
43
|
+
layout=widgets.Layout(width="80%"))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _cmap_color(cmap, i, n):
|
|
47
|
+
"""Sample color i of n from a colormap.
|
|
48
|
+
|
|
49
|
+
For small qualitative colormaps (tab10, Set1, etc. with N <= 20), returns
|
|
50
|
+
sequential discrete colors (1st, 2nd, 3rd...). For everything else
|
|
51
|
+
(continuous or large-N listed colormaps like viridis with N=256),
|
|
52
|
+
interpolates evenly across the full [0,1] range.
|
|
53
|
+
"""
|
|
54
|
+
if isinstance(cmap, ListedColormap) and cmap.N <= 20:
|
|
55
|
+
return cmap(i % cmap.N)
|
|
56
|
+
return cmap(i / max(n - 1, 1))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_palette_colors(cmap_name, n=10):
|
|
60
|
+
"""Get n hex colors from a colormap."""
|
|
61
|
+
try:
|
|
62
|
+
cmap = matplotlib.colormaps.get_cmap(cmap_name)
|
|
63
|
+
except Exception:
|
|
64
|
+
cmap = matplotlib.colormaps.get_cmap("tab10")
|
|
65
|
+
return [to_hex(_cmap_color(cmap, i, n)) for i in range(n)]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _make_color_dot(hex_color):
|
|
69
|
+
return (f'<div style="width:14px;height:14px;background:{hex_color};'
|
|
70
|
+
f'border:1px solid #666;border-radius:2px;flex-shrink:0"></div>')
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cmap_color_btn(initial_color, on_change_fn, cmap_panel=None):
|
|
74
|
+
"""Compact color button; click toggles colormap swatch row + palette.
|
|
75
|
+
|
|
76
|
+
*on_change_fn(hex_val)* is called when a new color is picked.
|
|
77
|
+
Returns ``(color_btn, swatch_row)`` widgets for flexible layout.
|
|
78
|
+
"""
|
|
79
|
+
color_btn = widgets.Button(
|
|
80
|
+
layout=widgets.Layout(width='28px', height='24px',
|
|
81
|
+
padding='0', min_width='28px'),
|
|
82
|
+
tooltip="Click to choose color")
|
|
83
|
+
color_btn.style.button_color = initial_color
|
|
84
|
+
|
|
85
|
+
swatch_btns = []
|
|
86
|
+
for _ in range(10):
|
|
87
|
+
b = widgets.Button(
|
|
88
|
+
layout=widgets.Layout(width="18px", height="16px",
|
|
89
|
+
padding="0", margin="1px",
|
|
90
|
+
min_width="18px"))
|
|
91
|
+
swatch_btns.append(b)
|
|
92
|
+
|
|
93
|
+
palette_btn = widgets.Button(
|
|
94
|
+
icon="paint-brush", tooltip="Custom color...",
|
|
95
|
+
layout=widgets.Layout(width="18px", height="16px",
|
|
96
|
+
padding="0", min_width="18px",
|
|
97
|
+
margin="1px"))
|
|
98
|
+
palette_btn.style.button_color = "#e8e8e8"
|
|
99
|
+
palette_btn.add_class("pb-swatch-btn")
|
|
100
|
+
|
|
101
|
+
_pk_cls = f"pb-txtpk-{id(color_btn)}"
|
|
102
|
+
picker = widgets.ColorPicker(
|
|
103
|
+
value=initial_color, concise=True,
|
|
104
|
+
layout=widgets.Layout(width="1px", height="1px",
|
|
105
|
+
overflow="hidden", padding="0",
|
|
106
|
+
margin="0", border="0"))
|
|
107
|
+
picker.add_class(_pk_cls)
|
|
108
|
+
|
|
109
|
+
_js = widgets.Output(
|
|
110
|
+
layout=widgets.Layout(height="0px", overflow="hidden"))
|
|
111
|
+
|
|
112
|
+
swatch_row = widgets.HBox(
|
|
113
|
+
swatch_btns + [palette_btn, picker, _js],
|
|
114
|
+
layout=widgets.Layout(display='none', align_items='center',
|
|
115
|
+
gap='1px'))
|
|
116
|
+
|
|
117
|
+
_upd = [False]
|
|
118
|
+
|
|
119
|
+
def _refresh():
|
|
120
|
+
cname = 'tab10'
|
|
121
|
+
if cmap_panel and hasattr(cmap_panel, '_selected'):
|
|
122
|
+
cname = cmap_panel._selected
|
|
123
|
+
colors = _get_palette_colors(cname, 10)
|
|
124
|
+
for i, btn in enumerate(swatch_btns):
|
|
125
|
+
btn.style.button_color = colors[i]
|
|
126
|
+
|
|
127
|
+
def _apply(hex_val):
|
|
128
|
+
_upd[0] = True
|
|
129
|
+
color_btn.style.button_color = hex_val
|
|
130
|
+
picker.value = hex_val
|
|
131
|
+
_upd[0] = False
|
|
132
|
+
on_change_fn(hex_val)
|
|
133
|
+
|
|
134
|
+
for b in swatch_btns:
|
|
135
|
+
def _on_sw(btn, _b=b):
|
|
136
|
+
_apply(_b.style.button_color)
|
|
137
|
+
b.on_click(_on_sw)
|
|
138
|
+
|
|
139
|
+
def _toggle(btn):
|
|
140
|
+
if swatch_row.layout.display == 'none':
|
|
141
|
+
_refresh()
|
|
142
|
+
swatch_row.layout.display = ''
|
|
143
|
+
else:
|
|
144
|
+
swatch_row.layout.display = 'none'
|
|
145
|
+
color_btn.on_click(_toggle)
|
|
146
|
+
|
|
147
|
+
def _on_pal(b):
|
|
148
|
+
with _js:
|
|
149
|
+
_js.clear_output()
|
|
150
|
+
from IPython.display import display as ipy_display, Javascript
|
|
151
|
+
ipy_display(Javascript(
|
|
152
|
+
"setTimeout(function(){"
|
|
153
|
+
"var el=document.querySelector('.%s input[type=\"color\"]');"
|
|
154
|
+
"if(el)el.click();"
|
|
155
|
+
"},150);" % _pk_cls))
|
|
156
|
+
palette_btn.on_click(_on_pal)
|
|
157
|
+
|
|
158
|
+
def _on_pk(change):
|
|
159
|
+
if _upd[0]:
|
|
160
|
+
return
|
|
161
|
+
_apply(change["new"])
|
|
162
|
+
picker.observe(_on_pk, names="value")
|
|
163
|
+
|
|
164
|
+
return color_btn, swatch_row
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _refresh_legend(ax):
|
|
168
|
+
"""Recreate the legend so it reflects current artist properties."""
|
|
169
|
+
if ax is None:
|
|
170
|
+
return
|
|
171
|
+
leg = ax.get_legend()
|
|
172
|
+
if leg is None:
|
|
173
|
+
return
|
|
174
|
+
handles, labels = ax.get_legend_handles_labels()
|
|
175
|
+
if not handles:
|
|
176
|
+
return
|
|
177
|
+
props = {}
|
|
178
|
+
try:
|
|
179
|
+
props['frameon'] = leg.get_frame().get_visible()
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
try:
|
|
183
|
+
props['fontsize'] = leg._fontsize
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
try:
|
|
187
|
+
props['ncol'] = getattr(leg, '_ncols', 1)
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
try:
|
|
191
|
+
props['markerfirst'] = leg._markerfirst
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
try:
|
|
195
|
+
props['handletextpad'] = leg.handletextpad
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
198
|
+
try:
|
|
199
|
+
props['handleheight'] = leg.handleheight
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
try:
|
|
203
|
+
props['loc'] = leg._loc
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
206
|
+
try:
|
|
207
|
+
if hasattr(leg, '_bbox_to_anchor') and leg._bbox_to_anchor is not None:
|
|
208
|
+
inv = ax.transAxes.inverted()
|
|
209
|
+
bx, by = inv.transform(
|
|
210
|
+
(leg._bbox_to_anchor.x0, leg._bbox_to_anchor.y0))
|
|
211
|
+
props['bbox_to_anchor'] = (round(bx, 3), round(by, 3))
|
|
212
|
+
except Exception:
|
|
213
|
+
pass
|
|
214
|
+
ax.legend(handles, labels, **props)
|
|
215
|
+
# Preserve custom legend text colors
|
|
216
|
+
if hasattr(ax, '_matplotly_leg_text_colors'):
|
|
217
|
+
new_leg = ax.get_legend()
|
|
218
|
+
if new_leg:
|
|
219
|
+
for _i, _c in enumerate(ax._matplotly_leg_text_colors):
|
|
220
|
+
if _i < len(new_leg.get_texts()):
|
|
221
|
+
new_leg.get_texts()[_i].set_color(_c)
|