Welcome, guest | Sign In | My Account | Store | Cart

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.

Python, 131 lines
  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.

2 comments

David DiCarlo 7 years, 1 month ago  # | flag

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

Ronald (author) 7 years, 1 month ago  # | flag

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

Created by Ronald on Sun, 4 May 2014 (MIT)
Python recipes (4591)
Ronald's recipes (1)

Required Modules

Other Information and Tasks