Source code for eyefeatures.visualization.dynamic_visualization

import io

import numpy as np
import pandas as pd
import plotly.graph_objects as go
from PIL import Image

from eyefeatures.utils import _select_regressions


def _built_figure(
    fig_dict: dict, animation_duration: int = 500
):  # animation_duration in ms
    """Function for building a layout for plot."""
    fig_dict["layout"]["width"] = 600
    fig_dict["layout"]["height"] = 600
    fig_dict["layout"]["updatemenus"] = [
        {
            "buttons": [
                {
                    "args": [
                        None,
                        {
                            "frame": {"duration": animation_duration, "redraw": False},
                            "fromcurrent": True,
                            "transition": {
                                "duration": 300,
                                "easing": "quadratic-in-out",
                            },
                        },
                    ],
                    "label": "Play",
                    "method": "animate",
                },
                {
                    "args": [
                        [None],
                        {
                            "frame": {"duration": 0, "redraw": False},
                            "mode": "immediate",
                            "transition": {"duration": 0},
                        },
                    ],
                    "label": "Pause",
                    "method": "animate",
                },
            ],
            "direction": "left",
            "pad": {"r": 10, "t": 87},
            "showactive": False,
            "type": "buttons",
            "x": 0.1,
            "xanchor": "right",
            "y": 0.0,
            "yanchor": "top",
        }
    ]


[docs] def tracker_animation( data_: pd.DataFrame, x: str, y: str, path_color: str = "green", path_width: float = 0.5, points_color: str = "black", points_width: float = 6, add_regression: bool = False, regression_color: str = "red", meta_data: list[str] = None, rule: tuple[int, ...] = None, deviation: int | tuple[int, ...] = None, aoi: str = None, aoi_c: dict[str, str] = None, tracker_color: str = "red", animation_duration: int = 500, save_gif: str = None, frames_count: int = 1, ): """Function for tracker animation. Args: data_: DataFrame with fixations. x: x coordinate of fixation. y: y coordinate of fixation. path_color: color of saccades. path_width: width of saccades. points_color: color of points. points_width: width of points. add_regression: whether to add regressions. regression_color: color of regressions. meta_data: list of columns that will be used for meta data. rule: must be either 1) tuple of quadrants direction to classify regressions, 1st quadrant being upper-right square of plane and counting anti-clockwise or 2) tuple of angles in degrees(0 <= angle <= 360). deviation: if None, then `rule` is interpreted as quadrants. Otherwise, `rule` is interpreted as angles. If integer, then is a +-deviation for all angles. If tuple of integers, then must be of the same length as `rule`, each value being a corresponding deviation for each angle. Angle = 0 is positive x-axis direction, rotating anti-clockwise. aoi: AOI of fixations. aoi_c: colormap for AOI. tracker_color: color of tracker. animation_duration: duration of animation. save_gif: path to save animation. frames_count: TODO. """ data = data_.reset_index(drop=True) X = data[x].values Y = data[y].values dX = data[x].diff() dY = data[y].diff() indexes = data.index fig_dict = {"data": [], "layout": {}, "frames": []} _built_figure(fig_dict, animation_duration) sliders_dict = { "active": 0, "yanchor": "top", "xanchor": "left", "currentvalue": { "font": {"size": 20}, "prefix": "Index:", "visible": True, "xanchor": "right", }, "transition": {"duration": 300, "easing": "cubic-in-out"}, "pad": {"b": 10, "t": 50}, "len": 0.9, "x": 0.1, "y": 0.0, "steps": [], } edges = { "x": X, "y": Y, "mode": "lines", "line": {"color": path_color, "width": path_width}, "name": "saccades", } if aoi is not None and aoi_c is None: aoi_c = {} areas = data[aoi].unique() for area in areas: color = ( np.random.randint(0, 255), np.random.randint(0, 255), np.random.randint(0, 255), ) aoi_c[area] = f"rgb({color[0]}, {color[1]}, {color[2]})" if add_regression: mask = _select_regressions(dX, dY, rule, deviation) reg_ind_x = dX[mask].index reg_ind = [] for i in reg_ind_x: if i != 0: reg_ind.append(i - 1) reg_ind.append(i) data["is_reg"] = [1 if z in reg_ind else 0 for z in data.index] edges = [] first_reg = True first_sac = True for i in range(1, len(data)): if data.loc[i - 1, "is_reg"] == data.loc[i, "is_reg"] == 1: edges.append( { "x": [data.loc[i - 1, x], data.loc[i, x]], "y": [data.loc[i - 1, y], data.loc[i, y]], "mode": "lines", "line": {"color": regression_color, "width": path_width}, "name": "regressions", "showlegend": first_reg, } ) first_reg = False else: edges.append( { "x": [data.loc[i - 1, x], data.loc[i, x]], "y": [data.loc[i - 1, y], data.loc[i, y]], "mode": "lines", "line": {"color": path_color, "width": path_width}, "name": "saccades", "showlegend": first_sac, } ) first_sac = False fig_dict["data"].extend(edges) else: fig_dict["data"].append(edges) if aoi is not None: areas = data[aoi].unique() nodes = [] for area in areas: annotate = [] data_area = data[data[aoi] == area] indexes_area = data_area.index for i in range(len(indexes_area)): if meta_data is not None: row = data_area.loc[ indexes_area[i], data_area.columns.intersection(meta_data) ].values comments = [] for j in range(len(meta_data)): comments.append(f"{meta_data[j]}: {row[j]}") annotate.append("<br>".join(comments)) nodes.append( { "x": data_area[x].values, "y": data_area[y].values, "mode": "markers", "marker": {"color": aoi_c[area], "size": points_width}, "name": area, "text": annotate, } ) fig_dict["data"].extend(nodes) else: annotate = [] for i in range(len(indexes)): if meta_data is not None: row = data.loc[indexes[i], data.columns.intersection(meta_data)].values comments = [] for j in range(len(meta_data)): comments.append(f"{meta_data[j]}: {row[j]}") annotate.append("<br>".join(comments)) nodes = { "x": X, "y": Y, "mode": "markers", "marker": {"color": points_color, "size": points_width}, "name": "fixations", "text": annotate, } fig_dict["data"].append(nodes) fig_dict["data"].append( { "x": [X[0]], "y": [Y[0]], "mode": "markers", "marker": {"color": tracker_color}, "name": "tracker", } ) gif_list = [] for i in range(len(indexes)): frame = {"data": [], "name": str(i)} if add_regression: frame["data"].extend(edges) else: frame["data"].append(edges) if aoi is not None: frame["data"].extend(nodes) else: frame["data"].append(nodes) frame["data"].append( { "x": [X[i]], "y": [Y[i]], "mode": "markers", "marker": {"color": tracker_color}, "name": "tracker", } ) fig_dict["frames"].append(frame) if save_gif is not None: img = Image.open(io.BytesIO(go.Figure(frame).to_image(format="png"))) for _ in range(frames_count): gif_list.append(img) slider_step = { "args": [ [str(i)], { "frame": {"duration": 300, "redraw": False}, "mode": "immediate", "transition": {"duration": 300}, }, ], "label": str(i), "method": "animate", } sliders_dict["steps"].append(slider_step) fig_dict["layout"]["sliders"] = [sliders_dict] fig = go.Figure(fig_dict) fig.show() if save_gif is not None: gif_list[0].save( save_gif, save_all=True, append_images=gif_list[1:], duration=1000, loop=0, fps=1, )
[docs] def scanpath_animation( data_: pd.DataFrame, x: str, y: str, path_color: str = "green", path_width: float = 0.5, points_color: str = "black", points_width: float = 6, add_regression: bool = False, regression_color: str = "red", rule: tuple[int, ...] = None, deviation: int | tuple[int, ...] = None, animation_duration: int = 500, save_gif: str = None, frames_count: int = 1, ): """Function for tracker animation. Args: data_: DataFrame with fixations. x: x coordinate of fixation. y: y coordinate of fixation. path_color: color of saccades. path_width: width of saccades. points_color: color of points. points_width: width of points. add_regression: whether to add regressions. regression_color: color of regressions. rule: must be either 1) tuple of quadrants direction to classify regressions, 1st quadrant being upper-right square of plane and counting anti-clockwise or 2) tuple of angles in degrees(0 <= angle <= 360). deviation: if None, then `rule` is interpreted as quadrants. Otherwise, `rule` is interpreted as angles. If integer, then is a +-deviation for all angles. If tuple of integers, then must be of the same length as `rule`, each value being a corresponding deviation for each angle. Angle = 0 is positive x-axis direction, rotating anti-clockwise. animation_duration: duration of animation. save_gif: path to save animation. frames_count: TODO. """ data = data_.reset_index(drop=True) X = data[x].values Y = data[y].values x_min, x_max, y_min, y_max = X.min(), X.max(), Y.min(), Y.max() indexes = data.index fig_dict = {"data": [], "layout": {}, "frames": []} _built_figure(fig_dict, animation_duration) sliders_dict = { "active": 0, "yanchor": "top", "xanchor": "left", "currentvalue": { "font": {"size": 20}, "prefix": "Index:", "visible": True, "xanchor": "right", }, "transition": {"duration": 300, "easing": "cubic-in-out"}, "pad": {"b": 10, "t": 50}, "len": 0.9, "x": 0.1, "y": 0.0, "steps": [], } fig_dict["layout"]["xaxis"] = { "range": [x_min - 0.1, x_max + 0.1], "automargin": False, } fig_dict["layout"]["yaxis"] = { "range": [y_min - 0.1, y_max + 0.1], "automargin": False, } fig_dict["data"].extend( [ { "x": [], "y": [], "mode": "lines", "line": {"color": path_color, "width": path_width}, } for _ in range(len(indexes)) ] ) graph = [] if add_regression: dX = data[x].diff() dY = data[y].diff() mask = _select_regressions(dX, dY, rule, deviation) reg_ind_x = dX[mask].index reg_ind = [] for i in reg_ind_x: if i != 0: reg_ind.append(i - 1) reg_ind.append(i) data["is_reg"] = [1 if z in reg_ind else 0 for z in data.index] gif_list = [] for i in range(1, len(indexes)): frame = {"data": [], "name": str(i), "layout": {"xaxis": {}, "yaxis": {}}} color = path_color name = "saccedes" if add_regression and (data.loc[i - 1, "is_reg"] == data.loc[i, "is_reg"] == 1): color = regression_color name = "regression" new_edge = { "x": [data.loc[i - 1, x], data.loc[i, x]], "y": [data.loc[i - 1, y], data.loc[i, y]], "mode": "lines+markers", "line": {"color": color, "width": path_width}, "marker": {"color": points_color, "size": points_width}, "name": name, "showlegend": False, } frame["data"].extend(graph) frame["data"].append(new_edge) frame["layout"]["xaxis"] = { "range": [x_min - 0.1, x_max + 0.1], "automargin": False, } frame["layout"]["yaxis"] = { "range": [y_min - 0.1, y_max + 0.1], "automargin": False, } fig_dict["frames"].append(frame) if save_gif is not None: img = Image.open(io.BytesIO(go.Figure(frame).to_image(format="png"))) for _ in range(frames_count): gif_list.append(img) graph.append(new_edge) slider_step = { "args": [ [str(i)], { "frame": {"duration": 300, "redraw": False}, "mode": "immediate", "transition": {"duration": 300}, }, ], "label": str(i), "method": "animate", } sliders_dict["steps"].append(slider_step) fig_dict["layout"]["sliders"] = [sliders_dict] fig = go.Figure(fig_dict) fig.show() if save_gif is not None: gif_list[0].save( save_gif, save_all=True, append_images=gif_list[1:], duration=1000, fps=1, loop=0, )