The Problem
Tkinter ships with enough widgets to build a functional desktop GUI in an afternoon.
What it doesn't ship with is any built-in way to display data that changes over time.
If you're building a CPU monitor, a sensor dashboard, or any tool that needs to
visualize a live stream of values, you have two realistic options: embed matplotlib
in a FigureCanvasTkAgg, or roll your own canvas drawing logic. The first option
works but pulls in a dependency that's larger than most projects need. The second
option means rebuilding the same axis, scaling, and rendering logic every time.
Why Existing Solutions Didn't Cut It
The matplotlib-in-Tkinter approach is the most commonly recommended solution, and
it's fine for static charts. For live data though, it has friction:
- You manage figure/canvas lifecycle manually.
- Animation via
FuncAnimationfights with Tkinter's event loop unless you're careful withblit=Trueand backend selection. - The import footprint (
numpy,matplotlib) is heavy for an app whose chart is a minor feature.
The other option — drawing on a tk.Canvas directly — is fine at small scale but
requires you to reimplement axis labels, scaling, grid lines, and multi-line
coordination every single time.
What I wanted: a LineChart class I could drop into any Tkinter app the same way
I'd drop in a ttk.Treeview. Create it, pack it, feed it data. Done.
How It Works
tkchart exposes two classes: LineChart (the widget) and Line (a data series
attached to a chart).
import tkchart
chart = tkchart.LineChart(
master=root,
x_axis_values=("t-9", "t-8", "t-7", "t-6", "t-5",
"t-4", "t-3", "t-2", "t-1", "t"),
y_axis_values=(0, 1000),
y_axis_section_count=5,
x_axis_section_count=10,
)
chart.pack(pady=10)
line = tkchart.Line(
master=chart,
color="#5dffb6",
size=2,
style="dashed",
style_type=(10, 5),
fill="enabled",
)
LineChart owns the canvas, axes, labels, and grid. Line is a lightweight
descriptor — it holds style properties and a data buffer, but the chart controls
all rendering.
Feeding data happens via show_data():
def stream():
while True:
chart.show_data(line=line, data=[random.randint(0, 1000)])
time.sleep(0.5)
threading.Thread(target=stream, daemon=True).start()
This is designed to be called from a background thread. Internally, canvas
operations are dispatched to the main thread via Tkinter's after() mechanism —
the caller doesn't have to think about it.
Key architectural decisions:
Decoupled
LinefromLineChart: EachLinemaintains its own data
buffer independently.get_line_data(),get_current_visible_data(), and
related methods let you query what's on screen at any point — useful for
triggering alerts or logging snapshots.Scrolling X-axis: As data arrives, the X-axis label set scrolls. The
x_axis_valuestuple defines the visible label template, not a fixed dataset.
This means the chart is conceptually infinite on the time axis.Runtime reconfiguration: v2.2.0 added
configure_*()methods for almost
every visual property. You can change axis colors, pointer behavior, or
line fill at runtime without destroying and recreating the widget.
chart.configure_bg_color("#1a1a2e")
line.configure_color("#ff6b9d")
line.configure_fill("enabled")
- Pointer with callback: An optional hover pointer shows interpolated values at cursor position and fires a user-supplied callback function — so you can wire it to a label or trigger an action based on which data point is hovered.
One Thing That Surprised Me
The show_data() call accepts a list, not a single value. I intended this to
support batch inserts — you can push multiple data points in one call, and the
chart will render them all in sequence.
The tricky part: when multiple Line objects share the same LineChart, their
data lengths need to stay synchronized for the X-axis to remain coherent. The
chart uses the maximum data length across all lines as its internal clock. If one
line accumulates data faster than another, the slower line's visible portion gets
padded implicitly.
This means callers have to be deliberate about calling show_data() at consistent
rates across all lines if they want correct synchronization. It works well when all
lines are driven from the same loop (the common case), but it's a real footgun if
you have two independent threads pushing to two separate lines at different intervals.
I haven't found a clean solution that doesn't add per-line timestamps and complicate
the rendering model significantly. For now, the docs recommend keeping all
show_data() calls inside a single loop.
What's Next
-
Bar chart support: The
LineChartarchitecture is canvas-based enough that adding aBarChartclass is feasible. The axis and label system could be shared. -
Export: A method to snapshot the current canvas state to a PNG. The
tk.Canvas.postscript()method gets close but requires an extra conversion step. -
Typed stubs: The codebase predates type hints. Adding
.pyistub files would make autocomplete and mypy integration much better.
Call to Action
The design decision I'm least certain about: the Line-as-descriptor pattern
where Line holds style but LineChart owns all rendering. It keeps the rendering
logic centralized, but it means Line objects are inert outside the context of their
parent chart.
An alternative would be to make Line a proper canvas actor that draws itself —
closer to how matplotlib's Artist hierarchy works. That would allow lines to be
moved between charts, but it would also scatter the rendering logic.
If you've designed a similar multi-series chart component — in any language or
framework — I'd genuinely like to hear which pattern held up better over time:
centralized renderer or autonomous actors.
GitHub: https://github.com/thisal-d/tkchart
PyPI: https://pypi.org/project/tkchart/
PyPI: pip install tkchart


Top comments (1)
GitHub: github.com/thisal-d/tkchart
PyPI: pypi.org/project/tkchart/