Related
I'm attempting to recreate this image using coordinates. So far I haven't had much luck. Specifically I'm running into trouble being able to see all the colors. I think what I need is a way to determine which rectangles are at the forefront vs background. Would I be better off using matplotlib? Any help on this would be greatly appreciated.
CURRENT CODE
frame = tools.csv_read(file=['attack_zones'])
x = frame.groupby('identifier')
x = x.agg(Xmin=('X', np.min), Xmax=('X', np.max)).reset_index()
y = frame.groupby('identifier')
y = y.agg(Ymin=('Y', np.min), Ymax=('Y', np.max)).reset_index()
x = x.merge(y,on='identifier',how='left')
x = x.sort_values('identifier',ascending=True)
fig = go.Figure()
#Create scatter trace of text labels
fig.add_trace(go.Scatter(
x=[-48, 52],
y=[113, 242],
text=["Rectangle reference to the plot",
"Rectangle reference to the axes"],
mode="text",
))
#Set axes properties
fig.update_xaxes(range=[-134, 134])
fig.update_yaxes(range=[0, 345])
#Set identifier colors
def colors(identifier):
if identifier < 10:
return 'purple'
if identifier < 20:
return 'pink'
if identifier < 30:
return 'yellow'
else:
return 'white'
for iden,xmin,xmax,ymin,ymax in zip(x['identifier'],x['Xmin'],x['Xmax'],x['Ymin'],x['Ymax']):
fig.add_shape(type="rect",
xref="x", yref="y",
x0=xmin, y0=ymin,
x1=xmax, y1=ymax,
fillcolor=colors(iden),
)
fig.show()
There appears to be repeatable portions in your desired image – specifically each "square" can be thought of as a 3x3 array with text labels everywhere except for the center. Therefore, I would recommend writing a function with parameters for the two corners of your square, the text annotations, and fill color of the square, then calling this function repeatedly.
import plotly.graph_objects as go
fig = go.Figure()
def draw_square_with_labels(top_left, bottom_right, text_labels, fill_color, fig=fig):
""" Adds a 3x3 square trace with labels ordered clockwise on a Plotly graph_object
Args:
top_left: starting corner of a rectangle as a tuple or list of form (x0,y0)
bottom_right: ending corner of a rectangle as a tuple or list of form (x0,y0)
text_labels: a list of text labels starting from the location of the top_left and moving clockwise
fill_color: fill color for the square
Returns:
fig with a 3x3 colored square trace with labels added
"""
x0,y0 = top_left
x2,y2 = bottom_right
x1,y1 = (x0+x2)/2, (y0+y2)/2
xy_coordinates = [
[x0,y0],[x1,y0],[x2,y0],
[x2,y1],[x2,y2],[x1,y2],
[x0,y2],[x0,y1],[x0,y0]
]
x = [c[0] for c in xy_coordinates]
y = [c[1] for c in xy_coordinates]
text_positions = [
"bottom right",
"bottom center",
"bottom left",
"middle left",
"top left",
"top center",
"top right",
"middle right",
]
fig.add_trace(go.Scatter(
x=x,
y=y,
mode='lines+text',
line_color="rgba(200,200,200,0.7)",
text=text_labels,
textposition=text_positions,
fill='toself',
fillcolor = fill_color,
))
side_lengths = [10,7,4,1]
text_labels_array = [
[31,32,33,36,39,38,37,34,""],
[21,22,23,26,29,28,27,24,""],
[11,12,13,16,19,18,17,14,""],
[1,2,3,6,9,8,7,4,""],
]
fill_colors = [
"rgba(255,255,255,0.5)",
"rgba(255,255,167,0.5)",
"rgba(255,182,193,0.5)",
"rgba(219,112,147,0.5)",
]
for side_length, text_labels, fill_color in zip(side_lengths,text_labels_array,fill_colors):
draw_square_with_labels(
top_left=[-side_length,side_length],
bottom_right=[side_length,-side_length],
text_labels=text_labels,
fill_color=fill_color
)
## add '5' at the center
fig.add_annotation(x=0, y=0, text="5", showarrow=False)
## add guidelines
edge_x, edge_y = 1,10
fig.add_shape(type="line",
x0=-edge_x, y0=-edge_y, x1=-edge_x, y1=edge_y,
line=dict(color="rgba(200,200,200,0.7)")
)
fig.add_shape(type="line",
x0=edge_x, y0=-edge_y, x1=edge_x, y1=edge_y,
line=dict(color="rgba(200,200,200,0.7)")
)
fig.add_shape(type="line",
x0=-edge_y, y0=edge_x, x1=edge_y, y1=edge_x,
line=dict(color="rgba(200,200,200,0.7)")
)
fig.add_shape(type="line",
x0=-edge_y, y0=-edge_x, x1=edge_y, y1=-edge_x,
line=dict(color="rgba(200,200,200,0.7)")
)
## add green dashed rectangle
fig.add_shape(type="rect",
x0=-3.5, y0=-3.5, x1=3.5, y1=3.5,
line=dict(color="rgba(46,204,113,0.7)", dash='dash')
)
fig.update_xaxes(visible=False, showticklabels=False)
fig.update_yaxes(visible=False, showticklabels=False)
fig.update_layout(template='plotly_white', showlegend=False)
fig.show()
I am plotting data from multiple sources and using SpanSelector to analyze the data during a particular event. I can select the data from each subplot and extract the data. However, when I make a new selection in a new subplot, I want the selection in the previous subplot to disappear.
There is span.set_visible(True/False). But for this to work, I need to know which subplot is being selected. I tried to find that using lambda function, but it is not working at the moment.
I tried first:
for signal in self.signals:
self.span[k] = SpanSelector(
self.ax[k],
onselect = lambda min, max : self.onselect_function(min, max, k),
# onselect = self.onselect_function,
direction = 'horizontal',
minspan = 1,
useblit = True,
interactive = True,
button = 1,
props = {'facecolor': 'red', 'alpha':0.5},
drag_from_anywhere = True
)
k += 1
def onselect_function(self, min_value, max_value, selected_graph)
...
But it keeps on sending a value of selected_graph = 3 for selected_graph, no matter, which subplot I select. There are 3 subgraphs ( k = 0 - 2).
I thought this was because k was not in the namespace, and I tried:
onselect = lambda min, max, k : self.onselect_function(min, max, k)
But now, I get the following error:
File "C:\Users\Ashu\miniconda3\envs\my_env\lib\site-packages\matplotlib\cbook\__init__.py", line 287, in process
func(*args, **kwargs)
File "C:\Users\Ashu\miniconda3\envs\my_env\lib\site-packages\matplotlib\widgets.py", line 1958, in release
self._release(event) File "C:\Users\Ashu\miniconda3\envs\my_env\lib\site-packages\matplotlib\widgets.py", line 2359, in _release
self.onselect(vmin, vmax)
TypeError: <lambda>() missing 1 required positional argument: 'k'
Ideally, I would want the selection to highlight the x-axis range in all subplots (I can still do that manually as I have info on what range is being selected, as shown in the figure with dashed lines). But I need the previous selection to disappear when the new selection begins.
Example of how it currently displays. The last selection was made in the top graph, so each graph shows the textual information for that selection (51-53), but the highlights in the bottom two graphs are in the wrong parts. I tried clearing axes as well but that also doesn't work.
Edit: Adding minimal reproducible example
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector
from random import randrange
import math
plt.style.use('seaborn')
noOfDataToShow = 10
noOfSignals = 3
fig, ax = plt.subplots(nrows = noOfSignals, ncols = 1)
x_pos = []
signal_data_holder = []
for i in range(noOfDataToShow):
x_pos.append(i)
for i in range(noOfSignals):
temp = []
for j in range (noOfDataToShow):
temp.append(randrange(0,10))
signal_data_holder.append(temp)
def onselect_function(min_value, max_value, selected_graph = 0):
k = 0
for param in signal_data_holder:
ax[k].cla()
ax[k].set_ylim([0,10])
ax[k].plot(x_pos, param, label = str (math.trunc(param[noOfDataToShow - 1])))
ax[k].legend(loc = "upper left")
k += 1
#******Calculating Min_Max (Can be ignored) ******#
min_value = math.floor(min_value)
max_value = math.ceil(max_value)
min_value_data = [None] * noOfSignals
max_value_data = [None] * noOfSignals
if (min_value < 0):
min_value = 0
if (max_value >= noOfDataToShow):
max_value = noOfDataToShow
for k in range(noOfSignals):
min_value_data = signal_data_holder[k][min_value]
max_value_data = signal_data_holder[k][min_value]
for i in range(min_value, max_value):
if (signal_data_holder[k][i] < min_value_data):
min_value_data = signal_data_holder[k][i]
elif (signal_data_holder[k][i] > max_value_data):
max_value_data = signal_data_holder[k][i]
labelText = "Min_Value: " + str (min_value_data) + "\n"
labelText += "Max_Value: " + str (max_value_data) + "\n"
labelText += "Range: " + str (min_value) + "-" + str (max_value) + "\n"
ax[k].legend(labels = [labelText], loc = "upper left")
print(min_value_data, max_value_data)
ax[k].axvline(min_value, color = 'red', linestyle = 'dashed')
ax[k].axvline(max_value, color = 'red', linestyle = 'dashed')
fig.canvas.draw()
#******Calculating Min_Max (Can be ignored) ******#
return min_value, max_value
span = [None] * noOfSignals
for k in range(noOfSignals):
span[k] = SpanSelector(
ax[k],
# onselect = lambda min, max, k : onselect_function(min, max, k),
# onselect = lambda min, max : onselect_function(min, max, k),
onselect = onselect_function,
direction = 'horizontal',
minspan = 1,
useblit = True,
interactive = True,
button = 1,
props = {'facecolor': 'red', 'alpha':0.5},
drag_from_anywhere = True
)
k = 0
for param in signal_data_holder:
ax[k].cla()
ax[k].set_ylim([0,10])
ax[k].plot(x_pos, param, label = str (math.trunc(param[noOfDataToShow - 1])))
ax[k].legend(loc = "upper left")
k += 1
plt.show()
I don't think you can directly retrieve the axis object linked to the span selector (or at least, I wouldn't know how). However, we can also collect the axis object of the last mouse click:
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector
import numpy as np
#sample data
fig, axis = plt.subplots(3)
for i, ax in enumerate(axis):
t=np.linspace(-i, i+1 , 100)
ax.plot(t, np.sin(2 * np.pi * t))
#list to store the axis last used with a mouseclick
curr_ax = []
#detect the currently modified axis
def on_click(event):
if event.inaxes:
curr_ax[:] = [event.inaxes]
#modify the current axis objects
def onselect(xmin, xmax):
#ignore if accidentally clicked into an axis object
if xmin==xmax:
return
#set all span selectors invisible accept the current
for ax, span in zip(axis, list_of_spans):
if ax != curr_ax[0]:
span.set_visible(False)
#do something with xmin, xmax
print(xmin, xmax)
fig.canvas.draw_idle()
#collect span selectors in a list in the same order as their axes objects
list_of_spans = [SpanSelector(
ax,
onselect,
"horizontal",
useblit=True,
props=dict(alpha=0.5, facecolor="tab:blue"),
interactive=True,
drag_from_anywhere=True
)
for ax in axis]
plt.connect('button_press_event', on_click)
plt.show()
I am playing with the third example of "Scatter plots with a legend" in the matplotlib manual.
I have tweaked the marker sizes to:
s = (50 / price) ** 2
And as an input to legend_elements I am using:
func=lambda s: 50 / np.sqrt(s)
I get the output below. The marker sizes of the legend are wrong. Why is that?
Here is the code:
import numpy as np
import matplotlib.pyplot as plt
volume = np.random.rayleigh(27, size=40)
amount = np.random.poisson(10, size=40)
ranking = np.random.normal(size=40)
price = np.random.uniform(1, 10, size=40)
fig, ax = plt.subplots()
s = (50 / price) ** 2
scatter = ax.scatter(volume, amount, c=ranking, s=s,
vmin=-3, vmax=3, cmap="Spectral", label=price)
legend1 = ax.legend(*scatter.legend_elements(num=5),
loc="upper left", title="Ranking")
ax.add_artist(legend1)
kw = dict(prop="sizes", num=5, color=scatter.cmap(0.7), fmt="$ {x:.2f}",
func=lambda s: 50 / np.sqrt(s),
)
legend2 = ax.legend(*scatter.legend_elements(**kw),
loc="lower right", title="Price")
for p, v, a in zip(price, volume, amount):
ax.annotate(round(p, 0), (v, a))
plt.show()
The issue appears to be related to the inverse relationship between price and marker size. The way the data is calculated in legend_elements doesn't account for this, and the calculation doesn't quite work. I've submitted a pull request.
The problem is in np.interp that expects increasing input for the second argument. Here is a work around for now that sorts the input first:
legend2 = ax.legend(*legend_elements(scatter, **kw),
loc="lower right", title="Price")
Run this after defining legend_elements as:
def legend_elements(self, prop="colors", num="auto",
fmt=None, func=lambda x: x, **kwargs):
"""
Creates legend handles and labels for a PathCollection. This is useful
for obtaining a legend for a :meth:`~.Axes.scatter` plot. E.g.::
scatter = plt.scatter([1, 2, 3], [4, 5, 6], c=[7, 2, 3])
plt.legend(*scatter.legend_elements())
Also see the :ref:`automatedlegendcreation` example.
Parameters
----------
prop : string, optional, default *"colors"*
Can be *"colors"* or *"sizes"*. In case of *"colors"*, the legend
handles will show the different colors of the collection. In case
of "sizes", the legend will show the different sizes.
num : int, None, "auto" (default), array-like, or `~.ticker.Locator`,
optional
Target number of elements to create.
If None, use all unique elements of the mappable array. If an
integer, target to use *num* elements in the normed range.
If *"auto"*, try to determine which option better suits the nature
of the data.
The number of created elements may slightly deviate from *num* due
to a `~.ticker.Locator` being used to find useful locations.
If a list or array, use exactly those elements for the legend.
Finally, a `~.ticker.Locator` can be provided.
fmt : str, `~matplotlib.ticker.Formatter`, or None (default)
The format or formatter to use for the labels. If a string must be
a valid input for a `~.StrMethodFormatter`. If None (the default),
use a `~.ScalarFormatter`.
func : function, default *lambda x: x*
Function to calculate the labels. Often the size (or color)
argument to :meth:`~.Axes.scatter` will have been pre-processed
by the user using a function *s = f(x)* to make the markers
visible; e.g. *size = np.log10(x)*. Providing the inverse of this
function here allows that pre-processing to be inverted, so that
the legend labels have the correct values;
e.g. *func = np.exp(x, 10)*.
kwargs : further parameters
Allowed keyword arguments are *color* and *size*. E.g. it may be
useful to set the color of the markers if *prop="sizes"* is used;
similarly to set the size of the markers if *prop="colors"* is
used. Any further parameters are passed onto the `.Line2D`
instance. This may be useful to e.g. specify a different
*markeredgecolor* or *alpha* for the legend handles.
Returns
-------
tuple (handles, labels)
with *handles* being a list of `.Line2D` objects
and *labels* a matching list of strings.
"""
handles = []
labels = []
hasarray = self.get_array() is not None
if fmt is None:
fmt = mpl.ticker.ScalarFormatter(useOffset=False, useMathText=True)
elif isinstance(fmt, str):
fmt = mpl.ticker.StrMethodFormatter(fmt)
fmt.create_dummy_axis()
if prop == "colors":
if not hasarray:
warnings.warn("Collection without array used. Make sure to "
"specify the values to be colormapped via the "
"`c` argument.")
return handles, labels
u = np.unique(self.get_array())
size = kwargs.pop("size", mpl.rcParams["lines.markersize"])
elif prop == "sizes":
u = np.unique(self.get_sizes())
color = kwargs.pop("color", "k")
else:
raise ValueError("Valid values for `prop` are 'colors' or "
f"'sizes'. You supplied '{prop}' instead.")
fmt.set_bounds(func(u).min(), func(u).max())
if num == "auto":
num = 9
if len(u) <= num:
num = None
if num is None:
values = u
label_values = func(values)
else:
if prop == "colors":
arr = self.get_array()
elif prop == "sizes":
arr = self.get_sizes()
if isinstance(num, mpl.ticker.Locator):
loc = num
elif np.iterable(num):
loc = mpl.ticker.FixedLocator(num)
else:
num = int(num)
loc = mpl.ticker.MaxNLocator(nbins=num, min_n_ticks=num-1,
steps=[1, 2, 2.5, 3, 5, 6, 8, 10])
label_values = loc.tick_values(func(arr).min(), func(arr).max())
cond = ((label_values >= func(arr).min()) &
(label_values <= func(arr).max()))
label_values = label_values[cond]
yarr = np.linspace(arr.min(), arr.max(), 256)
xarr = func(yarr)
ix = np.argsort(xarr)
values = np.interp(label_values, xarr[ix], yarr[ix])
kw = dict(markeredgewidth=self.get_linewidths()[0],
alpha=self.get_alpha())
kw.update(kwargs)
for val, lab in zip(values, label_values):
if prop == "colors":
color = self.cmap(self.norm(val))
elif prop == "sizes":
size = np.sqrt(val)
if np.isclose(size, 0.0):
continue
h = mlines.Line2D([0], [0], ls="", color=color, ms=size,
marker=self.get_paths()[0], **kw)
handles.append(h)
if hasattr(fmt, "set_locs"):
fmt.set_locs(label_values)
l = fmt(lab)
labels.append(l)
return handles, labels
You can also manually create your own legend. The trick here is that you have to apply np.sqrt to sizes in the legend for some reason I don't quite follow but #busybear has in her snippet.
import numpy as np
import matplotlib.pyplot as plt
volume = np.random.rayleigh(27, size=40)
amount = np.random.poisson(10, size=40)
ranking = np.random.normal(size=40)
price = np.random.uniform(1, 10, size=40)
fig, ax = plt.subplots()
s = (50 / price) ** 2
scatter = ax.scatter(volume, amount, c=ranking, s=s,
vmin=-3, vmax=3, cmap="Spectral", label=price)
legend1 = ax.legend(*scatter.legend_elements(num=5),
loc="upper left", title="Ranking")
ax.add_artist(legend1)
# # easy legend
# kw = dict(prop="sizes", num=5, color=scatter.cmap(0.7), fmt="$ {x:.2f}",
# func=lambda s: 50 / np.sqrt(s),
# )
# legend2 = ax.legend(*scatter.legend_elements(**kw),
# loc="lower right", title="Price")
# ax.add_artist(legend2)
# manual legend
legend_values = np.array([2,4,6,8])
legend_sizes = (50 / legend_values) ** 2
# IMPORTANT: for some reason the square root needs to be applied to sizes in the legend
legend_sizes_sqrt = np.sqrt(legend_sizes)
elements3 = [Line2D([0], [0], color=scatter.cmap(0.7), lw=0, marker="o", linestyle=None, markersize=s) for s in legend_sizes_sqrt]
legend3 = ax.legend(elements3, [f"$ {p:.2f}" for p in legend_values], loc='lower right', title="Price")
ax.add_artist(legend3)
for p, v, a in zip(price, volume, amount):
ax.annotate(round(p, 0), (v, a))
plt.show()
When I use matplotlib colorbars it seems to take what would have been a centered axis and force it to become left align.
I'm adding a colorbar like this:
def colorbar(scalars, colors):
""" adds a color bar next to the axes """
printDBG('colorbar()')
# Parameters
xy, width, height = _axis_xy_width_height()
orientation = ['vertical', 'horizontal'][0]
TICK_FONTSIZE = 8
#
listed_cmap = scores_to_cmap(scalars, colors)
# Create scalar mappable with cmap
sorted_scalars = sorted(scalars)
sm = plt.cm.ScalarMappable(cmap=listed_cmap)
sm.set_array(sorted_scalars)
# Use mapable object to create the colorbar
COLORBAR_SHRINK = .42 # 1
COLORBAR_PAD = .01 # 1
COLORBAR_ASPECT = np.abs(20 * height / (width)) # 1
printDBG('[df] COLORBAR_ASPECT = %r' % COLORBAR_ASPECT)
cb = plt.colorbar(sm, orientation=orientation, shrink=COLORBAR_SHRINK,
pad=COLORBAR_PAD, aspect=COLORBAR_ASPECT)
# Add the colorbar to the correct label
axis = cb.ax.xaxis if orientation == 'horizontal' else cb.ax.yaxis
position = 'bottom' if orientation == 'horizontal' else 'right'
axis.set_ticks_position(position)
axis.set_ticks([0, .5, 1])
cb.ax.tick_params(labelsize=TICK_FONTSIZE)
I'm not sure if there is anything I can do to prevent this behavior.
So it's somewhat well known that in matplotlib zoom, pressing 'x' or 'y' when zooming will zoom on only the x or y axis. I would like to modify this slightly by subclassing the NavigationToolbar2 in backend_bases.py
Specifically, I would like to have the functionality that if the mouse is in the region on the canvas below a plot (or above, depending on where I have put my axes), or to the left or right of the plot, to choose to zoom with _zoom_mode = 'x' or 'y'. (In addition to keeping the default zoom to rect functionality when the mouse is inside the plot.)
Looking at backend_bases, it appears the method I need to modify is press_zoom
def press_zoom(self, event):
if event.button=1:
self._button_pressed=1
elif event.button == 3:
self._button_pressed=3
else:
self._button_pressed=None
return
x, y = event.x, event.y
# push the current view to define home if stack is empty
if self._views.empty(): self.push_current()
self._xypress=[]
for i, a in enumerate(self.canvas.figure.get_axes()):
if (x is not None and y is not None and a.in_axes(event) and
a.get_navigate() and a.can_zoom()) :
self._xypress.append(( x, y, a, i, a.viewLim.frozen(),
a.transData.frozen() ))
id1 = self.canvas.mpl_connect('motion_notify_event', self.drag_zoom)
id2 = self.canvas.mpl_connect('key_press_event',
self._switch_on_zoom_mode)
id3 = self.canvas.mpl_connect('key_release_event',
self._switch_off_zoom_mode)
self._ids_zoom = id1, id2, id3
self._zoom_mode = event.key
self.press(event)
and add an elif to the big if statement there and use it to set the zoom mode there, but what I cannot figure out (yet), is how to tell if the mouse is in the region appropriate for 'x' or 'y' zoom mode.
So, how can I tell when the mouse is just outside of a plot?
By converting the x and y coordinate into Axes coordinates. You can tell if they're just outside the axes, if they're less than 0 or larger than 1.
Here is a simple example of how it could work.
def is_just_outside(fig, event):
x,y = event.x, event.y
print x, y
for ax in fig.axes:
xAxes, yAxes = ax.transAxes.inverted().transform([x, y])
print xAxes, yAxes
if (-0.02 < xAxes < 0) | (1 < xAxes < 1.02):
print "just outside x-axis"
if (-0.02 < yAxes < 0) | (1 < yAxes < 1.02):
print "just outside y-axis"
x = np.linspace(-np.pi,np.pi,100)
y = np.sin(x)
fig = plt.figure()
plt.plot(x,y)
ax = fig.add_subplot(111)
fig.canvas.mpl_connect('button_press_event', lambda e: is_just_outside(fig, e))
plt.show()