How can I plot two data series with completely different scales on the same PyQtGraph plot, using a left Y-axis for one and a right Y-axis for the other?
When you're working with data that has two different scales — say temperature and humidity, or voltage and current — plotting both series on a single Y-axis can make one of them look flat and unreadable. The solution is to use two Y-axes: one on the left for one data series, and one on the right for the other.
PyQtGraph supports this through its ViewBox system, which lets you overlay a second axis with its own independent scale. In this tutorial, we'll walk through how to set this up from scratch.
A simple single-axis plot
Let's start with a basic PyQtGraph plot showing a single data series. This gives us a foundation to build on.
import sys
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets
app = QtWidgets.QApplication(sys.argv)
win = pg.GraphicsLayoutWidget(show=True, title="Two Y-Axes Example")
win.resize(800, 400)
plot = win.addPlot()
plot.setLabel("left", "Series 1")
plot.setLabel("bottom", "Time")
x = np.linspace(0, 10, 100)
y1 = np.sin(x) * 10 # Values range roughly -10 to 10
plot.plot(x, y1, pen="r")
sys.exit(app.exec())
This creates a window with a single plot and a red sine wave. The left Y-axis is labeled "Series 1" and the bottom axis shows "Time".
Now let's add a second data series — but one that lives on a completely different scale.
Adding a second Y-axis
To add a second Y-axis on the right side of the plot, we need to do a few things:
- Create a new
ViewBoxto hold the second data series. - Link that
ViewBoxto the right-hand axis of the existing plot. - Make sure both views stay synchronized when the plot is resized or the X-axis is panned.
Here's the full working example:
import sys
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets
app = QtWidgets.QApplication(sys.argv)
win = pg.GraphicsLayoutWidget(show=True, title="Two Y-Axes Example")
win.resize(800, 400)
# Create the main plot with the left Y-axis.
plot = win.addPlot()
plot.setLabel("left", "Series 1 (sin)", color="r")
plot.setLabel("bottom", "Time")
# Generate some sample data.
x = np.linspace(0, 10, 100)
y1 = np.sin(x) * 10 # Range: roughly -10 to 10
y2 = np.cos(x) * 1000 # Range: roughly -1000 to 1000
# Plot the first series on the main plot (left Y-axis).
plot.plot(x, y1, pen="r")
# Show the right axis and give it a label.
plot.showAxis("right")
plot.setLabel("right", "Series 2 (cos)", color="b")
# Create a new ViewBox for the second series.
viewbox2 = pg.ViewBox()
plot.scene().addItem(viewbox2)
plot.getAxis("right").linkToView(viewbox2)
viewbox2.setXLink(plot)
# Create a PlotCurveItem for the second series and add it to the new ViewBox.
curve2 = pg.PlotCurveItem(x, y2, pen="b")
viewbox2.addItem(curve2)
# Keep the second ViewBox geometry in sync with the main plot.
def update_views():
viewbox2.setGeometry(plot.vb.sceneBoundingRect())
viewbox2.linkedViewChanged(plot.vb, viewbox2.XAxis)
plot.vb.sigResized.connect(update_views)
update_views()
sys.exit(app.exec())
Run this and you'll see a red sine wave scaled to the left Y-axis (roughly ±10) and a blue cosine wave scaled to the right Y-axis (roughly ±1000). Both share the same X-axis, but each has its own independent vertical scale.
Let's walk through what each part is doing.
How it works
Showing the right axis
By default, PyQtGraph plots only display the left and bottom axes. We need to explicitly show the right axis:
plot.showAxis("right")
plot.setLabel("right", "Series 2 (cos)", color="b")
This makes the right axis visible and gives it a label. The color parameter tints the label text, which is a nice way to visually link each axis to its corresponding data series.
Creating a second ViewBox
A ViewBox in PyQtGraph is the area that manages the coordinate system and handles panning and scaling for a set of items. The main plot already has its own ViewBox (accessible as plot.vb). To get an independent Y-axis, we need a second one:
viewbox2 = pg.ViewBox()
plot.scene().addItem(viewbox2)
We add the new ViewBox directly to the plot's scene so it renders in the same graphical space.
Linking the axis and the X-axis
Next, we connect the right axis to our new ViewBox, and we link the X-axis so both views scroll and zoom together horizontally:
plot.getAxis("right").linkToView(viewbox2)
viewbox2.setXLink(plot)
linkToView tells the right axis to display tick values based on the coordinate system of viewbox2. setXLink ensures that when you pan or zoom along the X-axis on the main plot, the second ViewBox follows along.
Keeping the geometry in sync
When the window is resized, the main plot's ViewBox changes size — but our second ViewBox won't automatically follow. We need to update it manually:
def update_views():
viewbox2.setGeometry(plot.vb.sceneBoundingRect())
viewbox2.linkedViewChanged(plot.vb, viewbox2.XAxis)
plot.vb.sigResized.connect(update_views)
update_views()
The sigResized signal fires whenever the main ViewBox changes size. Our update_views function then sets the second ViewBox to the same geometry. The call to linkedViewChanged ensures the X-axis link is properly refreshed.
We also call update_views() once immediately to set the correct initial geometry. Without this, the second series may not appear in the right position until the first resize event occurs.
Adding data to the second ViewBox
We can't use plot.plot() for the second series because that would add it to the main ViewBox (and the left Y-axis). Instead, we create a PlotCurveItem and add it to viewbox2 directly:
curve2 = pg.PlotCurveItem(x, y2, pen="b")
viewbox2.addItem(curve2)
PlotCurveItem is the same type of item that plot.plot() creates under the hood. By adding it to viewbox2, the data is scaled according to the right Y-axis.
Color-coding the axes
To make it immediately clear which axis belongs to which series, it helps to color the axis tick marks to match the pen color of the corresponding line. We already set the label color above, but we can also style the axis ticks themselves:
plot.getAxis("left").setPen("r")
plot.getAxis("right").setPen("b")
Add these lines after the axis setup and both axes will be tinted to match their data series. This small visual detail makes dual-axis plots much easier to read at a glance.
Complete working example
Here's the full example with color-coded axes and everything wired up:
import sys
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets
app = QtWidgets.QApplication(sys.argv)
win = pg.GraphicsLayoutWidget(show=True, title="Two Y-Axes Example")
win.resize(800, 400)
# Create the main plot.
plot = win.addPlot()
plot.setLabel("left", "Series 1 (sin)", color="r")
plot.setLabel("right", "Series 2 (cos)", color="b")
plot.setLabel("bottom", "Time")
# Show the right axis.
plot.showAxis("right")
# Color-code the axes to match the data series.
plot.getAxis("left").setPen("r")
plot.getAxis("right").setPen("b")
# Generate sample data with very different scales.
x = np.linspace(0, 10, 100)
y1 = np.sin(x) * 10 # Left axis: range ~ -10 to 10
y2 = np.cos(x) * 1000 # Right axis: range ~ -1000 to 1000
# Plot series 1 on the main (left) Y-axis.
plot.plot(x, y1, pen="r")
# Create a second ViewBox for series 2.
viewbox2 = pg.ViewBox()
plot.scene().addItem(viewbox2)
plot.getAxis("right").linkToView(viewbox2)
viewbox2.setXLink(plot)
# Add series 2 to the second ViewBox.
curve2 = pg.PlotCurveItem(x, y2, pen="b")
viewbox2.addItem(curve2)
# Keep the ViewBox geometries synchronized.
def update_views():
viewbox2.setGeometry(plot.vb.sceneBoundingRect())
viewbox2.linkedViewChanged(plot.vb, viewbox2.XAxis)
plot.vb.sigResized.connect(update_views)
update_views()
sys.exit(app.exec())
You can pan and zoom the plot, and both series will stay aligned along the X-axis while maintaining their own independent Y-axis scales. Try changing the data to use your own values — the technique works the same way regardless of what you're plotting.
This approach extends naturally if you need to add more axes or overlay additional data series. Each additional series just needs its own ViewBox, linked to an axis and kept in sync through the same update_views pattern.
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick
(PyQt6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!