Skip to main content

Lightweight interactive chart widget for PyQt5 with built-in approximation, multi-series, and extensible fit modes

Project description

pyqt5-chart-widget

Zero dependencies beyond PyQt5. Drop the package in, plot your data. No matplotlib, no numpy — ~1000 lines total.

Originally built for an industrial powder feeder control application where a calibration curve needed to render fast, look clean, and not require importing half of scipy.

image

Install

pip install pyqt5-chart-widget

Python >= 3.8, PyQt5 >= 5.15.


Quickstart

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from pyqt5_chart_widget import ChartWidget

app = QApplication(sys.argv)
win = QMainWindow()

chart = ChartWidget()
chart.setLabel("left",   "Flow, g/min")
chart.setLabel("bottom", "RPM")

xs = [i * 100 for i in range(20)]
ys = [x * 0.05 + x ** 1.1 * 0.0003 for x in xs]

line = chart.plot(label="Pump A")
line.setData(xs=xs, ys=ys)

scatter = chart.addScatter(size=10, label="Calibration")
scatter.setData(x=xs[::3], y=ys[::3])

fit = chart.addFit(line, mode_key="poly3", label="Polynomial fit")

win.setCentralWidget(chart)
win.resize(900, 500)
win.show()
sys.exit(app.exec_())

ChartWidget

ChartWidget(
    parent=None,
    show_toolbar:  bool = True,
    show_legend:   bool = False,
    show_sidebar:  bool = False,
    font:          QFont | None = None,
    threaded_fit:  bool = False,
    grid_px_x:        int = 7,
    grid_px_y:        int = 7,
)

threaded_fit=True moves fit computation off the main thread. Useful for large datasets or slow spline fits. Leave it False if you can't create additional threads — some embedded runtimes and multiprocessing workers don't allow it.

Method Description
plot(color, width, label, dashed) Add a line series → _LineItem
addScatter(size, color, label) Add a scatter series → _ScatterItem
addFit(source, mode_key, color, width, dashed, label) Add a fit curve for a series → _FitItem
addLine(y=, x=, color, width, dashed) Add an infinite reference line → _InfLine
setLabel(side, text) Set axis label — "left" or "bottom"
autofit() Fit view to current data bounds
setAutofitEnabled(bool) Enable/disable auto-fit when data changes (default: True)
setLatestPointVisible(bool) Show/hide latest-point badges on axis rulers
setThreadedFit(bool) Switch threaded fit computation on/off at runtime
setGridDensity(x, y) Update grid line count on both axes at runtime
setToolbarVisible(visible) Show/hide the toolbar
setSidebarVisible(visible) Show/hide the sidebar
setLegendVisible(visible) Show/hide the legend overlay
sidebar() Return the SidebarLabel instance (or None)
removeItem(item) Remove any series or line
clearAll() Remove all series
refreshFitMenu() Rebuild the fit-mode dropdown (call after register_fit_mode)
exportCsv() Open save dialog, write all series to CSV
exportImage() Open save dialog, save canvas as PNG
grabImage() Return QPixmap of the canvas without a dialog

Color arguments accept any string QColor understands: "#e74c3c", "red", "rgb(200,100,50)". Omit color to get one from the auto-palette.


Series

line = chart.plot(label="Sensor A")
line.setData(xs=[...], ys=[...])
line.setVisible(True)
line.setRawVisible(False)   # hide raw data points; keep the fit curve visible
line.setLabel("New label")

scatter = chart.addScatter(size=10, label="Samples")
scatter.setData(x=[...], y=[...])
scatter.setRawVisible(False)

fit = chart.addFit(line, mode_key="spline", label="Spline fit")
fit.setModeKey("poly3")     # change algorithm live
fit.setVisible(False)

hline = chart.addLine(y=100.0, color="#f39c12", dashed=True)
hline.setValue(150.0)
hline.setVisible(True)

setRawVisible(False) hides the raw line or scatter while leaving any associated fit curve untouched. Use this when you only want to show the approximation.


Getting fit data out of the chart

All three methods run synchronously regardless of whether threaded_fit is on. They are safe to call from any code path that needs the numbers.

getData — raw lists

fit = chart.addFit(line, mode_key="poly3")
line.setData(xs=[...], ys=[...])

xs, ys = fit.getData()
# 400-point dense evaluation across the source data's x range.
# Both xs and ys are plain Python lists of floats.

xs, ys = fit.getData(x_lo=0.0, x_hi=500.0, n_pts=1000)
# Explicit range and resolution.

asDict — dictionary form

result = fit.asDict()
# {"x": [...], "y": [...]}

result = fit.asDict(x_lo=0.0, x_hi=500.0, n_pts=200)

Useful when passing data to JSON serialization or pandas:

import pandas as pd
df = pd.DataFrame(fit.asDict())

asTuples — list of (x, y) pairs

points = fit.asTuples()
# [(x0, y0), (x1, y1), ...]

points = fit.asTuples(x_lo=0.0, x_hi=500.0, n_pts=200)

Convenient for iteration, table rendering, or writing to a file:

for x, y in fit.asTuples():
    print(f"{x:.3f}  {y:.3f}")

evaluate — single point or arbitrary x values

y = fit.evaluate(250.0)
# Returns a single float — the model's prediction at x=250.

ys = fit.evaluate([0.0, 100.0, 200.0, 300.0])
# Returns a list of floats, one per input x.
# Returns None (scalar) or [None, ...] (list) if the fit cannot be computed.

Use evaluate when you need the model's output at specific x values that don't follow a regular grid:

sensor_readings = [12.4, 87.1, 143.9, 210.0]
predicted = fit.evaluate(sensor_readings)

Built-in fit modes

Key Method Min points
linear_origin y = kx (through origin) 1
linear Ordinary least-squares 2
poly2 Polynomial degree 2 2
poly3 Polynomial degree 3 2
poly4 Polynomial degree 4 2
pchip Piecewise Cubic Hermite (monotone) 2
spline Natural cubic spline 2

Custom fit modes

from pyqt5_chart_widget import FitMode, register_fit_mode

def my_fit(x_pts, y_pts, x_eval):
    # x_pts / y_pts are pre-sorted and de-duplicated before this is called.
    # Return a list of floats with the same length as x_eval.
    k = sum(xi * yi for xi, yi in zip(x_pts, y_pts)) / sum(xi**2 for xi in x_pts)
    return [k * xi for xi in x_eval]

register_fit_mode(FitMode("my_fit", "My custom fit", my_fit, min_points=2))

# If the chart is already on screen, rebuild the dropdown:
chart.refreshFitMenu()

Threaded fit computation

For large datasets or expensive spline fits, pass threaded_fit=True:

chart = ChartWidget(threaded_fit=True)

Or flip it at runtime:

chart.setThreadedFit(True)

While a fit is computing, the canvas shows the previous cached result. The update arrives asynchronously and triggers a repaint automatically. If you zoom or pan quickly, the pending computation is queued and runs once the in-flight worker finishes.

Leave threaded_fit=False (the default) if creating threads is not allowed in your environment — some embedded runtimes, multiprocessing child processes, and certain test frameworks don't support it.


Grid density

Control how many grid lines appear on each axis:

chart = ChartWidget(grid_x=5, grid_y=4)

# Change at runtime:
chart.setGridDensity(x=10, y=8)

The numbers are targets, not hard counts. nice_ticks rounds them to human-readable values, so the actual line count may be off by one. Minimum on either axis is 2.


Auto-fit toggle

By default, the view refits whenever you push new data via setData. Disable it to manage the viewport yourself:

chart.setAutofitEnabled(False)
chart.autofit()  # still works on demand

The toolbar has an Auto-fit toggle button for this. Double-clicking the chart always triggers a fit regardless of the setting.


Latest-point markers

chart.setLatestPointVisible(True)

Or use the Latest button in the toolbar. Each visible series gets a small colored badge on the X and Y axis rulers showing the last value in the series. Handy when streaming live data and you need to read the current value without hovering.


Crosshair and hover

Hover over the plot area to get a crosshair, a snap dot on the nearest point, and an X/Y tooltip. The snap works on:

  • Individual scatter points
  • Interpolated positions along line segments (not just the data points)
  • Interpolated positions on fit curves

A tangent line is drawn at the snap point for line series and fit curves.


Sidebar

chart = ChartWidget(show_sidebar=True)
sb = chart.sidebar()

sb.addLabel("Controls")
sb.addSeparator()
sb.addButton("Toggle line", lambda: line.setVisible(not line.visible))
sb.addButton("Export CSV",  chart.exportCsv, tooltip="Save data")
sb.clear()

Legend

chart = ChartWidget(show_legend=True)
# or later:
chart.setLegendVisible(True)

Only series that have a label and are visible appear in the legend.


Color palette

from pyqt5_chart_widget import set_palette, reset_colors

set_palette(["#ff0000", "#00ff00", "#0000ff"])  # replace the built-in palette
reset_colors()                                    # restart the auto-color index

Internationalization

from pyqt5_chart_widget import set_tr, update_strings

# Hook into your own translation system:
set_tr(lambda key: my_translations.get(key, key))

# Or patch individual strings:
update_strings({
    "chart_widget.btn_fit":      "Fit view",
    "chart_widget.btn_csv":      "Save CSV",
    "chart_widget.btn_autofit_toggle": "Live fit",
    "chart_widget.btn_latest":   "Now",
})

Interaction

Action Result
Scroll wheel Zoom in/out centered on cursor
Left drag Pan
Double-click Reset view to data bounds
Hover Crosshair + snap to nearest point/curve + tangent

Zoom is clamped to a safe range — no matter how far in or out you scroll, the widget will not hang or crash.


Module layout

pyqt5_chart_widget/
├── __init__.py       — public API re-exports
├── chart_widget.py   — ChartWidget, toolbar
├── canvas.py         — rendering, interaction, hover
├── items.py          — _LineItem, _ScatterItem, _FitItem, _InfLine
├── math_utils.py     — FitMode, built-in fitters, register_fit_mode, nice_ticks
├── sidebar.py        — SidebarLabel
├── palette.py        — auto-color palette
└── i18n.py           — tr(), set_tr(), update_strings()

Why not matplotlib or pyqtgraph?

Matplotlib embedded in Qt is slow to start, awkward to theme, and brings in a lot of machinery for one calibration curve. Pyqtgraph is excellent but requires numpy as a hard dependency. This widget is ~1000 lines, has zero runtime dependencies beyond PyQt5, and does exactly what the name says.


License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyqt5_chart_widget-4.1.1.tar.gz (34.9 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

pyqt5_chart_widget-4.1.1-cp313-cp313-win_amd64.whl (97.0 kB view details)

Uploaded CPython 3.13Windows x86-64

pyqt5_chart_widget-4.1.1-cp38-cp38-win_amd64.whl (101.9 kB view details)

Uploaded CPython 3.8Windows x86-64

File details

Details for the file pyqt5_chart_widget-4.1.1.tar.gz.

File metadata

  • Download URL: pyqt5_chart_widget-4.1.1.tar.gz
  • Upload date:
  • Size: 34.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.12

File hashes

Hashes for pyqt5_chart_widget-4.1.1.tar.gz
Algorithm Hash digest
SHA256 dba9221f05a3b460daf0012ea3fc4a9ee483b0c85486c89f2a00b24382ad9413
MD5 eb6d7816499e0c6da543939c7f78fbb0
BLAKE2b-256 35e297a6d8af444001abe598c52bff1d2a03851b58f6d07d62df76581890d304

See more details on using hashes here.

File details

Details for the file pyqt5_chart_widget-4.1.1-cp313-cp313-win_amd64.whl.

File metadata

File hashes

Hashes for pyqt5_chart_widget-4.1.1-cp313-cp313-win_amd64.whl
Algorithm Hash digest
SHA256 a681d58841144e0b1fb1ca1092bdb436a4e238874faea52bcd7be98d1ce6cdef
MD5 35106d58868ff411850b2bdfe08384ed
BLAKE2b-256 012ad03267e3054a0cb55efbb8a18d2b3194c0e1cd185681cbe771677b538b53

See more details on using hashes here.

File details

Details for the file pyqt5_chart_widget-4.1.1-cp38-cp38-win_amd64.whl.

File metadata

File hashes

Hashes for pyqt5_chart_widget-4.1.1-cp38-cp38-win_amd64.whl
Algorithm Hash digest
SHA256 78cd6462ccbe2ff95999361ebf8c4a9cebd16cf0df2d8859d3b3ebdfc34a94dd
MD5 9cb34ede02d7e15d48203233aea4f9f8
BLAKE2b-256 01c119b7a3413025c8be031e460a17b42e6771cb205bbf2bab743f6e725ec915

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page