Restricting panning range in matplotlib plots - python

Is there any way to restrict the range that a user can interactively pan a plot in matplotlib?
For example, I'd like to limit the user's panning range to the positive quadrant of the plot, so x > 0 and y > 0, effectively creating floors at x = 0 and y = 0.

There is not built-in way to restrict zooming or panning other than restricting it to either the x or y direction. So you need to implement that yourself.
Connecting to the 'xlim_changed' and 'ylim_changed' events, you may check if the limits are valid or not, and potentially reset them to the valid range.
import matplotlib.pyplot as plt
import numpy as np
fig,ax=plt.subplots()
x = np.sin(np.linspace(0,10, 266))+1
ax.plot(x)
class Restrictor():
def __init__(self, ax, x=lambda x: True,y=lambda x: True):
self.res = [x,y]
self.ax =ax
self.limits = self.get_lim()
self.ax.callbacks.connect('xlim_changed', lambda evt: self.lims_change(axis=0))
self.ax.callbacks.connect('ylim_changed', lambda evt: self.lims_change(axis=1))
def get_lim(self):
return [self.ax.get_xlim(), self.ax.get_ylim()]
def set_lim(self, axis, lim):
if axis==0:
self.ax.set_xlim(lim)
else:
self.ax.set_ylim(lim)
self.limits[axis] = self.get_lim()[axis]
def lims_change(self, event=None, axis=0):
curlim = np.array(self.get_lim()[axis])
if self.limits[axis] != self.get_lim()[axis]:
# avoid recursion
if not np.all(self.res[axis](curlim)):
# if limits are invalid, reset them to previous state
self.set_lim(axis, self.limits[axis])
else:
# if limits are valid, update previous stored limits
self.limits[axis] = self.get_lim()[axis]
res = Restrictor(ax, x=lambda x: x>=0,y=lambda x: x>=0)
plt.show()
The user experience of such limitations are not great, since a certain action (like moving the mouse) is not answered with the expected behaviour (plot does not move); but one has to judge for oneself if this would still make sense.

Related

Ipywidgets: alter slider defaults based on a scenario

I'm using ipywidges to create a plot. I'd like to have a dropdown with options (e.g. Scenario A, Scenario B). Each scenario should change the slider position (a=0, b=1), and one should be able to modify the parameters freely afterwards. Any ideas?
Here is my toy example:
import ipywidgets as widgets
def line(a=0,b=1):
#fig, ax = plt.subplots(figsize=(6, 4))
x = np.arange(-10,10)
y = a+xrange*b
plt.xlim((-10,10))
plt.ylim((-10,10))
plt.plot(x, y)
widgets.interact(line, a=(-10,10,0.1), b=(-10,10,0.1))
I was playing with an additional wrapper functions but with no success. In reality of course, I would like to have a few more scenarios and a lot more parameters.
Just as another answer, you could also link different widgets. In this case we are going to link the a and b sliders with a Dropdown menu, such that a change of the latter will call the functions on_change_* and switch the default values of the sliders (depending on the chosen scenario).
import ipywidgets as widgets
import numpy as np
import matplotlib.pyplot as plt
def line(a=0,b=0):
x = np.arange(-10,10)
y = a+x*b
plt.xlim((-10,10))
plt.ylim((-10,10))
plt.plot(x, y)
a_slider = widgets.FloatSlider(min=-10, max=10, step=0.1, value=0)
b_slider = widgets.FloatSlider(min=-10, max=10, step=0.1, value=1)
drop = widgets.Dropdown(options=["a", "b"], value="a", description='Scenario:')
def on_choose_a(d):
if drop.value == "a":
a_slider.value = 2
else:
a_slider.value = 5
return a_slider.value
def on_choose_b(d):
if drop.value == "a":
b_slider.value = 3
else:
b_slider.value = 7
return b_slider.value
widgets.dlink((drop, "value"), (a_slider, "value"), on_choose_a)
widgets.dlink((drop, "value"), (b_slider, "value"), on_choose_b)
display(drop)
widgets.interact(line, a=a_slider, b=b_slider);
All right, I think I've found a very nice workaround:
def line(a=0,b=0):
x = np.arange(-10,10)
y = a+x*b
plt.xlim((-10,10))
plt.ylim((-10,10))
plt.plot(x, y)
sliders = widgets.interact(a=(-10,10,0.1), b=(-10,10,0.1))
def test(chose_defaults):
if chose_defaults=="a":
#sliders
def h(a=5,b=5):
return(line(a,b))
if chose_defaults=="b":
#sliders
def h(a=0,b=1):
return(line(a,b))
widgets.interact(test, chose_defaults=["a","b"])
The above code basically nests two widgets. Firstly a separate widget for chosing a scenario is shown; action for the scenarios are the plots that differ only in the default setup.

Updating matplotlib bar chart in real time

I'm coding the Bubble Sort algorithm, where it takes in a list of random integers, sorts them in increasing order, and plots the sorted list of integers as the y-values of a bar graph. My first bar chart is the unsorted list and the second one is the sorted one.
I want the graph to update in real time. I would do this by feeding matplotlib lists that are sorted a bit more and more. (ie new lists each time)
I've looked up many tutorials on updating charts in matplotlib, but they are mostly for ones like scatter plots where one point is added at a time. I've also seen Updating a matplotlib bar graph?, but I cannot understand it for the life of me...
So, how would I go about making this bar graph update in real time? Thank you!
My code is below. You can ignore the sorting algorithm. All that you need to know is that it returns a list of sorted numbers in the form [1, 2, 3, 4, 5].
import random
import matplotlib.pyplot as plt
import numpy as np
print("bubble sort")
plt.style.use('fivethirtyeight')
n = int(input("How many items would you like to sort? "))
list_num = np.random.randint(0,500, n)
orig_list_num = list_num.copy()
def is_sorted(list_num):
Flag = True
j=-1
while Flag == True:
Flag=False
j+=1
for i in range(0, (len(list_num))-j):
if i+1 >= len(list_num):
break
if list_num[i] > list_num[i+1]:
tempvar=list_num[i]
list_num[i] = list_num[i+1]
list_num[i+1] = tempvar
Flag=True
return list_num
list_num = is_sorted(list_num)
#unsorted graph
indices = np.arange(n)
plt.subplot(1, 2, 1)
plt.bar(indices, orig_list_num)
plt.xlabel("Unsorted")
#sorted graph
plt.subplot(1, 2, 2)
plt.bar(indices, list_num)
plt.xlabel("Sorted")
plt.show()
It is first version without FuncAnimation for animation. It may be slow.
And it may have problem to close window befor end of animation.
First I use
plt.show(block=False)
to display plots without blocking code
and next I run is_sorted() to update plot.
Inside is_sorted() I use
plt.cla()
to clear last drawn axis
And now I can draw bar again in the same place but with new data.
plt.bar(indices, list_num)
After that I slow down code so it has time to display plot in window
plt.pause(0.01)
without pause it may display empty window.
BTW:
if you use -1 in (len(list_num))-j-1 then you don't need if i+1 >= len(list_num): break
import random
import matplotlib.pyplot as plt
import numpy as np
# --- functions --
def is_sorted(list_num):
flag = True
j = -1
while flag:
flag = False
j += 1
for i in range(0, (len(list_num))-j-1):
if list_num[i] > list_num[i+1]:
# in Python you can do it without `tempvar`
list_num[i], list_num[i+1] = list_num[i+1], list_num[i]
flag = True
plt.cla()
plt.bar(indices, list_num)
plt.pause(0.01)
if closed:
flag = False
break # it exits `for`-loop but not `while`-loop and I need `flag = False`
def on_close(event):
global closed # to assign value to external variable which I need in `is_sorted
closed = True
print('Closing window')
# --- main ---
closed = False # default value at start
print("bubble sort")
plt.style.use('fivethirtyeight')
#n = int(input("How many items would you like to sort? "))
n = 20
list_num = np.random.randint(0, 500, n)
orig_list_num = list_num.copy()
indices = np.arange(n)
#unsorted graph
plt.subplot(1, 2, 1)
plt.bar(indices, orig_list_num)
plt.xlabel("Unsorted")
#sorted graph
plt.subplot(1, 2, 2)
plt.bar(indices, list_num)
plt.xlabel("Sorted")
plt.ion() # `Interaction ON` siliar to `block=False`
#plt.show(block=False)
# assign function to plot
fig = plt.gcf()
fig.canvas.mpl_connect('close_event', on_close)
is_sorted(list_num)
input("Press ENTER to exit")
EDIT:
Using idea from your link I created second version - I think it can run little faster but I expected much better speed.
In this version I assign bar to variable
bar2 = plt.bar(indices, list_num)
so later I can chagen bar heigh without drawing all again
bar2[i].set_height(list_num[i])
bar2[i+1].set_height(list_num[i+1])
import random
import matplotlib.pyplot as plt
import numpy as np
# --- functions --
def is_sorted(list_num):
flag = True
j = -1
while flag:
flag = False
j += 1
for i in range(0, (len(list_num))-j-1):
if list_num[i] > list_num[i+1]:
# in Python you can do it without `tempvar`
list_num[i], list_num[i+1] = list_num[i+1], list_num[i]
flag = True
# replace all values
#for rect, value in zip(bar2, list_num):
# rect.set_height(value)
# replace only two new values
bar2[i].set_height(list_num[i])
bar2[i+1].set_height(list_num[i+1])
plt.pause(0.001)
if closed:
flag = False
break # it exits `for`-loop but not `while`-loop and I need `flag = False`
def on_close(event):
global closed # to assign value to external variable which I need in `is_sorted
closed = True
print('Closing window')
# --- main ---
closed = False # default value at start
print("bubble sort")
plt.style.use('fivethirtyeight')
#n = int(input("How many items would you like to sort? "))
n = 20
list_num = np.random.randint(0, 500, n)
orig_list_num = list_num.copy()
indices = np.arange(n)
#unsorted graph
plt.subplot(1, 2, 1)
plt.bar(indices, orig_list_num)
plt.xlabel("Unsorted")
#sorted graph
plt.subplot(1, 2, 2)
bar2 = plt.bar(indices, list_num)
plt.xlabel("Sorted")
plt.ion() # `Interaction ON` siliar to `block=False`
#plt.show(block=False)
# assign function to plot
fig = plt.gcf()
fig.canvas.mpl_connect('close_event', on_close)
is_sorted(list_num)
input("Press ENTER to exit")
I was thinking about version with FuncAnimation but it would need to change a lot cod. It would need to create code which use single for-loop instead while+for - and then FuncAnimation would repeate code instead this for-loop.
I had other idea - in plt.plot() you could replace in plot data with ax.set_ydata() but plt.bar() doesn't have this function. But set_height() seems similar.
In answer to question How do I correctly implement a bubble sort algorithm in python tkinter? I created animation directly in tkinter without matplotlib

Create random points within a polygon within a class

I am trying to create a single point within a polygon using a class for use in an agent based model.
Currently I am able to create random points constrained to the bounds of the polygon, but not the polygon itself. My code at present appears to ignore the if statement within the while loop. I am very new to python so this could be a limitation I am missing.
Here is my current code:
import geopandas as gpd
import matplotlib.pyplot as plt
import random
import pandas as pd
bounds = gpd.read_file("./data/liverpool_bounds.gpkg")
class Agent():
def __init__(self, bounds):
x_min, y_min, x_max, y_max = bounds.total_bounds
counter = 0
while counter != 1:
x = random.uniform(x_min, x_max)
y = random.uniform(y_min, y_max)
df = pd.DataFrame({'x': [x], 'y': [y]})
self.agent = gpd.GeoDataFrame(
df, geometry=gpd.points_from_xy(df.x, df.y))
if self.agent.within(bounds) is True:
counter = 1
# counter does not increase
print(counter)
# gives both True and False
print(self.agent.within(bounds))
Agent(bounds).agent
This code gives an infinite loop. Expected behavior would be to stop given a Boolean True value, and to continue with False, until a True value.
Don't use the counter variable, but a break statement when the point is sampled within the polygon. The counter variable will always be one on exit so this does not carry new information. I'm not really familiar with the Geopandas library, but you can achieve a solution with Shapely, which is a very nice library imo. With this program structure your object becomes more generally useable.
from shapely.geometry import Point, Polygon
import random
bounds = [(0, 0), (1, 0), (1, 1), (0, 1)]
class Agent():
def __init__(self, bounds):
self.polygon = Polygon(bounds)
# implement your object wide dataframe here to which you can append
def add_random_point(self):
xmin, ymin, xmax, ymax = self.polygon.bounds
while True:
x = random.uniform(xmin, xmax)
y = random.uniform(ymin, ymax)
if Point(x, y).within(self.polygon):
# if this condition is true, add to a dataframe here
print(x, y)
break
obj = Agent(bounds)
obj.add_random_point()

How to turn a hand drawn curve (vector drawing or paint etc..) into a function?

I would like to be able to draw a curve like this sample, and then turn that into a function that approximates the curve. Some pseudo python code might look like
>> drawing = file.open('sample_curve.jpg')
>> approx_function = function_from_drawing(drawing, x_scale=10, y_scale=5, y_offset=3)
>> print approx_function(2.2)
5.3
I figure you might be able to pick a pixed in each column that has the line going through it (and decide to use the lowest one if there is more than one) and and then smooth that out with bezier curves. I guess what I'm wondering is what is does something like this exist already (of course it does...) and how can I integrate this with python. Also, how would I go about implementing this in python if I can't find something that is up to snuff? Would it be easier to use a vector drawing instead?
this is my preliminary hacky solution:
from PIL import Image
import numpy as np
class Pic_Function():
def __init__(self, picture_path):
self.picture = Image.open(picture_path)
self.pixels = self.picture.load()
self.columns = []
# is there really no image method to get a numpy array of pixels?
for i in range(self.picture.size[0]):
self.columns.append([self.pixels[i,j] for j in range(self.picture.size[1])])
self.first_black = []
for i in self.columns:
try:
self.first_black.append(self.picture.size[0] - i.index((0,0,0)))
except ValueError:
self.first_black.append(None)
self.max, self.min = max(self.first_black), min([j for j in self.first_black if j != None])
def at(self, x):
upper_idx = int(math.ceil(x))
lower_idx = upper_idx - 1
try:
upper = self.first_black[upper_idx]
lower = self.first_black[lower_idx]
except IndexError:
return 0
if None in [upper, lower]:
return 0
up_weight, low_weight = abs(upper-x), abs(lower-x)
return (up_weight*upper + low_weight*lower)/(up_weight + low_weight)
def norm_at(self, x, length):
un_normed = self.at(x*self.picture.size[0]/length)
return (un_normed - self.min)/self.max

How do I plot a step function with Matplotlib in Python?

This should be easy but I have just started toying with matplotlib and python. I can do a line or a scatter plot but i am not sure how to do a simple step function. Any help is much appreciated.
x = 1,2,3,4
y = 0.002871972681775004, 0.00514787917410944, 0.00863476098280219, 0.012003316194034325
It seems like you want step.
E.g.
import matplotlib.pyplot as plt
x = [1,2,3,4]
y = [0.002871972681775004, 0.00514787917410944,
0.00863476098280219, 0.012003316194034325]
plt.step(x, y)
plt.show()
If you have non-uniformly spaced data points, you can use the drawstyle keyword argument for plot:
x = [1,2.5,3.5,4]
y = [0.002871972681775004, 0.00514787917410944,
0.00863476098280219, 0.012003316194034325]
plt.plot(x, y, drawstyle='steps-pre')
Also available are steps-mid and steps-post.
New in matplotlib 3.4.0
There is a new plt.stairs method to complement plt.step:
plt.stairs and the underlying StepPatch provide a cleaner interface for plotting stepwise constant functions for the common case that you know the step edges.
This supersedes many use cases of plt.step, for instance when plotting the output of np.histogram.
Check out the official matplotlib gallery for how to use plt.stairs and StepPatch.
When to use plt.step vs plt.stairs
Use the original plt.step if you have reference points. Here the steps are anchored at [1,2,3,4] and extended to the left:
plt.step(x=[1,2,3,4], y=[20,40,60,30])
Use the new plt.stairs if you have edges. The previous [1,2,3,4] step points correspond to [1,1,2,3,4] stair edges:
plt.stairs(values=[20,40,60,30], edges=[1,1,2,3,4])
Using plt.stairs with np.histogram
Since np.histogram returns edges, it works directly with plt.stairs:
data = np.random.normal(5, 3, 3000)
bins = np.linspace(0, 10, 20)
hist, edges = np.histogram(data, bins)
plt.stairs(hist, edges)
I think you want pylab.bar(x,y,width=1) or equally pyplot's bar method. if not checkout the gallery for the many styles of plots you can do. Each image comes with example code showing you how to make it using matplotlib.
Draw two lines, one at y=0, and one at y=1, cutting off at whatever x your step function is for.
e.g. if you want to step from 0 to 1 at x=2.3 and plot from x=0 to x=5:
import matplotlib.pyplot as plt
# _
# if you want the vertical line _|
plt.plot([0,2.3,2.3,5],[0,0,1,1])
#
# OR:
# _
# if you don't want the vertical line _
#plt.plot([0,2.3],[0,0],[2.3,5],[1,1])
# now change the y axis so we can actually see the line
plt.ylim(-0.1,1.1)
plt.show()
In case someone just wants to stepify some data rather than actually plot it:
def get_x_y_steps(x, y, where="post"):
if where == "post":
x_step = [x[0]] + [_x for tup in zip(x, x)[1:] for _x in tup]
y_step = [_y for tup in zip(y, y)[:-1] for _y in tup] + [y[-1]]
elif where == "pre":
x_step = [_x for tup in zip(x, x)[:-1] for _x in tup] + [x[-1]]
y_step = [y[0]] + [_y for tup in zip(y, y)[1:] for _y in tup]
return x_step, y_step

Categories