This is a very simple example of how to implement a Strip Chart in Tkinter. It's somewhat slow. I've done a few things to help with performance but more could probably be done.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | # (c) MIT License Copyright 2014 Ronald H Longo
# Please reuse, modify or distribute freely.
from collections import OrderedDict
import tkinter as tk
class StripChart( tk.Frame ):
def __init__( self, parent, scale, historySize, trackColors, *args, **opts ):
# Initialize
super().__init__( parent, *args, **opts )
self._trackHist = OrderedDict() # Map: TrackName -> list of canvas objID
self._trackColor = trackColors # Map: Track Name -> color
self._chartHeight = scale + 1
self._chartLength = historySize * 2 # Stretch for readability
self._canvas = tk.Canvas( self, height=self._chartHeight + 17,
width=self._chartLength, background='black' )
self._canvas.grid( sticky=tk.N+tk.S+tk.E+tk.W )
# Draw horizontal to divide plot from tick labels
x, y = 0, self._chartHeight + 2
x2, y2 = self._chartLength, y
self._baseLine = self._canvas.create_line( x, y, x2, y2, fill='white' )
# Init track def and histories lists
self._trackColor.update( { 'tick':'white', 'tickline':'white',
'ticklabel':'white' } )
for trackName in self._trackColor.keys():
self._trackHist[ trackName ] = [ None for x in range(historySize) ]
def plotValues( self, **vals ):
for trackName, trackHistory in self._trackHist.items():
# Scroll left-wards
self._canvas.delete( trackHistory.pop(0) )
# Remove left-most canvas objs
self._canvas.move( trackName, -2, 0 )
# Scroll canvas objs 2 pixels left
# Plot the new values
try:
val = vals[ trackName ]
x = self._chartLength
y = self._chartHeight - val
color = self._trackColor[ trackName ]
objId = self._canvas.create_line( x, y, x+1, y, fill=color,
width=3, tags=trackName )
trackHistory.append( objId )
except:
trackHistory.append( None )
def drawTick( self, text=None, **lineOpts ):
# draw vertical tick line
x = self._chartLength
y = 1
x2 = x
y2 = self._chartHeight
color = self._trackColor[ 'tickline' ]
objId = self._canvas.create_line( x, y, x2, y2, fill=color,
tags='tick', **lineOpts )
self._trackHist[ 'tickline' ].append( objId )
# draw tick label
if text is not None:
x = self._chartLength
y = self._chartHeight + 10
color = self._trackColor[ 'ticklabel' ]
objId = self._canvas.create_text( x, y, text=text,
fill=color, tags='tick' )
self._trackHist[ 'ticklabel' ].append( objId )
def configTrackColors( self, **trackColors ):
# Change plotted data color
for trackName, colorName in trackColors.items( ):
self._canvas.itemconfigure( trackName, fill=colorName )
# Change settings so future data has the new color
self._trackColor.update( trackColors )
if __name__ == '__main__':
top = tk.Tk( )
graph = StripChart( top, 100, 300, { 'A':'blue', 'B':'green', 'C':'red' } )
graph.grid( )
val_A = 0
val_B = 0
val_C = 0
delta = [ -3, -2, -1, 0, 1, 2, 3 ] # randomly vary the values by one of these
tickCount = 0
def nextVal( current, lowerBound, upperBound ):
from random import choice
current += choice( delta )
if current < lowerBound:
return lowerBound
elif current > upperBound:
return upperBound
else:
return current
def plotNextVals( ):
global val_A, val_B, val_C, tickCount
if tickCount % 50 == 0:
graph.drawTick( text=str(tickCount), dash=(1,4) )
tickCount += 1
val_A = nextVal( val_A, 0, 99 )
val_B = nextVal( val_B, 0, 99 )
val_C = nextVal( val_C, 0, 99 )
graph.plotValues( A=val_A, B=val_B, C=val_C )
#changeColor = { 800: 'black',
#1200: 'yellow',
#1600: 'orange',
#2000: 'white',
#2400: 'brown',
#2800: 'blue' }
#if tickCount in changeColor:
#graph.configTrackColors( A=changeColor[tickCount] )
top.after( 1, plotNextVals )
top.after( 1, plotNextVals )
top.mainloop( )
|
A StripChart object is instantiated much like any Tkinter widget. The scale argument is the plot area height in pixels (e.g. a value of 100 will insure that there are exactly 100 distinct pixels for data points in a given column of the chart. An additional 18 characters of height is added along the bottom of the drawn widget to allow for tick labels. This historySize constructor argument determines how many history points will remain on the displayed chart. Actual plot points are separated by a pixel. So the width of the drawn widget is twice the historySize. Track colors is a dictionary of track names (strings) mapped to tkinter colors. A track is a distinct set of data values. In the example code I plot three tracks on the strip chart each a different color.
drawTick(). Periodically one might wish to drop a vertical on the chart and provide a label (e.g. every 5 seconds). Supply drawTick() a label and a line style (Tkinter dash-line specification) and it will draw a tick with a designated label at the bottom of the chart.
plotValues(). Provide a set of values to plot in the form of keyed arguments. Arguments have the form trackName=value. The chart will scroll all current plotted values to the left by two pixels then plot the new values in the right-most column of the chart.
configTrackColors(). Provide a set of key value pairs to re-configure track colors. Arguments have the form trackName=color. This allows you to change a track color at any time (all existing and new values will immediately have the new color). Changing the color to 'black', hides the track data.
Example Code. The example code generates random data for three tracks 'A', 'B', and 'C'. Uncomment the bit of code in plotNextVals() to see how it looks to change a track's color after data has been plotted.
Hope you find this useful.
2017.03.10
- Repaired one syntax error.
- Added Copyright to the code.
- Modified the description to be more clear.
Hello Ronald-
I found your code and it was exactly what I was looking for. My application: a visual running strip chart of current data from a target board I'm measuring. It worked straight away as-is. I had fun adding in pyserial so I could pull the data in via a USB serial port. Even more fun, since I've been avoiding Python3 - I really don't care for the new print statements at all.
Anyway, I would like to eventually publish this code when I write an application node for the current measurement board I've put together. After consulting my local licence guru, we see that this post indicates that the code is licensed under MIT, but there is no copyright information that we can find here. It also doesn't include your last name.
Would you please consider revising the post and code to add in a more explicit license and copyright statement? Such as:
MIT License Copyright 2014 Ronald <lastname>
thanks, david
David,
Thanks for your input. I'm so glad you have found a use for this recipe. I've made several modifications including your suggestion. Please enjoy.
Ron