2

I've been trying to annotate individual points in a 3d scatter plot and getting them updated dynamically.

Referred to this: Matplotlib: Annotating a 3D scatter plot

But I'm using FuncAnimation to dynamically update my points instead, the above link does not have a solution that does that lets you know how you can constantly change the position of your text at every interval of funcanimation.

The issue here is that although I could get the text to be drawn out at the very start, subsequent intervals does not update the position of my texts.

Below is the code

import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d.axes3d as p3
import matplotlib.animation as animation

class Simulator:

    def __init__(self):
        s_1 = ((0.5, 0.5, 0.0), (0.5,0.5,0.2), (0.5,0.5,1.0), (1.9,0.5,2.0))
        s_2 = ((1.9, 0.5, 0.0), (1.9,0.5,0.2), (1.9,0.5,1.0), (1.9,1.9,2.0))
        s_3 = ((1.2, 1.2, 0.0), (1.2,1.2,0.2), (1.2,1.2,1.0), (1.2,1.2,2.5))
        s_4 = ((0.5, 1.9, 0.0), (0.5,1.9,0.2), (0.5,1.9,1.0), (0.5,0.5,2.0))
        s_5 = ((1.9, 1.9, 0.0), (1.9,1.9,0.2), (1.9,1.9,1.0), (0.5,1.9,2.0))

        self.data = {
        's_1': {'raw': s_1},
        's_2': {'raw': s_2},
        's_3': {'raw': s_3},
        's_4': {'raw': s_4},
        's_5': {'raw': s_5}
        }

        ###### Setup ######
        self.fig = plt.figure()
        self.ax = self.fig.add_subplot(111, projection='3d')
        # Setting the axes properties
        self.ax.set_xlim3d([0.0, 3.0])
        self.ax.set_xlabel('X')

        self.ax.set_ylim3d([0.0, 3.0])
        self.ax.set_ylabel('Y')

        self.ax.set_zlim3d([0.0, 3.0])
        self.ax.set_zlabel('Z')

        for point,dic in self.data.items():
            dic['x'] = []
            dic['y'] = []
            dic['z'] = []
            dic['length'] = len(dic['raw'])
            for coords in dic['raw']:
                dic['x'].append(coords[0])
                dic['y'].append(coords[1])
                dic['z'].append(coords[2])

        # Interval in milliseconds
        self.anim = animation.FuncAnimation(self.fig, self.update, init_func=self.setup, interval=1000)

        plt.show()


    def setup(self):
        plots = []

        for point,dic in self.data.items():
            dic['plot'] = self.ax.scatter3D([], [], [], c='red', picker = True)
            dic['label'] = self.ax.text3D(dic['x'][0], dic['y'][0], dic['z'][0], point, zorder=1, color='k')

    def update(self, i):

        plots = []

        seq_x = []
        seq_y = []
        seq_z = []

        for point,dic in self.data.items():
            if i < dic['length']:

                seq_x = dic['x'][i]
                seq_y = dic['y'][i]
                seq_z = dic['z'][i]
                dic['plot']._offsets3d = [seq_x], [seq_y], [seq_z]

                #### THIS IS NOT WORKING!!!! ####
                dic['label'].set_position((seq_x, seq_y, seq_z))

                #### BUT SOMEHOW THIS IS WORKING AND THE TEXT's COLORS GETS UPDATED??? ####
                dic['label'].set_color('red')

                #### IF SOMEONE IS KIND ENOUGH, I HAVE NO IDEA WHY THIS DOES NOT WORK TOO :( ####
                dic['plot'].set_color('blue')
                plots.append(dic['plot'])
            else:
                self.anim.event_source.stop()
                print('Simulation ended.')
        return plots


Simulator()

enter image description here

As you can see from the image above, the texts are not right beside the points where they should be. What i would like is for the text to follow the points as it rises.

The portion below is NOT part of the main question, but another question that I have:

Does any one know how to change the color of a scatter plot? I've tried set_color('another color') but it is not working, the color of the points does not update itself.

@ImportanceOfBeingErnest

Well, its not as if i have not tried to use those solutions

I disagree that all but one solution answer showed a solution for any kind of updating. Here is why.

Accepted answer by HYRY:

I tried this answer originally, but I just don't know where you would input the z-coordinates, if you could kindly point it out to me. This answer is just so complicated. I do not even need the pointy arrow thing, I just need the point to be labelled there at all times, with the label following wherever the point goes. Besides, the text only updates on a mouse release, instead of dynamically updating the label. If you were to try the code that I have, you would realise that even if you drag the screen around, the points are still moving on their own, and the labels should as well.

Answer by msch:

I love this answer, its simple and straight to the core of the issue. This question is basically a build up from this answer. I guess my command of English is not as strong as yours, so I've edited the question once again to make sure that it says exactly what it is. Please let me know if it is still unclear.

Answer by Luchko:

This answer looks good, but is actually really complicated. I really can't even understand where to start for this. I've tried adding that class in, and then using the code

annotate3D(ax, s=str('hi'), xyz=xyz_, fontsize=10, xytext=(-3,3),textcoords='offset points', ha='right',va='bottom')

I got the error: TypeError: annotate3D() got multiple values for argument 's'

Answers by DonCristobal, fredcallaway and Rafael J:

Uses: fig.canvas.draw(), which there is no point to considering I'm using FuncAnimation. Wouldn't that defeat the purpose of using funcanimation if I were to use fig.canvas.draw()? Let me know if I'm wrong

The answer by duhaime:

There is no updating of text, it is just a simple slap there with ax.text.

Aesreal
  • 131
  • 1
  • 12
  • `set_position` is not expected to work with 3D coordinates. But there are several solutions in the linked question. Why not use one of them? – ImportanceOfBeingErnest Jul 28 '18 at 21:00
  • the solutions in the linked question are more of a 1-time drawing rather than a dynamic updating. I need the text to follow the points as it moves. don't think i was clear enough with the issue, so i've updated the question. – Aesreal Jul 29 '18 at 03:53
  • Well, you are right that the motivation for updating is different in the linked question, yet, all but one answer show solutions that are directly applicable to any kind of updating. – ImportanceOfBeingErnest Jul 29 '18 at 10:21
  • The solutions provided are very complicated, I'm looking for something really simple. The comment has a word limitation, so I've updated the question again. – Aesreal Jul 29 '18 at 11:08

1 Answers1

3

The easiest is probably to create 2D text.

text = ax.text2D(x, y, text)

And then to update its projected coordinates.

x2, y2, _ = proj3d.proj_transform(seq_x, seq_y, seq_z, ax.get_proj())
text.set_position((x2,y2))

Full working code:

import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d.axes3d as p3
from mpl_toolkits.mplot3d import proj3d
import matplotlib.animation as animation

class Simulator:

    def __init__(self):
        s_1 = ((0.5, 0.5, 0.0), (0.5,0.5,0.2), (0.5,0.5,1.0), (1.9,0.5,2.0))
        s_2 = ((1.9, 0.5, 0.0), (1.9,0.5,0.2), (1.9,0.5,1.0), (1.9,1.9,2.0))
        s_3 = ((1.2, 1.2, 0.0), (1.2,1.2,0.2), (1.2,1.2,1.0), (1.2,1.2,2.5))
        s_4 = ((0.5, 1.9, 0.0), (0.5,1.9,0.2), (0.5,1.9,1.0), (0.5,0.5,2.0))
        s_5 = ((1.9, 1.9, 0.0), (1.9,1.9,0.2), (1.9,1.9,1.0), (0.5,1.9,2.0))

        self.data = {
        's_1': {'raw': s_1},
        's_2': {'raw': s_2},
        's_3': {'raw': s_3},
        's_4': {'raw': s_4},
        's_5': {'raw': s_5}
        }

        ###### Setup ######
        self.fig = plt.figure()
        self.ax = self.fig.add_subplot(111, projection='3d')
        # Setting the axes properties
        self.ax.set_xlim3d([0.0, 3.0])
        self.ax.set_xlabel('X')

        self.ax.set_ylim3d([0.0, 3.0])
        self.ax.set_ylabel('Y')

        self.ax.set_zlim3d([0.0, 3.0])
        self.ax.set_zlabel('Z')

        for point,dic in self.data.items():
            dic['x'] = []
            dic['y'] = []
            dic['z'] = []
            dic['length'] = len(dic['raw'])
            for coords in dic['raw']:
                dic['x'].append(coords[0])
                dic['y'].append(coords[1])
                dic['z'].append(coords[2])

        # Interval in milliseconds
        self.anim = animation.FuncAnimation(self.fig, self.update, init_func=self.setup, interval=1000)

        plt.show()


    def setup(self):
        plots = []

        for point,dic in self.data.items():
            dic['plot'] = self.ax.scatter3D([], [], [], c='red', picker = True)
            dic['label'] = self.ax.text2D(dic['x'][0], dic['y'][0], point, zorder=1, color='k')

    def update(self, i):

        plots = []

        seq_x = []
        seq_y = []
        seq_z = []

        for point,dic in self.data.items():
            if i < dic['length']:

                seq_x = dic['x'][i]
                seq_y = dic['y'][i]
                seq_z = dic['z'][i]
                dic['plot']._offsets3d = [seq_x], [seq_y], [seq_z]

                #### Set position of text
                x2, y2, _ = proj3d.proj_transform(seq_x, seq_y, seq_z, self.ax.get_proj())
                dic['label'].set_position((x2,y2))

            else:
                self.anim.event_source.stop()
                print('Simulation ended.')
        return plots


Simulator()
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Thank you so much for your time, it is indeed working as intended now! Can I just ask where you learnt how to do this from? I've been scanning through the documentations for hours but it feels as if it is incomplete. How do you even know to use that particular method? – Aesreal Jul 29 '18 at 11:37
  • Essentially I looked at the linked accepted answer and simplified it. In general it helps to look at the documentation (although for 3D plots it may not cover everything) or the source code itself. – ImportanceOfBeingErnest Jul 29 '18 at 11:41
  • Ahh, I"m starting to see how. Thank you again! I'll try and improve and get better at this. – Aesreal Jul 29 '18 at 11:44