class at_graph(Frame): def __init__(self, parent): Frame.__init__(self, parent) self.parent = parent self.u = utils('atoutput.pkl') self.km = dict() self.price = dict() self.km[0] = (min(self.u.all_km), max(self.u.all_km)) self.price[0] = (min(self.u.all_price), max(self.u.all_price)) self.zoom_level = 0 try: self.parent.title("Auto trader results") self.is_standalone = True except: self.is_standalone = False self.style = Style() self.style.theme_use("classic") # Assume the parent is the root widget; make the frame take up the # entire widget size. print self.is_standalone if self.is_standalone: self.w, self.h = map(int, self.parent.geometry().split('+')[0].split('x')) self.w, self.h = 800, 800 else: self.w, self.h = 600, 600 self.c = None # Are they hovering over a data point? self.is_hovering = False # Filter the description strings: lower and whiten any non-matching # data point. self.filter = '' self.re = list() self.replot() def replot(self, zlfrac=None): """Replot the graph. If zlfrac is not None, then it should be a fractional value between 0 and 1; this is used to do smooth zooming, which doesn't plot the axes (it only redraws the car points).""" if self.c is not None: self.c.destroy() self.c = Canvas(self, height=self.h, width=self.w, bd=1, bg='#f3f5f9') self.c.grid(sticky=S, pady=1, padx=1) zl = self.zoom_level if zlfrac is not None: z1l, z1h = self.zoom_price_start z2l, z2h = self.zoom_price_end price_low = z1l + (z2l - z1l) * zlfrac price_high = z1h + (z2h - z1h) * zlfrac z1l, z1h = self.zoom_km_start z2l, z2h = self.zoom_km_end km_low = z1l + (z2l - z1l) * zlfrac km_high = z1h + (z2h - z1h) * zlfrac self.axis((price_low, price_high), 'y', draw=False) self.axis((km_low, km_high), 'x', draw=False) self.car_points(draw=False) else: self.axis(self.price[zl], 'y') self.axis(self.km[zl], 'x') self.car_points() self.pack(fill=BOTH, expand=1) def xyp(self, x, y): "Given x in km and y in $, return canvas position (xp, yp)." xp = int(math.floor((1.0 * x - self.x1) / (self.x2 - self.x1) \ * (self.xp2 - self.xp1) + self.xp1 + 0.5)) yp = int(math.floor((1.0 * y - self.y1) / (self.y2 - self.y1) \ * (self.yp2 - self.yp1) + self.yp1 + 0.5)) return (xp, yp) def axis(self, arange, ax, draw=True): "Add an axis ax='x' or 'y', with arange=(min, max) values." if draw: a1, a2, ast = self.u.axis(*arange) else: a1, a2 = arange ast = (a2 - a1) * 0.2 nt = int(math.floor((a2 - a1) / ast + 0.5)) + 1 st_offset = 50 # Remember the min and max axis values, along with the canvas points # that correspond to each location (xp1 and xp2). This allows us to # calculate where on the canvas a particular (x, y) value falls. if ax == 'x': self.x1, self.x2 = a1, a2 self.xp1, self.xp2 = st_offset, self.w - st_offset self.xtick = [a1 + i * ast for i in range(nt)] # Remember where the midpoint of the axis is, relative to canvas. self.xmid = (self.xp1 + self.xp2) / 2 else: self.y1, self.y2 = a1, a2 self.yp1, self.yp2 = self.h - st_offset, st_offset self.ytick = [a1 + i * ast for i in range(nt)] # Remember where the midpoint of the axis is, relative to canvas. self.ymid = (self.yp1 + self.yp2) / 2 # Figure out tick labels. atick = ['%g' % ((a1 + i * ast) / 1000) for i in range(nt)] # Figure out maximum decimal places on all tick labels, and ensure # they all have that many decimal places. max_dec = max(map(lambda x: 0 if '.' not in x else len(x.split('.')[1]), atick)) if max_dec > 0: atick = map(lambda x: x + '.' + '0'*max_dec if '.' not in x else x + '0'*(max_dec - len(x.split('.')[1])), atick) yst, xst = self.h - st_offset, st_offset # Draw axis line proper, and axis label. if draw: if ax == 'x': self.c.create_line(xst, yst, self.w - st_offset, yst) xp = (xst + self.w - st_offset) / 2 self.c.create_text(xp, yst + 30, text='Mileage (km x 1000)') else: self.c.create_line(xst, yst, xst, st_offset) self.c.create_text(xst, st_offset - 30, text='Price') self.c.create_text(xst, st_offset - 15, text='($000)') tick_anchor = [N, E][ax == 'y'] tick_x, tick_y = xst, yst tick_step = ([self.w, self.h][ax == 'y'] - st_offset * 2 * 1.0) / \ (nt - 1) label_offset = 3 for i1, tick in enumerate(atick): x_of, y_of = -label_offset, label_offset if ax == 'y': y_of = int(-i1 * tick_step) else: x_of = int(i1 * tick_step) if draw: self.c.create_text(tick_x + x_of, tick_y + y_of, text=tick, anchor=tick_anchor) x_mini, y_mini = 0, 0 x_maxi, y_maxi = 0, 0 if ax == 'y': x_of += label_offset x_mini, x_maxi = 8, self.w - st_offset * 2 # Remember what y coord this grid line is at. if i1 == 0: self.y_grid = [] self.y_grid.append(tick_y + y_of) else: y_of -= label_offset y_mini, y_maxi = -8, st_offset * 2 - self.h # Remember what x coord this grid line is at. if i1 == 0: self.x_grid = [] self.x_grid.append(tick_x + x_of) if draw: # Draw the little solid tick, next to the axis. self.c.create_line(tick_x + x_of, tick_y + y_of, tick_x + x_of + x_mini, tick_y + y_of + y_mini) # Draw a dashed grid line, across the entire graph. self.c.create_line(tick_x + x_of, tick_y + y_of, tick_x + x_of + x_maxi, tick_y + y_of + y_maxi, dash=(1, 3)) def car_points(self, draw=True): "Plot the cars themselves." # 199 215 151 151 199 224 230 162 157 250 224 167 178 165 192 249 200 216 204 204 204 191 173 158 color_order = ['#c7d797', '#97c7e0', '#e6a29d', '#fae0a7', '#b2a5c0', '#f9c8d8', '#bfad9e', '#cccccc'] #color_order = ['#98df8a', '#dbdb8d', '#aec7e8', '#c9acd4', '#f7b6d2', # '#ffbb80', '#dc9b8d', '#e9ab17', '#dddddd'] # Those colors above aren't saturated enough. Saturate them more. color_order = map(lambda x: resaturate(x, -80), color_order) # Change color depending on year. cy = dict() for i1, year in enumerate(reversed(sorted(set(self.u.all_year)))): cy[year] = color_order[-1] if i1 < len(color_order): cy[year] = color_order[i1] i1 = -1 # Tuples of (index into self.u.all_* arrays, x position, y position). self.ov_dict = dict() if draw: self.c.focus_set() self.c.bind('<Button-1>', func=self.zoom) self.c.bind('<Button-2>', func=self.unzoom) self.c.bind('<Left>', func=self.left_key) self.c.bind('<Right>', func=self.right_key) self.c.bind('<Up>', func=self.up_key) self.c.bind('<Down>', func=self.down_key) legend = set() osz = 3 + self.zoom_level * 1 # Total vehicle count, and vehicles which pass the filter count. self.vcount = self.fcount = 0 for year, km, price in zip(self.u.all_year, self.u.all_km, self.u.all_price): x, y = self.xyp(km, price) i1 += 1 if x < self.x_grid[0] or x > self.x_grid[-1] or \ y > self.y_grid[0] or y < self.y_grid[-1]: continue self.vcount += 1 legend.add((year, cy[year])) filtered = False if not re.search(self.filter, self.u.all_descr[i1], re.I): filtered = True # If a data point is filtered out, make its outline reflect its # model year, and fill it with white. # # Else, make its outline and fill color reflect the model year, and # upon mouseover, make it entirely red. ov = self.c.create_oval(x-osz, y-osz, x+osz, y+osz, outline=cy[year], activeoutline=['red', cy[year]][filtered], fill=[cy[year], 'white'][filtered], activefill=['red', 'white'][filtered], ) self.ov_dict[ov] = (i1, x, y, cy[year], filtered) # If a data point is filtered out, mousing over it does nothing, # and also, lower it behind everything else. if filtered: self.c.lower(ov) else: self.fcount += 1 if draw: use_tag = 'Tag %d' % i1 self.c.addtag_withtag(use_tag, ov) self.c.tag_bind(use_tag, sequence='<Enter>', func=self.mouseover) self.c.tag_bind(use_tag, sequence='<Leave>', func=self.mouseoff) self.c.tag_bind(use_tag, sequence='<Button-1>', func=self.select) if draw: # OK, add a legend for every year that's displayed. i1 = 0 for yr, color in reversed(sorted(legend)): xp, yp = self.x_grid[-1] + 10, self.y_grid[-1] + 15 * i1 self.c.create_oval(xp-osz, yp-osz, xp+osz, yp+osz, outline=color, fill=color) self.c.create_text(xp + 8, yp, text=str(yr), anchor=W) i1 += 1 # And, add a title. tistr = 'Vehicle count: %d' % self.vcount if self.fcount != self.vcount: tistr = 'Filtered vehicle count: %d' % self.fcount xp = (self.x_grid[0] + self.x_grid[-1]) / 2 yp = self.y_grid[-1] - 30 self.c.create_text(xp, yp, text=tistr, font=('Helvetica', '16')) zstr1 = 'Click on a blank graph location to zoom in' zstr2 = 'Right click to zoom out' if self.zoom_level == 0: zstr = zstr1 elif self.zoom_level == 2: zstr = zstr2 else: zstr = zstr1 + ', or r' + zstr2[1:] self.c.create_text(xp, yp + 16, text=zstr, font=('Helvetica', '14')) def mouseover(self, event): oval = event.widget.find_closest(event.x, event.y)[0] # XXX Sometimes, the closest widget is an axis grid line, not an oval. # Need to handle this correctly eventually. if oval not in self.ov_dict: return self.is_hovering = True ind, x, y, color, filtered = self.ov_dict[oval] # Figure out how high the box needs to be by creating the text off- # graph, then getting its bbox and deleting it. w = 200 de_text = self.u.all_descr[ind] deobj = self.c.create_text(self.w + 3, self.h + 3, text=de_text, anchor=N+W, width=w-6, font=('Helvetica', '14')) bbox = self.c.bbox(deobj) self.c.delete(deobj) h = 18 + bbox[3] - bbox[1] border = 5 if x > self.xmid: x -= (w + border) else: x += border if y > self.ymid: y -= (h + border) else: y += border self.re = list() self.re.append(self.c.create_rectangle(x, y, x + w, y + h, fill=resaturate(color, 50))) pr_text = '$%s' % self.u.commafy(self.u.all_price[ind]) self.re.append(self.c.create_text(x + 3, y + 3, text=pr_text, anchor=N+W, font=('Helvetica', '10'))) km_text = '%skm' % self.u.commafy(self.u.all_km[ind]) self.re.append(self.c.create_text(x + w - 3, y + 3, text=km_text, anchor=N+E, font=('Helvetica', '10'))) wh_text = self.u.all_wherestr[ind] if wh_text[0].isdigit(): wh_text += ' away' self.re.append(self.c.create_text(x + w/2, y + 3, text=wh_text, anchor=N, font=('Helvetica', '10'))) self.re.append(self.c.create_text(x + 3, y + 16, text=de_text, anchor=N+W, width=w-6, font=('Helvetica', '14'))) def set_filter(self, st): "Given a string 'st', filter ovals whose description doesn't match." self.filter = st self.replot() def mouseoff(self, event): "Code for mousing off a data point." # The tooptip rectangle and all its sub-objects need to be destroyed. map(self.c.delete, self.re) # Also, need to note that we're no longer over an oval -- that way, # Button-1 events will cause a zoom, rather than launching a web page. self.is_hovering = False def _zoom_animation(self): import time from math import tanh scale = 5 for i1 in range(-scale, scale+1): self.replot(zlfrac=0.5 + 0.5*tanh(i1*2.0/scale)/tanh(2.0)) self.c.update() def zoom(self, event): # Only zoom in if we're actually within the graph boundaries. if event.x <= self.x_grid[0] or event.x > self.x_grid[-1]: return if event.y >= self.y_grid[0] or event.y < self.y_grid[-1]: return # Don't zoom if we're hovering over a data point: let the web browser # event handler operate. if self.is_hovering: return # Don't zoom in more than twice. if self.zoom_level >= 2: return # Find the grid square which we're inside. for i1 in range(len(self.x_grid) - 1): if event.x <= self.x_grid[i1 + 1]: xgrid = i1 + 1 break for i1 in range(len(self.y_grid) - 1): if event.y >= self.y_grid[i1 + 1]: ygrid = i1 + 1 break self.zoom_level += 1 zl = self.zoom_level # Make the limits of the new graph be those of the grid square which # was clicked inside. self.km[zl] = (self.xtick[xgrid-1], self.xtick[xgrid]) self.price[zl] = (self.ytick[ygrid-1], self.ytick[ygrid]) if zl == 1: self.zoom_price_start = self.u.axis(*self.price[0])[:2] self.zoom_km_start = self.u.axis(*self.km[0])[:2] else: self.zoom_price_start = self.price[zl - 1] self.zoom_km_start = self.km[zl - 1] self.zoom_price_end = self.price[zl] self.zoom_km_end = self.km[zl] self._zoom_animation() self.replot() def unzoom(self, event): # If already at maximum zoom, nothing to be done. if self.zoom_level == 0: return # If not clicking inside graph boundaries, don't unzoom. if event.x <= self.x_grid[0] or event.x > self.x_grid[-1]: return if event.y >= self.y_grid[0] or event.y < self.y_grid[-1]: return self.zoom_level -= 1 zl = self.zoom_level self.zoom_price_start = self.price[zl + 1] self.zoom_km_start = self.km[zl + 1] if zl == 0: self.zoom_price_end = self.u.axis(*self.price[0])[:2] self.zoom_km_end = self.u.axis(*self.km[0])[:2] else: self.zoom_price_end = self.price[zl] self.zoom_km_end = self.km[zl] self._zoom_animation() self.replot() def left_key(self, event): zl = self.zoom_level if zl == 0: return # If at left edge already, don't scroll. kz = self.km[zl] if self.km[0][0] > kz[0]: return self.zoom_price_start = self.zoom_price_end = self.price[zl] self.zoom_km_start = kz self.km[zl] = (kz[0] - (kz[1] - kz[0]), kz[0]) self.zoom_km_end = self.km[zl] self._zoom_animation() self.replot() def right_key(self, event): zl = self.zoom_level if zl == 0: return # If at right edge already, don't scroll. kz = self.km[zl] if self.km[0][1] < kz[1]: return self.zoom_price_start = self.zoom_price_end = self.price[zl] self.zoom_km_start = kz self.km[zl] = (kz[1], kz[1] + (kz[1] - kz[0])) self.zoom_km_end = self.km[zl] self._zoom_animation() self.replot() def down_key(self, event): zl = self.zoom_level if zl == 0: return # If at bottom edge already, don't scroll. pz = self.price[zl] if self.price[0][0] > pz[0]: return self.zoom_km_start = self.zoom_km_end = self.km[zl] self.zoom_price_start = pz self.price[zl] = (pz[0] - (pz[1] - pz[0]), pz[0]) self.zoom_price_end = self.price[zl] self._zoom_animation() self.replot() def up_key(self, event): zl = self.zoom_level if zl == 0: return # If at top edge already, don't scroll. pz = self.price[zl] if self.price[0][1] < pz[1]: return self.zoom_km_start = self.zoom_km_end = self.km[zl] self.zoom_price_start = pz self.price[zl] = (pz[1], pz[1] + (pz[1] - pz[0])) self.zoom_price_end = self.price[zl] self._zoom_animation() self.replot() def select(self, event): "Open a web page, when a data point has been clicked on." oval = event.widget.find_closest(event.x, event.y)[0] # XXX As above, sometimes the closest widget is a grid line, not an # oval. Need to handle this correctly, eventually. if oval not in self.ov_dict: return ind, xp, yp, color, filtered = self.ov_dict[oval] webbrowser.open(self.u.all_alink[ind])
class histogramWidget: BACKGROUND = "#222222" EDGE_HISTOGRAM_COLOR = "#999999" NODE_HISTOGRAM_COLOR = "#555555" TOOLTIP_COLOR="#FFFF55" PADDING = 8 CENTER_WIDTH = 1 CENTER_COLOR = "#444444" ZERO_GAP = 1 UPDATE_WIDTH = 9 UPDATE_COLOR = "#FFFFFF" HANDLE_WIDTH = 5 HANDLE_COLOR = "#FFFFFF" HANDLE_LENGTH = (HEIGHT-2*PADDING) TICK_COLOR = "#FFFFFF" TICK_WIDTH = 10 TICK_FACTOR = 2 LOG_BASE = 10.0 def __init__(self, parent, x, y, width, height, data, logScale=False, callback=None): self.canvas = Canvas(parent,background=histogramWidget.BACKGROUND, highlightbackground=histogramWidget.BACKGROUND,width=width,height=height) self.canvas.place(x=x,y=y,width=width,height=height,bordermode="inside") self.logScale = logScale self.callback = callback self.edgeBars = [] self.nodeBars = [] self.binValues = [] self.numBins = len(data) - 1 self.currentBin = self.numBins # start the slider at the highest bin edgeRange = 0.0 nodeRange = 0.0 for values in data.itervalues(): if values[0] > edgeRange: edgeRange = values[0] if values[1] > nodeRange: nodeRange = values[1] edgeRange = float(edgeRange) # ensure that it will yield floats when used in calculations... nodeRange = float(nodeRange) if logScale: edgeRange = math.log(edgeRange,histogramWidget.LOG_BASE) nodeRange = math.log(nodeRange,histogramWidget.LOG_BASE) # calculate the center line - but don't draw it yet self.center_x = histogramWidget.PADDING if self.logScale: self.center_x += histogramWidget.TICK_WIDTH+histogramWidget.PADDING self.center_y = height/2 self.center_x2 = width-histogramWidget.PADDING self.center_y2 = self.center_y + histogramWidget.CENTER_WIDTH # draw the histograms with background-colored baseline rectangles (these allow tooltips to work on very short bars with little area) self.bar_interval = float(self.center_x2 - self.center_x) / (self.numBins+1) bar_x = self.center_x edge_y2 = self.center_y-histogramWidget.PADDING edge_space = edge_y2-histogramWidget.PADDING node_y = self.center_y2+histogramWidget.PADDING node_space = (height-node_y)-histogramWidget.PADDING thresholds = sorted(data.iterkeys()) for threshold in thresholds: self.binValues.append(threshold) edgeWeight = data[threshold][0] nodeWeight = data[threshold][1] if logScale: if edgeWeight > 0: edgeWeight = math.log(edgeWeight,histogramWidget.LOG_BASE) else: edgeWeight = 0 if nodeWeight > 0: nodeWeight = math.log(nodeWeight,histogramWidget.LOG_BASE) else: nodeWeight = 0 bar_x2 = bar_x + self.bar_interval edge_y = histogramWidget.PADDING + int(edge_space*(1.0-edgeWeight/edgeRange)) edge = self.canvas.create_rectangle(bar_x,edge_y,bar_x2,edge_y2,fill=histogramWidget.EDGE_HISTOGRAM_COLOR,width=0) baseline = self.canvas.create_rectangle(bar_x,edge_y2+histogramWidget.ZERO_GAP,bar_x2,edge_y2+histogramWidget.PADDING,fill=histogramWidget.BACKGROUND,width=0) self.canvas.addtag_withtag("Threshold: %f" % threshold,edge) self.canvas.addtag_withtag("No. Edges: %i" % data[threshold][0],edge) self.canvas.tag_bind(edge,"<Enter>",self.updateToolTip) self.canvas.tag_bind(edge,"<Leave>",self.updateToolTip) self.edgeBars.append(edge) self.canvas.addtag_withtag("Threshold: %f" % threshold,baseline) self.canvas.addtag_withtag("No. Edges: %i" % data[threshold][0],baseline) self.canvas.tag_bind(baseline,"<Enter>",self.updateToolTip) self.canvas.tag_bind(baseline,"<Leave>",self.updateToolTip) node_y2 = node_y + int(node_space*(nodeWeight/nodeRange)) node = self.canvas.create_rectangle(bar_x,node_y,bar_x2,node_y2,fill=histogramWidget.NODE_HISTOGRAM_COLOR,width=0) baseline = self.canvas.create_rectangle(bar_x,node_y-histogramWidget.PADDING,bar_x2,node_y-histogramWidget.ZERO_GAP,fill=histogramWidget.BACKGROUND,width=0) self.canvas.addtag_withtag("Threshold: %f" % threshold,node) self.canvas.addtag_withtag("No. Nodes: %i" % data[threshold][1],node) self.canvas.tag_bind(node,"<Enter>",self.updateToolTip) self.canvas.tag_bind(node,"<Leave>",self.updateToolTip) self.nodeBars.append(node) self.canvas.addtag_withtag("Threshold: %f" % threshold,baseline) self.canvas.addtag_withtag("No. Nodes: %i" % data[threshold][1],baseline) self.canvas.tag_bind(baseline,"<Enter>",self.updateToolTip) self.canvas.tag_bind(baseline,"<Leave>",self.updateToolTip) bar_x = bar_x2 # now draw the center line self.centerLine = self.canvas.create_rectangle(self.center_x,self.center_y,self.center_x2,self.center_y2,fill=histogramWidget.CENTER_COLOR,width=0) # draw the tick marks if logarithmic if self.logScale: tick_x = histogramWidget.PADDING tick_x2 = histogramWidget.PADDING+histogramWidget.TICK_WIDTH start_y = edge_y2 end_y = histogramWidget.PADDING dist = start_y-end_y while dist > 1: dist /= histogramWidget.TICK_FACTOR self.canvas.create_rectangle(tick_x,end_y+dist-1,tick_x2,end_y+dist,fill=histogramWidget.TICK_COLOR,width=0) start_y = node_y end_y = height-histogramWidget.PADDING dist = end_y-start_y while dist > 1: dist /= histogramWidget.TICK_FACTOR self.canvas.create_rectangle(tick_x,end_y-dist,tick_x2,end_y-dist+1,fill=histogramWidget.TICK_COLOR,width=0) # draw the update bar bar_x = self.currentBin*self.bar_interval + self.center_x bar_x2 = self.center_x2 bar_y = self.center_y-histogramWidget.UPDATE_WIDTH/2 bar_y2 = bar_y+histogramWidget.UPDATE_WIDTH self.updateBar = self.canvas.create_rectangle(bar_x,bar_y,bar_x2,bar_y2,fill=histogramWidget.UPDATE_COLOR,width=0) # draw the handle handle_x = self.currentBin*self.bar_interval-histogramWidget.HANDLE_WIDTH/2+self.center_x handle_x2 = handle_x+histogramWidget.HANDLE_WIDTH handle_y = self.center_y-histogramWidget.HANDLE_LENGTH/2 handle_y2 = handle_y+histogramWidget.HANDLE_LENGTH self.handleBar = self.canvas.create_rectangle(handle_x,handle_y,handle_x2,handle_y2,fill=histogramWidget.HANDLE_COLOR,width=0) self.canvas.tag_bind(self.handleBar, "<Button-1>",self.adjustHandle) self.canvas.tag_bind(self.handleBar, "<B1-Motion>",self.adjustHandle) self.canvas.tag_bind(self.handleBar, "<ButtonRelease-1>",self.adjustHandle) parent.bind("<Left>",lambda e: self.nudgeHandle(e,-1)) parent.bind("<Right>",lambda e: self.nudgeHandle(e,1)) # init the tooltip as nothing self.toolTipBox = self.canvas.create_rectangle(0,0,0,0,state="hidden",fill=histogramWidget.TOOLTIP_COLOR,width=0) self.toolTip = self.canvas.create_text(0,0,state="hidden",anchor="nw") self.canvas.bind("<Enter>",self.updateToolTip) self.canvas.bind("<Leave>",self.updateToolTip) def adjustHandle(self, event): newBin = int(self.numBins*(event.x-self.center_x)/float(self.center_x2-self.center_x)+0.5) if newBin == self.currentBin or newBin < 0 or newBin > self.numBins: return self.canvas.move(self.handleBar,(newBin-self.currentBin)*self.bar_interval,0) self.currentBin = newBin if self.callback != None: self.callback(self.binValues[newBin]) def nudgeHandle(self, event, distance): temp = self.currentBin+distance if temp < 0 or temp > self.numBins: return self.canvas.move(self.handleBar,distance*self.bar_interval,0) self.currentBin += distance if self.callback != None: self.callback(self.binValues[self.currentBin]) def update(self, currentBins): currentBar = self.canvas.coords(self.updateBar) self.canvas.coords(self.updateBar,currentBins*self.bar_interval+self.center_x,currentBar[1],currentBar[2],currentBar[3]) def updateToolTip(self, event): allTags = self.canvas.gettags(self.canvas.find_overlapping(event.x,event.y,event.x+1,event.y+1)) if len(allTags) == 0: self.canvas.itemconfig(self.toolTipBox,state="hidden") self.canvas.itemconfig(self.toolTip,state="hidden") return outText = "" for t in allTags: if t == "current": continue outText += t + "\n" outText = outText[:-1] # strip the last return self.canvas.coords(self.toolTip,event.x+20,event.y) self.canvas.itemconfig(self.toolTip,state="normal",text=outText,anchor="nw") # correct if our tooltip is off screen textBounds = self.canvas.bbox(self.toolTip) if textBounds[2] >= WIDTH-2*histogramWidget.PADDING: self.canvas.itemconfig(self.toolTip, anchor="ne") self.canvas.coords(self.toolTip,event.x-20,event.y) if textBounds[3] >= HEIGHT-2*histogramWidget.PADDING: self.canvas.itemconfig(self.toolTip, anchor="se") elif textBounds[3] >= HEIGHT-2*histogramWidget.PADDING: self.canvas.itemconfig(self.toolTip, anchor="sw") # draw the box behind it self.canvas.coords(self.toolTipBox,self.canvas.bbox(self.toolTip)) self.canvas.itemconfig(self.toolTipBox, state="normal")