Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dotplot layer artist for histogram #58

Merged
merged 4 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion glue_plotly/common/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def rgb_colors(layer_state, mask, cmap_att):
def color_info(layer_state, mask=None,
mode_att="cmap_mode",
cmap_att="cmap_att"):
if getattr(layer_state, mode_att) == "Fixed":
if getattr(layer_state, mode_att, "Fixed") == "Fixed":
return fixed_color(layer_state)
else:
return rgb_colors(layer_state, mask, cmap_att)
52 changes: 52 additions & 0 deletions glue_plotly/common/dotplot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from uuid import uuid4

from plotly.graph_objs import Scatter

from glue.core import BaseData

from .common import color_info, dimensions


def dot_radius(viewer, layer_state):
edges = layer_state.histogram[0]
viewer_state = viewer.state
diam_world = min([edges[i + 1] - edges[i] for i in range(len(edges) - 1)])
width, height = dimensions(viewer)
diam = diam_world * width / abs(viewer_state.x_max - viewer_state.x_min)
if viewer_state.y_min is not None and viewer_state.y_max is not None:
max_diam_world_v = 1
diam_pixel_v = max_diam_world_v * height / abs(viewer_state.y_max - viewer_state.y_min)
diam = min(diam_pixel_v, diam)
return diam / 2


def traces_for_layer(viewer, layer_state, add_data_label=True):
legend_group = uuid4().hex
dots_id = uuid4().hex

x = []
y = []
edges, counts = layer_state.histogram
counts = counts.astype(int)
for i in range(len(edges) - 1):
x_i = (edges[i] + edges[i + 1]) / 2
y_i = range(1, counts[i] + 1)
x.extend([x_i] * counts[i])
y.extend(y_i)

radius = dot_radius(viewer, layer_state)
marker = dict(color=color_info(layer_state, mask=None), size=radius)

name = layer_state.layer.label
if add_data_label and not isinstance(layer_state.layer, BaseData):
name += " ({0})".format(layer_state.layer.data.label)

Check warning on line 42 in glue_plotly/common/dotplot.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/common/dotplot.py#L42

Added line #L42 was not covered by tests

return [Scatter(
x=x,
y=y,
mode="markers",
marker=marker,
name=name,
legendgroup=legend_group,
meta=dots_id,
)]
2 changes: 1 addition & 1 deletion glue_plotly/common/scatter2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def size_info(layer_state, mask=None):
return s


def base_marker(layer_state, mask):
def base_marker(layer_state, mask=None):
color = color_info(layer_state, mask)
marker = dict(size=size_info(layer_state, mask),
color=color,
Expand Down
78 changes: 78 additions & 0 deletions glue_plotly/common/tests/test_dotplot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from numpy import unique
from plotly.graph_objs import Scatter

from glue.core import Data
from glue_qt.app import GlueApplication
from glue_qt.viewers.histogram import HistogramViewer

from glue_plotly.common import sanitize
from glue_plotly.common.dotplot import traces_for_layer

from glue_plotly.viewers.histogram.viewer import PlotlyHistogramView
from glue_plotly.viewers.histogram.dotplot_layer_artist import PlotlyDotplotLayerArtist


class SimpleDotplotViewer(PlotlyHistogramView):
_data_artist_cls = PlotlyDotplotLayerArtist
_subset_artist_cls = PlotlyDotplotLayerArtist


class TestDotplot:

def setup_method(self, method):
x = [86, 86, 76, 78, 93, 100, 90, 87, 73, 61, 71, 68, 78,
9, 87, 32, 34, 2, 57, 79, 48, 5, 8, 19, 7, 78,
16, 15, 58, 34, 20, 63, 96, 97, 86, 92, 35, 59, 75,
0, 53, 45, 59, 74, 59, 4, 69, 76, 97, 77, 24, 99,
50, 6, 1, 55, 13, 40, 27, 17, 92, 72, 40, 29, 64,
38, 77, 11, 91, 23, 59, 92, 5, 88, 15, 90, 40, 100,
47, 28, 3, 44, 89, 75, 13, 94, 95, 43, 17, 88, 6,
94, 100, 28, 45, 36, 63, 14, 90, 66]
self.data = Data(label="dotplot", x=x)
self.app = GlueApplication()
self.app.session.data_collection.append(self.data)
self.viewer = self.app.new_data_viewer(HistogramViewer)
self.viewer.add_data(self.data)
self.mask, self.sanitized = sanitize(self.data['x'])

viewer_state = self.viewer.state
viewer_state.hist_n_bin = 18
viewer_state.x_axislabel_size = 14
viewer_state.y_axislabel_size = 8
viewer_state.x_ticklabel_size = 18
viewer_state.y_ticklabel_size = 20
viewer_state.x_min = 0
viewer_state.x_max = 100
viewer_state.y_min = 0
viewer_state.y_max = 15
viewer_state.x_axislabel = 'X Axis'
viewer_state.y_axislabel = 'Y Axis'

self.layer = self.viewer.layers[0]
self.layer.state.color = '#0e1dab'
self.layer.state.alpha = 0.85

def teardown_method(self, method):
self.viewer.close(warn=False)
self.viewer = None
self.app.close()
self.app = None

def test_basic_dots(self):
traces = traces_for_layer(self.viewer, self.layer.state)
assert len(traces) == 1
dots = traces[0]
assert isinstance(dots, Scatter)

assert len(unique(dots.x)) == 18
expected_y = (1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 1,
2, 3, 4, 5, 6, 1, 2, 3, 4, 1, 2, 3, 1, 2,
3, 4, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2,
3, 4, 5, 1, 2, 1, 2, 3, 4, 5, 6, 7, 1, 2,
3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8,
1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 1, 2, 3,
4, 5, 6, 7, 8, 9, 10, 11, 1, 2, 3, 4, 5,
6, 7, 8)

assert dots.y == expected_y
assert dots.marker.size == 16 # Default figure is 640x480
160 changes: 160 additions & 0 deletions glue_plotly/viewers/histogram/dotplot_layer_artist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# NB: This dot plot layer artist shouldn't be used together with the
# normalized mode, as a dotplot only makes sense when the heights are integral.

import numpy as np

from glue.core.exceptions import IncompatibleAttribute
from glue.viewers.common.layer_artist import LayerArtist
from glue.viewers.histogram.state import HistogramLayerState
from glue_plotly.common.common import fixed_color

from glue_plotly.common.dotplot import dot_radius, traces_for_layer

__all__ = ["PlotlyDotplotLayerArtist"]

SCALE_PROPERTIES = {'y_log', 'normalize', 'cumulative'}
HISTOGRAM_PROPERTIES = SCALE_PROPERTIES | {'layer', 'x_att', 'hist_x_min',
'hist_x_max', 'hist_n_bin', 'x_log'}

# Note that, because we need to scale the dots based on pixel space due to how Plotly sizes scatters,
# we need to update the dot sizing when the bounds change
VISUAL_PROPERTIES = {'alpha', 'color', 'zorder', 'visible', 'x_min', 'x_max', 'y_min', 'y_max'}
DATA_PROPERTIES = {'layer', 'x_att', 'y_att'}


class PlotlyDotplotLayerArtist(LayerArtist):

_layer_state_cls = HistogramLayerState

def __init__(self, view, viewer_state, layer_state=None, layer=None):
super().__init__(

Check warning on line 30 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L30

Added line #L30 was not covered by tests
viewer_state,
layer_state=layer_state,
layer=layer
)

self.view = view
self.bins = None
self._dots_id = None

Check warning on line 38 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L36-L38

Added lines #L36 - L38 were not covered by tests

self._viewer_state.add_global_callback(self._update_dotplot)
self.state.add_global_callback(self._update_dotplot)

Check warning on line 41 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L40-L41

Added lines #L40 - L41 were not covered by tests

def _get_dots(self):
return self.view.figure.select_traces(dict(meta=self._dots_id))

Check warning on line 44 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L44

Added line #L44 was not covered by tests

def traces(self):
return self._get_dots()

Check warning on line 47 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L47

Added line #L47 was not covered by tests

def _calculate_histogram(self):
try:
self.state.reset_cache()
self.bins, self.hist_unscaled = self.state.histogram
except IncompatibleAttribute:
self.disable('Could not compute histogram')
self.bins = self.hist_unscaled = None

Check warning on line 55 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L50-L55

Added lines #L50 - L55 were not covered by tests

def _scale_histogram(self):

if self.bins is None:
return # can happen when the subset is empty

Check warning on line 60 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L59-L60

Added lines #L59 - L60 were not covered by tests

if self.bins.size == 0:
return

Check warning on line 63 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L62-L63

Added lines #L62 - L63 were not covered by tests

with self.view.figure.batch_update():

Check warning on line 65 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L65

Added line #L65 was not covered by tests

# We have to do the following to make sure that we reset the y_max as
# needed. We can't simply reset based on the maximum for this layer
# because other layers might have other values, and we also can't do:
#
# self._viewer_state.y_max = max(self._viewer_state.y_max, result[0].max())
#
# because this would never allow y_max to get smaller.

_, hist = self.state.histogram
self.state._y_max = hist.max()
if self._viewer_state.y_log:
self.state._y_max *= 2

Check warning on line 78 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L75-L78

Added lines #L75 - L78 were not covered by tests
else:
self.state._y_max *= 1.2

Check warning on line 80 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L80

Added line #L80 was not covered by tests

if self._viewer_state.y_log:
keep = hist > 0
if np.any(keep):
self.state._y_min = hist[keep].min() / 10

Check warning on line 85 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L82-L85

Added lines #L82 - L85 were not covered by tests
else:
self.state._y_min = 0

Check warning on line 87 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L87

Added line #L87 was not covered by tests
else:
self.state._y_min = 0

Check warning on line 89 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L89

Added line #L89 was not covered by tests

largest_y_max = max(getattr(layer, '_y_max', 0)

Check warning on line 91 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L91

Added line #L91 was not covered by tests
for layer in self._viewer_state.layers)
if np.isfinite(largest_y_max) and largest_y_max != self._viewer_state.y_max:
self._viewer_state.y_max = largest_y_max

Check warning on line 94 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L93-L94

Added lines #L93 - L94 were not covered by tests

smallest_y_min = min(getattr(layer, '_y_min', np.inf)

Check warning on line 96 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L96

Added line #L96 was not covered by tests
for layer in self._viewer_state.layers)
if np.isfinite(smallest_y_min) and smallest_y_min != self._viewer_state.y_min:
self._viewer_state.y_min = smallest_y_min

Check warning on line 99 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L98-L99

Added lines #L98 - L99 were not covered by tests

def _update_visual_attributes(self, changed, force=False):
if not self.enabled:
return

Check warning on line 103 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L102-L103

Added lines #L102 - L103 were not covered by tests

with self.view.figure.batch_update():
self.view.figure.for_each_trace(self._update_visual_attrs_for_trace, dict(meta=self._dots_id))

Check warning on line 106 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L105-L106

Added lines #L105 - L106 were not covered by tests

def _update_visual_attrs_for_trace(self, trace):
marker = trace.marker
marker.update(opacity=self.state.alpha, color=fixed_color(self.state), size=dot_radius(self.view, self.state))
print(marker)
trace.update(marker=marker,

Check warning on line 112 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L109-L112

Added lines #L109 - L112 were not covered by tests
visible=self.state.visible,
unselected=dict(marker=dict(opacity=self.state.alpha)))

def _update_data(self):
old_dots = self._get_dots()
if old_dots:
self.view._remove_traces(old_dots)

Check warning on line 119 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L117-L119

Added lines #L117 - L119 were not covered by tests

dots = traces_for_layer(self.view, self.state, add_data_label=True)
self._dots_id = dots[0].meta if dots else None
self.view.figure.add_traces(dots)

Check warning on line 123 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L121-L123

Added lines #L121 - L123 were not covered by tests

def _update_zorder(self):
traces = [self.view.selection_layer]
for layer in self.view.layers:
traces += list(layer.traces())
self.view.figure.data = traces

Check warning on line 129 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L126-L129

Added lines #L126 - L129 were not covered by tests

def _update_dotplot(self, force=False, **kwargs):
if (self._viewer_state.hist_x_min is None or

Check warning on line 132 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L132

Added line #L132 was not covered by tests
self._viewer_state.hist_x_max is None or
self._viewer_state.hist_n_bin is None or
self._viewer_state.x_att is None or
self.state.layer is None):
return

Check warning on line 137 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L137

Added line #L137 was not covered by tests

changed = self.pop_changed_properties()

Check warning on line 139 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L139

Added line #L139 was not covered by tests

if force or len(changed & HISTOGRAM_PROPERTIES) > 0:
self._calculate_histogram()
force = True

Check warning on line 143 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L141-L143

Added lines #L141 - L143 were not covered by tests

if force or len(changed & DATA_PROPERTIES) > 0:
self._update_data()
force = True

Check warning on line 147 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L145-L147

Added lines #L145 - L147 were not covered by tests

if force or len(changed & SCALE_PROPERTIES) > 0:
self._scale_histogram()

Check warning on line 150 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L149-L150

Added lines #L149 - L150 were not covered by tests

if force or len(changed & VISUAL_PROPERTIES) > 0:
self._update_visual_attributes(changed, force=force)

Check warning on line 153 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L152-L153

Added lines #L152 - L153 were not covered by tests

if force or "zorder" in changed:
self._update_zorder()

Check warning on line 156 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L155-L156

Added lines #L155 - L156 were not covered by tests

def update(self):
self.state.reset_cache()
self._update_dotplot(force=True)

Check warning on line 160 in glue_plotly/viewers/histogram/dotplot_layer_artist.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/viewers/histogram/dotplot_layer_artist.py#L159-L160

Added lines #L159 - L160 were not covered by tests
Loading