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.0.0.tar.gz (32.0 kB view details)

Uploaded Source

Built Distribution

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

pyqt5_chart_widget-4.0.0-py3-none-any.whl (29.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pyqt5_chart_widget-4.0.0.tar.gz
  • Upload date:
  • Size: 32.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.8.10

File hashes

Hashes for pyqt5_chart_widget-4.0.0.tar.gz
Algorithm Hash digest
SHA256 a207d70390e6a5d02fc48c4fbb80eee6501ad79b8adb778a009971bf297c7bea
MD5 8a7701d2bf554257194748f0259af0b1
BLAKE2b-256 446262743d685035f8200f69d8500778967b07e702a81b54a1e20ba1f435393b

See more details on using hashes here.

File details

Details for the file pyqt5_chart_widget-4.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pyqt5_chart_widget-4.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3c93b19ba8d79dd805fd76974a5d7d7a4ec03f1e085ff11f602144ae510e9d7a
MD5 1540ef308d1536692ab2711a12a7d17f
BLAKE2b-256 5519f16569fb28d243c95291779273c78310ba55637f786a8ec8bacaa4d5c959

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