3

I have just created a pie chart by Tkinter in Python as follows:

def frac(n): 
    return 360. * n / 500

import Tkinter
c = Tkinter.Canvas(width=100, height=100); c.pack()
c.create_arc((2,2,98,98), fill="red", start=frac(0), extent = 
frac(100))
c.create_arc((2,2,98,98), fill="blue", start=frac(100), extent = frac(400))
c.create_arc((2,2,98,98), fill="white", start=frac(400), extent = frac(100), width=0)
c.mainloop()

This is the result:

enter image description here

Now, I want to change the color of each slice when hovering the mouse over it. How can I do that? Many thanks

Khang Truong
  • 357
  • 1
  • 6
  • 16
  • What about trying that with matplotlib assistance!!!! –  Sep 01 '17 at 15:07
  • @Mandy8055 Can matplotlib handle such a thing as hover? – Right leg Sep 01 '17 at 15:09
  • Yes of course!!!All you need is event handling: https://matplotlib.org/users/event_handling.html.Although I have little information that this can be achieved purely in Tkinter;it can be achieved in matplotlib –  Sep 01 '17 at 15:09

2 Answers2

1

So, my code is a mess, but I hope it will help you get started and get the basic ideas.

The first idea is that you need to bind the <Motion> mouse event to the c canvas. The bind method takes two arguments: an event, which says when to do something, and a function, which says what to do. I chose to define a redraw_chart function, that draws the pie according to the position of the mouse. This function is what will be called on a <Motion> event, so I bind as follows:

c.bind('<Motion>', lambda e: redraw_chart(e.x, e.y))

The lambda function is just an anonymous function, that receives the event raised, and passes the two coordinates of the event (that is, the coordinates of the mouse) to the redraw_chart.

The redraw_chart function is really dumb: it draws the pie according to the coordinates it received:

def redraw_chart(x, y):
    global redCode, blueCode, whiteCode

    arc = get_arc(x, y)

    if arc == "red":
        c.itemconfig(redCode, fill="green")
        c.itemconfig(redCode, fill="blue")
        c.itemconfig(redCode, fill="white")   

    elif arc == "blue":
        c.itemconfig(redCode, fill="red")
        c.itemconfig(redCode, fill="green")
        c.itemconfig(redCode, fill="white")   

    elif arc == "white":
        c.itemconfig(redCode, fill="red")
        c.itemconfig(redCode, fill="blue")
        c.itemconfig(redCode, fill="green")   

    else:
        c.itemconfig(redCode, fill="green")
        c.itemconfig(redCode, fill="blue")
        c.itemconfig(redCode, fill="white")   

Now, what are redCode, blueCode and whiteCode? They are the addresses of the three arc objects created by the c.create_arc method. They are useful to modify the arcs, so as to avoid creating new ones. There is still one thing left to define: the get_arc function.

The get_arc function takes a (x, y) couple, representing a point of the canvas, and returns the corresponding arc:

def get_arc(x, y):
    if is_in_arc(x, y, redArc[0], redArc[0]+redArc[1]):
        return "red"
    elif is_in_arc(x, y, blueArc[0], blueArc[0]+blueArc[1]):
        return "blue"
    elif is_in_arc(x, y, whiteArc[0], whiteArc[0]+whiteArc[1]):
        return "white"
    else:
        return None

It relies on the is_in_arc function, that takes a point, a portion of the pie, and tells if the point lies in the portion.

def is_in_arc(x, y, angle0, angle1):
    if (x-50)**2 + (y-50)**2 > 48**2:
        return False

    theta = - np.arctan2(y-50, x-50)
    return angle0 <= frac(theta) <= angle1

The np.arctan2 function from numpy returns the angle in radians corresponding to the (x, y) point. Then, the fract method returns the corresponding value in degrees. I modified it, because I did not really understand yours:

def frac(n):
    if n < 0:
        n += 2*np.pi
    return 360 * n / (2*np.pi)

So here is what it looks like. You cannot see the cursor one the screenshot, but I guarantee you that the parts turn green when hovered.

Pie charts hovered

Here is the complete code:

import tkinter as tk
import numpy as np


def frac(n):
    if n < 0:
        n += 2*np.pi
    return 360 * n / (2*np.pi)


c = tk.Canvas(width=100, height=100)
c.pack()

redArc = (frac(0), frac(np.pi/3))
blueArc = (frac(np.pi/3), frac(4*np.pi/3))
whiteArc = (frac(5*np.pi/3), frac(np.pi/3))

redCode = c.create_arc((2,2,98,98), fill="red", start=redArc[0], extent=redArc[1])
blueCode = c.create_arc((2,2,98,98), fill="blue", start=blueArc[0], extent=blueArc[1])
whiteCode = c.create_arc((2,2,98,98), fill="white", start=whiteArc[0], extent=whiteArc[1])


def is_in_arc(x, y, angle0, angle1):
    if (x-50)**2 + (y-50)**2 > 48**2:
        return False
    theta = - np.arctan2(y-50, x-50)
    return angle0 <= frac(theta) <= angle1


def get_arc(x, y):
    if is_in_arc(x, y, redArc[0], redArc[0]+redArc[1]):
        return "red"
    elif is_in_arc(x, y, blueArc[0], blueArc[0]+blueArc[1]):
        return "blue"
    elif is_in_arc(x, y, whiteArc[0], whiteArc[0]+whiteArc[1]):
        return "white"
    else:
        return None


def redraw_chart(x, y):
    global redCode, blueCode, whiteCode

    arc = get_arc(x, y)

    if arc == "red":
        c.itemconfig(redCode, fill="green")
        c.itemconfig(redCode, fill="blue")
        c.itemconfig(redCode, fill="white")   

    elif arc == "blue":
        c.itemconfig(redCode, fill="red")
        c.itemconfig(redCode, fill="green")
        c.itemconfig(redCode, fill="white")   

    elif arc == "white":
        c.itemconfig(redCode, fill="red")
        c.itemconfig(redCode, fill="blue")
        c.itemconfig(redCode, fill="green")   

    else:
        c.itemconfig(redCode, fill="green")
        c.itemconfig(redCode, fill="blue")
        c.itemconfig(redCode, fill="white") 


c.bind('<Motion>', lambda e: redraw_chart(e.x, e.y))

c.mainloop()
Right leg
  • 16,080
  • 7
  • 48
  • 81
  • This code appears to simply pile new arcs on top of the existing ones, and will eventually run out of memory. Recoloring the existing arcs (via `c.itemconfig()` would be a vastly better approach. – jasonharper Sep 01 '17 at 13:15
  • @jasonharper Oh right, I forgot about this method. But the items are deleted anyway, so although it's not quite good, the objects don't actually pile up. – Right leg Sep 01 '17 at 13:16
  • You aren't deleting the items. You're deleting the IDs of the items. – jasonharper Sep 01 '17 at 13:18
  • @jasonharper It seems that I mixed `del` and `canvas.delete()`... My bad. But anyway, `canvas.itemconfig` is way better. By the way, I'm aware that this code is ugly, but I tried to start from OP's code. – Right leg Sep 01 '17 at 13:23
0

You can use the bind method to, well, bind an event and redraw the chart, like this:

def on_enter(event):
    c.create_arc((2,2,98,98), fill="orange", start=frac(100), extent = frac(400))
(...)
c.bind('<Enter>', on_enter)

See this answer for an example of how to embed the whole thing in a class.

iCart
  • 2,179
  • 3
  • 27
  • 36
  • How do you know what arc is hovered? – Right leg Sep 01 '17 at 12:21
  • The event has the x:y coordinates of the cursor. Based on that and the center of the circle, you can calculate which arc is under the cursor. My math lessons are too far back for me to whip out the formula, but i feel like it should be simple enough to find. – iCart Sep 01 '17 at 12:30