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

Adds style to node serialization. #317

Merged
merged 2 commits into from
Oct 11, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 8 additions & 1 deletion podpac/core/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import warnings
import importlib
from collections import OrderedDict
from copy import deepcopy
from hashlib import md5 as hash_alg

import numpy as np
Expand Down Expand Up @@ -318,6 +319,9 @@ def base_definition(self):
if lookup_attrs:
d["lookup_attrs"] = OrderedDict([(key, lookup_attrs[key]) for key in sorted(lookup_attrs.keys())])

if self.style.definition:
d["style"] = self.style.definition

return d

@property
Expand Down Expand Up @@ -572,7 +576,7 @@ def from_definition(cls, definition):

# parse and configure kwargs
kwargs = {}
whitelist = ["node", "attrs", "lookup_attrs", "plugin"]
whitelist = ["node", "attrs", "lookup_attrs", "plugin", "style"]

# DataSource, Compositor, and Algorithm specific properties
parents = inspect.getmro(node_class)
Expand Down Expand Up @@ -645,6 +649,9 @@ def from_definition(cls, definition):
for k, v in d.get("lookup_attrs", {}).items():
kwargs[k] = _get_subattr(nodes, name, v)

if "style" in d:
kwargs["style"] = Style.from_definition(d["style"])

for k in d:
if k not in whitelist:
raise ValueError("Invalid definition for node '%s': unexpected property '%s'" % (name, k))
Expand Down
6 changes: 5 additions & 1 deletion podpac/core/pipeline/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from podpac.core.settings import settings
from podpac.core.utils import OrderedDictTrait, JSONEncoder
from podpac.core.node import Node, NodeException
from podpac.core.style import Style
from podpac.core.data.datasource import DataSource
from podpac.core.data.types import ReprojectedSource, Array
from podpac.core.algorithm.algorithm import Algorithm
Expand Down Expand Up @@ -166,7 +167,7 @@ def _parse_node_definition(nodes, name, d):

# parse and configure kwargs
kwargs = {}
whitelist = ["node", "attrs", "lookup_attrs", "plugin"]
whitelist = ["node", "attrs", "lookup_attrs", "plugin", "style"]

# DataSource, Compositor, and Algorithm specific properties
parents = inspect.getmro(node_class)
Expand Down Expand Up @@ -213,6 +214,9 @@ def _parse_node_definition(nodes, name, d):
for k, v in d.get("lookup_attrs", {}).items():
kwargs[k] = _get_subattr(nodes, name, v)

if "style" in d:
kwargs["style"] = Style.from_definition(d["style"])

for key in d:
if key not in whitelist:
raise PipelineError("node '%s' has unexpected property %s" % (name, key))
Expand Down
77 changes: 43 additions & 34 deletions podpac/core/style.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from __future__ import division, print_function, absolute_import

import json
import six
from collections import OrderedDict

import traitlets as tl
import matplotlib
import matplotlib.cm
from matplotlib.colors import ListedColormap
import json
from collections import OrderedDict

from podpac.core.units import ureg
from podpac.core.utils import trait_is_defined, JSONEncoder
Expand All @@ -16,20 +18,20 @@ class Style(tl.HasTraits):

Attributes
----------
clim : TYPE
Description
cmap : TYPE
Description
enumeration_colors : TYPE
Description
enumeration_legend : TYPE
Description
is_enumerated : TYPE
Description
name : TYPE
Description
name : str
data name
units : TYPE
Description
data units
clim : list
[low, high], color map limits
colormap : str
matplotlib colormap name
cmap : matplotlib.cm.ColorMap
matplotlib colormap property
enumeration_colors : tuple
data colors (replaces colormap/cmap)
enumeration_legend : tuple
data legend, should correspond with enumeration_colors
"""

def __init__(self, node=None, *args, **kwargs):
Expand All @@ -40,19 +42,33 @@ def __init__(self, node=None, *args, **kwargs):

name = tl.Unicode()
units = tl.Unicode(allow_none=True)

is_enumerated = tl.Bool(allow_none=True, default_value=None)
clim = tl.List(default_value=[None, None])
colormap = tl.Unicode(allow_none=True, default_value=None)
enumeration_legend = tl.Tuple(trait=tl.Unicode)
enumeration_colors = tl.Tuple(trait=tl.Tuple)

clim = tl.List(default_value=[None, None])
cmap = tl.Instance("matplotlib.colors.Colormap")
@tl.validate("colormap")
def _validate_colormap(self, d):
if isinstance(d["value"], six.string_types):
matplotlib.cm.get_cmap(d["value"])
if d["value"] and self.enumeration_colors:
raise TypeError("Style can have a colormap or enumeration_colors, but not both")
return d["value"]

@tl.validate("enumeration_colors")
def _validate_enumeration_colors(self, d):
if d["value"] and self.colormap:
raise TypeError("Style can have a colormap or enumeration_colors, but not both")
return d["value"]

@tl.default("cmap")
def _cmap_default(self):
if self.is_enumerated and self.enumeration_colors:
@property
def cmap(self):
if self.colormap:
return matplotlib.cm.get_cmap(self.colormap)
elif self.enumeration_colors:
return ListedColormap(self.enumeration_colors)
return matplotlib.cm.get_cmap("viridis")
else:
return matplotlib.cm.get_cmap("viridis")

@property
def json(self):
Expand All @@ -74,25 +90,18 @@ def definition(self):
d["name"] = self.name
if self.units:
d["units"] = self.units
if self.is_enumerated is not None:
d["is_enumerated"] = self.is_enumerated
if self.is_enumerated and self.enumeration_legend:
if self.colormap:
d["colormap"] = self.colormap
if self.enumeration_legend:
d["enumeration_legend"] = self.enumeration_legend
if self.is_enumerated and self.enumeration_colors:
if self.enumeration_colors:
d["enumeration_colors"] = self.enumeration_colors
else:
d["cmap"] = self.cmap.name
if self.clim != [None, None]:
d["clim"] = self.clim
return d

@classmethod
def from_definition(cls, d):
if "cmap" in d:
if d["cmap"] == "from_list":
del d["cmap"]
else:
d["cmap"] = matplotlib.cm.get_cmap(d["cmap"])
return cls(**d)

@classmethod
Expand Down
25 changes: 25 additions & 0 deletions podpac/core/test/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from collections import OrderedDict
import json
import six
from copy import deepcopy

try:
import urllib.parse as urllib
Expand All @@ -23,6 +24,7 @@
from podpac.core import common_test_utils as ctu
from podpac.core.utils import ArrayTrait
from podpac.core.units import UnitsDataArray
from podpac.core.style import Style
from podpac.core.node import Node, NodeException
from podpac.core.cache import CacheCtrl, RamCacheStore

Expand Down Expand Up @@ -619,6 +621,29 @@ def test_pipeline(self):
p = n.pipeline
assert isinstance(p, podpac.pipeline.Pipeline)

def test_style(self):
node = podpac.data.Array(
source=[10, 20, 30],
native_coordinates=podpac.Coordinates([[0, 1, 2]], dims=["lat"]),
style=Style(name="test", units="m"),
)

d = node.definition
assert "style" in d[node.base_ref]

node2 = Node.from_definition(d)
assert node2 is not node
assert isinstance(node2, podpac.data.Array)
assert node2.style is not node.style
assert node2.style == node.style
assert node2.style.name == "test"
assert node2.style.units == "m"

# default style
node = podpac.data.Array(source=[10, 20, 30], native_coordinates=podpac.Coordinates([[0, 1, 2]], dims=["lat"]))
d = node.definition
assert "style" not in d[node.base_ref]


class TestUserDefinition(object):
def test_empty(self):
Expand Down
31 changes: 16 additions & 15 deletions podpac/core/test/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from collections import OrderedDict
from matplotlib import pyplot
import pytest

from podpac.core.style import Style

Expand All @@ -20,59 +21,59 @@ def test_cmap(self):
style = Style()
assert style.cmap.name == "viridis"

style = Style(cmap=pyplot.get_cmap("cividis"))
style = Style(colormap="cividis")
assert style.cmap.name == "cividis"

style = Style(is_enumerated=True, enumeration_colors=("c", "k"))
style = Style(enumeration_colors=("c", "k"))
assert style.cmap.name == "from_list"
assert style.cmap.colors == ("c", "k")

with pytest.raises(TypeError, match="Style can have a colormap or enumeration_colors"):
style = Style(colormap="cividis", enumeration_colors=("c", "k"))

def test_serialization(self):
# default
style = Style()
d = style.definition
assert isinstance(d, OrderedDict)
assert set(d.keys()) == {"cmap"}
assert d["cmap"] == "viridis"
assert len(d.keys()) == 0

s = Style.from_json(style.json)
assert isinstance(s, Style)

# with traits
style = Style(name="test", units="meters", cmap=pyplot.get_cmap("cividis"), clim=(-1, 1))
style = Style(name="test", units="meters", colormap="cividis", clim=(-1, 1))
d = style.definition
assert isinstance(d, OrderedDict)
assert set(d.keys()) == {"name", "units", "cmap", "clim"}
assert set(d.keys()) == {"name", "units", "colormap", "clim"}
assert d["name"] == "test"
assert d["units"] == "meters"
assert d["cmap"] == "cividis"
assert d["colormap"] == "cividis"
assert d["clim"] == [-1, 1]

s = Style.from_json(style.json)
assert s.name == style.name
assert s.units == style.units
assert s.cmap == style.cmap
assert s.colormap == style.colormap
assert s.clim == style.clim

# enumeration traits
style = Style(is_enumerated=True, enumeration_legend=("apples", "oranges"), enumeration_colors=["r", "o"])
style = Style(enumeration_legend=("apples", "oranges"), enumeration_colors=["r", "o"])
d = style.definition
assert isinstance(d, OrderedDict)
assert set(d.keys()) == {"is_enumerated", "enumeration_legend", "enumeration_colors"}
assert d["is_enumerated"] == True
assert set(d.keys()) == {"enumeration_legend", "enumeration_colors"}
assert d["enumeration_legend"] == ("apples", "oranges")
assert d["enumeration_colors"] == ("r", "o")

s = Style.from_json(style.json)
assert s.is_enumerated == style.is_enumerated
assert s.enumeration_legend == style.enumeration_legend
assert s.enumeration_colors == style.enumeration_colors
assert s.cmap.colors == style.cmap.colors

def test_eq(self):
style1 = Style(units="meters")
style2 = Style(units="meters")
style3 = Style(units="feet")
style1 = Style(name="test")
style2 = Style(name="test")
style3 = Style(name="other")

assert style1 is not style2
assert style1 is not style3
Expand Down
1 change: 0 additions & 1 deletion podpac/datalib/drought_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class DroughtCategory(Algorithm):
d4 = NodeTrait()
style = Style(
clim=[0, 6],
is_enumerated=True,
enumeration_colors=[
[0.45098039, 0.0, 0.0, 1.0],
[0.90196078, 0.0, 0.0, 1.0],
Expand Down