I'm trying to plot on a QGraphicScene data which can, depending on situation, vary by orders of magnitude. Since the pen I'm using is cosmetic I'd expect the view to be independent of the magnitude of the data. But what I get instead is this:
Sine wave with noise multiplied by 50000:
Sine wave with noise multiplied by 50:
However, if I zoom in to either of these plots (same amount of zooming in both), I eventually reach a level when both images look the same:
What is going on here? Why is the width of the pen changing just because the data values are bigger. And why does the scaling disappear when zoomed in?
The code to reproduce this follows. Left clicking on the plot zooms in, right clicking zooms out.
import sys
from PyQt4 import QtGui as QG
from PyQt4 import QtCore as QC
import numpy as n
class ZoomView(QG.QGraphicsView):
"""Zoomable QGraphicsView"""
def mouseReleaseEvent(self,event):
if event.button() == QC.Qt.LeftButton:
self.scale(1.5,1)
elif event.button() == QC.Qt.RightButton:
self.scale(1/1.5,1)
class MainUI(QG.QDialog):
def __init__(self, parent=None):
super(MainUI, self).__init__(parent)
layout = QG.QVBoxLayout()
self.setLayout(layout)
button_layout = QG.QHBoxLayout()
pb3 = QG.QPushButton('add plot')
button_layout.addWidget(pb3)
layout.addLayout(button_layout)
pb3.clicked.connect(self.scene_maker_singleshot)
scene = QG.QGraphicsScene()
view = ZoomView(self)
view.setTransformationAnchor(QG.QGraphicsView.AnchorUnderMouse)
view.setRenderHint(QG.QPainter.Antialiasing)
layout.addWidget(view)
view.setScene(scene)
self.view = view
self.scene = scene
def scene_maker_singleshot(self):
"""Draw scene and fit in view"""
t1 = 50
t2 = 100
QC.QTimer.singleShot(t1, self.make_scene)
QC.QTimer.singleShot(t2, lambda: self.view.fitInView(self.view.sceneRect()))
def make_scene(self):
scale = 50
#scale = 50000
noise_amp = 0.2*scale
points = 1000
xdata = n.arange(points)
#generate sine data and random noise
ydata = n.sin(xdata/(points/10.))*scale +\
n.random.randint(noise_amp, size=points)
pen = QG.QPen(QG.QColor("red"))
for i in xrange(1, xdata.size):
self.scene.addLine(xdata[i-1], ydata[i-1], xdata[i], ydata[i], pen)
if __name__=="__main__":
app = QG.QApplication(sys.argv)
gui = MainUI()
gui.setFixedSize(500,500)
gui.show()
app.exec_()
Just like Mad Physicist told you, set the pen to cosmetic or line-width of 0 (are equal in the behavior) and you have a non-scaling pen.
>> pen.setCosmetic(True);
Related
I'm trying to plot on a QGraphicScene data which can, depending on situation, vary by orders of magnitude. Since the pen I'm using is cosmetic I'd expect the view to be independent of the magnitude of the data. But what I get instead is this:
Sine wave with noise multiplied by 50000:
Sine wave with noise multiplied by 50:
However, if I zoom in to either of these plots (same amount of zooming in both), I eventually reach a level when both images look the same:
What is going on here? Why is the width of the pen changing just because the data values are bigger. And why does the scaling disappear when zoomed in?
The code to reproduce this follows. Left clicking on the plot zooms in, right clicking zooms out.
import sys
from PyQt4 import QtGui as QG
from PyQt4 import QtCore as QC
import numpy as n
class ZoomView(QG.QGraphicsView):
"""Zoomable QGraphicsView"""
def mouseReleaseEvent(self,event):
if event.button() == QC.Qt.LeftButton:
self.scale(1.5,1)
elif event.button() == QC.Qt.RightButton:
self.scale(1/1.5,1)
class MainUI(QG.QDialog):
def __init__(self, parent=None):
super(MainUI, self).__init__(parent)
layout = QG.QVBoxLayout()
self.setLayout(layout)
button_layout = QG.QHBoxLayout()
pb3 = QG.QPushButton('add plot')
button_layout.addWidget(pb3)
layout.addLayout(button_layout)
pb3.clicked.connect(self.scene_maker_singleshot)
scene = QG.QGraphicsScene()
view = ZoomView(self)
view.setTransformationAnchor(QG.QGraphicsView.AnchorUnderMouse)
view.setRenderHint(QG.QPainter.Antialiasing)
layout.addWidget(view)
view.setScene(scene)
self.view = view
self.scene = scene
def scene_maker_singleshot(self):
"""Draw scene and fit in view"""
t1 = 50
t2 = 100
QC.QTimer.singleShot(t1, self.make_scene)
QC.QTimer.singleShot(t2, lambda: self.view.fitInView(self.view.sceneRect()))
def make_scene(self):
scale = 50
#scale = 50000
noise_amp = 0.2*scale
points = 1000
xdata = n.arange(points)
#generate sine data and random noise
ydata = n.sin(xdata/(points/10.))*scale +\
n.random.randint(noise_amp, size=points)
pen = QG.QPen(QG.QColor("red"))
for i in xrange(1, xdata.size):
self.scene.addLine(xdata[i-1], ydata[i-1], xdata[i], ydata[i], pen)
if __name__=="__main__":
app = QG.QApplication(sys.argv)
gui = MainUI()
gui.setFixedSize(500,500)
gui.show()
app.exec_()
Just like Mad Physicist told you, set the pen to cosmetic or line-width of 0 (are equal in the behavior) and you have a non-scaling pen.
>> pen.setCosmetic(True);
First time posting here so apologies if I mess up the formatting. I've just started using Jupyter today and my plan was to set up a really basic calculation where I'd define two variables using sliders (asset allocation) and two variables from Excel (Expected Returns). I'd then click a button, and calculate the weighted average return for the whole portfolio. I got quite far, but I can't seem to make the button run the calculation at the end (I get a Traceback and Object non-callable errors).
Can someone highlight where I've gone wrong? I know it is in the last paragraph somewhere!
Thanks
import ipywidgets as widgets
from bqplot import pyplot as plt
import pandas as pd
import numpy as np
#Import risk and return assumptions from Excel and assign data to variables
assumptions=pd.read_excel (r'/Users/samwreford/Documents/Assumptions.xlsx')
equityReturnAssumption=(assumptions['Expected Return'][0])
bondReturnAssumption=(assumptions['Expected Return'][1])
#Define two sliders for the asset allocation, as well as a text description and a button to calculate total return
sliderA = widgets.FloatSlider(value=1, min = 0, max = 1, step = 0.01, description = 'Equities', readout_format='.1%')
sliderB = widgets.FloatSlider(value=0, min = 0, max = 1, step = 0.01, description = 'Bonds', readout_format='.1%')
caption = widgets.HTML(value='Enter the value of Equities and Bonds')
calculate = widgets.Button(description="Calculate Return")
display(caption, sliderA, sliderB,calculate)
#Adjust sliders to make sure the asset allocation sums to 100%
def ifSliderAChanges(change):
sliderB.value = 1-sliderA.value
sliderA.observe(ifSliderAChanges, names='value')
def ifSliderBChanges(change):
sliderA.value = 1-sliderB.value
sliderB.observe(ifSliderBChanges, names='value')
#When button is clicked, calculate the total return and print to screen
def on_button_clicked(a,b):
totalReturn=a*equityReturnAssumption + b*bondReturnAssumption
print(totalReturn)
calculate.on_click(on_button_clicked(sliderA.value, sliderB.value))
I'm not sure how to pass values into a function from a Jupyter Widget button on_click. Try this instead.
#When button is clicked, calculate the total return and print to screen
def on_button_clicked(change):
totalReturn = sliderA.value * equityReturnAssumption + sliderB.value * bondReturnAssumption
print(totalReturn)
calculate.on_click(on_button_clicked)
I created an interactive scatterplot using bqplot where you are allowed to drag points around (using enable_move=True).
I don't want the user to drag points above the line y=x.
If they do, I want the point to snap back to where it was most recently.
The problem is that I'm not sure how to avoid infinite recursion here.
The scatterplot needs to be aware of when its points are moved in order to check the move and possibly snap back.
However, when it begins to snap back, this change (of the point positions) seems to trigger that same callback.
Can anyone tell me the "correct" way to deal with this basic issue?
import bqplot.pyplot as plt
import numpy as np
def on_point_move(change, scat):
if np.any(newx < scat.y):
scat.x = change['old']
fig = plt.figure(animation_duration=400)
xs = 1.0*np.arange(3) # make sure these are floats
ys = 1.0*np.arange(3)
scat = plt.scatter(xs, ys, colors=['Red'], default_size=400, enable_move=True)
scat.observe(lambda change: on_point_move(change, scat), names=['x'])
fig
You can temporarily disable the observe in the on_point_move function. I've changed the logic a bit too.
import bqplot.pyplot as plt
import numpy as np
def on_point_move(change):
if np.any(scat.x < scat.y):
scat.unobserve_all()
if change['name'] == 'x':
scat.x = change['old']
elif change['name'] == 'y':
scat.y = change['old']
scat.observe(on_point_move, names=['x','y'])
fig = plt.figure(animation_duration=400)
xs = 1.0*np.arange(3) # make sure these are floats
ys = 1.0*np.arange(3)
scat = plt.scatter(xs, ys, colors=['Red'], default_size=400, enable_move=True)
scat.observe(on_point_move, names=['x','y'])
fig
Using the Zoom Line Example I have made a Python QChartView class that can scroll with the arrow keys and zoom with the plus and minus keys. (see my code below).
When I scroll left I would expect that the grid lines and axis ticks scroll the same amount as the data. However, only the data (the QLineSeries) scrolls to the left. The 5 grid lines remain at the same positions but their tick values are updated. This is undesirable as the new tick values can be anything.
I have looked in the documentation but could not find how to make the grid scroll together with the data. Am I missing something?
I would also like to be able to set the ticks to explicit values (so that I can perhaps implement the scrolling behavior myself). Is it possible to set the axis tick values to specific values?
My example code:
import sys
from math import pi, sin, sqrt
from PyQt5.QtChart import QLineSeries, QChart, QChartView
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
class ZoomPanChartView(QChartView):
""" QChartView that can zoom/pan with the keys
"""
def __init__(self, chart):
super().__init__(chart)
self.zoomFactor = sqrt(2) # QCharts default is 2
self.panPixels = 10
def keyPressEvent(self, keyEvent):
""" Panning (scrolling) is done with the arrow keys.
Zooming goes with the plus and minus keys.
The '=' key resets.
"""
key = keyEvent.key()
if key == Qt.Key_Equal:
self.chart().zoomReset()
if key == Qt.Key_Plus:
self.chart().zoom(self.zoomFactor)
elif key == Qt.Key_Minus:
self.chart().zoom(1/self.zoomFactor)
elif key == Qt.Key_Left:
self.chart().scroll(-self.panPixels, 0)
elif key == Qt.Key_Right:
self.chart().scroll(+self.panPixels, 0)
elif key == Qt.Key_Up:
self.chart().scroll(0, +self.panPixels)
elif key == Qt.Key_Down:
self.chart().scroll(0, -self.panPixels)
elif key == Qt.Key_0:
self.chart().axisX().applyNiceNumbers() # changes the range
else:
super().keyPressEvent(keyEvent)
def main():
app = QApplication(sys.argv)
chart = QChart()
series = QLineSeries()
for i in range(0, 100):
x = i * pi / 20
y = sin(x)
series.append(x, y)
chart.addSeries(series)
chart.createDefaultAxes()
chart.axisY().setRange(-1, 1)
chart.legend().hide()
chartView = ZoomPanChartView(chart)
chartView.show()
chartView.resize(400, 300)
sys.exit(app.exec_())
if __name__ == "__main__":
main()
You can use QCategoryAxis to place ticks where you want:
initialize:
ch = self.chView.chart()
self.chartAxisX = QCategoryAxis(labelsPosition=QCategoryAxis.AxisLabelsPositionOnValue, startValue=0.0)
ch.setAxisX(self.chartAxisX)
self.chartAxisY = QCategoryAxis(labelsPosition=QCategoryAxis.AxisLabelsPositionOnValue, startValue=0.0)
ch.setAxisY(self.chartAxisY)
add series:
ch.addSeries(s)
s.attachAxis(self.chartAxisX)
s.attachAxis(self.chartAxisY)
set ticks at multiples of 5:
for s in self.chartAxisX.categoriesLabels():
self.chartAxisX.remove(s)
for i in range(0, int(max_x_value) + 1, 5):
self.chartAxisX.append(str(i), i)
self.chartAxisX.setRange(0.0, max_x_value)
or use this generic function for any interval:
def format_axis(axis, min_value, max_value, step):
for s in axis.categoriesLabels():
axis.remove(s)
axis.setStartValue(min_value)
for i in range(ceil(min_value / step), floor(max_value / step) + 1):
v = i * step
axis.append('%g' % v, v)
axis.setRange(min_value, max_value)
format_axis(self.chartAxisX, -1.1, 0.98, 0.25)
The best I could find is setting a QValueAxis as the axis on QChart and calling QValueAxis::applyNiceNumbers() to adjust the range, i.e. max and min of the current scale, so that the numbers shown are a bit more human readable. But this will alter data's position instead of gridlines' positions. You can check the function's behaviour on the horizontalBarChart example.
I thought of using a QLineSeries data-set to make the grid myself, but I would need to change the tick's positions on the axis, which, as far as I was able to determine, is not easily made with current QChart.
Short answer: you can't do it with QCharts..
I've been working with Qwt library for some time and I can attest that the grid there behaves as expected and other behaviors are a bit more mature as well. Panning moves the grip around and zooming makes the grid resize in steps to stay human-readable. Maybe it's worth checking.
IMO you can do this with QCharts and QValueAxis:
QValueAxis *axisY = new QValueAxis;
axisY->setTickType(QValueAxis::TicksDynamic);
axisY->setTickAnchor(0.0);
axisY->setTickInterval(0.2);
See e.g. Nice Label Algoritm on how to determine nice tick intervals.
i tried to express the trajectory of bullet when there is a drag force.
however, i am not able to express the graph precisely.
how to depict trajectory from ode equation?.
this is my graph. this graph does not plausible. although i struggled setting different sign of vydot value, this is not working correctly.
from pylab import*
from scipy.integrate import odeint
import matplotlib.pyplot as plt
import numpy as np
g=10
m=1
k=0.01
y=zeros([2])
vy0=0
vydot=200
vx0=0
vxdot=200
y[0]=vy0
y[1]=vydot
x=zeros([2])
x[0]=vx0
x[1]=vxdot
t=linspace(0,1000,5000)
def fy(y,t):
g0=y[1]
g1=-k*y[1]
return array([g0,g1])
def fx(z,t):
g0=-x[1]
g1=-k*(x[1])-g
return array([g0,g1])
ans1=odeint(fy,y,t)
ans2=odeint(fx,x,t)
ydata=(ans1[:,])
xdata=(ans2[:,])
plt.plot(ydata,xdata)
show()"""
In air, as opposed to liquids, the bullet not only displaces the volume along its path, but also increases the impulse of the displaced air molecules proportional to the velocity. Thus the drag force is
vn=sqrt(vx²+vy²)
dragx = -k*vn*vx
dragy = -k*vn*vy
Thus use
def f(z,t):
x,y,vx,vy = z
vn = sqrt(vx*vx+vy*vy)
return array([vx, vy, -k*vn*vx, -k*vn*vy-g ])
For a first overview, consider the problem without drag. Then the solution is
x(t) = vx*t = 200m/s*t
y(t) = vy*t-g/2*t² = 200m/s*t - 5m/s²*t²
y(t)=0 is again met for t=2*vy/g at the x coordinate 2*vx*vy/g = 8000m. Maximum height is reached for t=vy/g at height vy²/(2g)=2000m.