Skip to content

WIP: adding themes #924

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

Closed
wants to merge 3 commits into from
Closed
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: 2 additions & 0 deletions plotly/graph_objs/__init__.py
Original file line number Diff line number Diff line change
@@ -12,3 +12,5 @@
from __future__ import absolute_import

from plotly.graph_objs.graph_objs import * # this is protected with __all__

from plotly.graph_objs.theme_lib import THEMES
761 changes: 530 additions & 231 deletions plotly/graph_objs/graph_objs.py

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions plotly/graph_objs/graph_objs_tools.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import absolute_import
import itertools
import textwrap
import six

@@ -268,3 +269,88 @@ def sort_keys(key):
"""
is_special = key in 'rtxyz'
return not is_special, key


class Cycler(object):
"""
An object that repeats indefinitely by cycling through a collection of
values
Usually used in a PlotlyTheme to set things like a sequence of trace colors
that should be applied.
"""
def __init__(self, vals):
self.vals = vals
self.n = len(vals)
self.cycler = itertools.cycle(vals)

def next(self):
return self.cycler.__next__()

def __getitem__(self, ix):
return self.vals[ix % self.n]

def reset(self):
self.cycler = itertools.cycle(self.vals)


def _reset_cyclers(obj):
if isinstance(obj, Cycler):
obj.reset()
return

if isinstance(obj, dict):
for val in obj.values():
_reset_cyclers(val)

if isinstance(obj, (list, tuple)):
for val in obj:
_reset_cyclers(val)


def _apply_theme_axis(fig, theme, ax, force):
long_ax = ax+"axis"

def apply_at_root(root):
ax_names = list(filter(lambda x: x.startswith(long_ax), root.keys()))

for ax_name in ax_names:
# update theme with data from fig, so the fig data takes
# precedence
new = theme.layout[long_ax].copy()
new.update(root[ax_name])
root[ax_name] = new

if len(ax_names) == 0:
root[long_ax] = theme.layout[long_ax].copy()

if long_ax in theme.layout or force:
apply_at_root(fig.layout)

if long_ax in theme.layout.scene or force:
apply_at_root(fig.layout.scene) # also apply to 3d scene


def _maybe_set_attr(obj, key, val):
"""
Set obj[key] = val _only_ when obj[key] is valid and blank
obj should be an instance of PlotlyDict. As this is an internal method
that should only be invoked by plotly.graph_objs.Figure.apply_theme
this should never be an issue.
"""
if isinstance(val, Cycler):
# if we have a cycler, extract the current value and apply it
_maybe_set_attr(obj, key, val.next())
return

if key in obj._get_valid_attributes(): # is valid
if isinstance(val, dict): # recurse into dict
for new_key, new_val in val.items():
_maybe_set_attr(obj[key], new_key, new_val)

else:
# TODO: should probably enumerate more type checks, but hopefully
# at this point we can just set the value
if key not in obj:
obj[key] = val
116 changes: 116 additions & 0 deletions plotly/graph_objs/theme_lib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#
# Note that the following themes used values from the matplotlib style library
# (https://github.com/matplotlib/matplotlib/tree/master/lib/matplotlib/mpl-data/stylelib):
#
# - ggplot
# - fivethirtyeight
# - seaborn
#

from . import graph_objs as go
from .graph_objs_tools import Cycler


def ggplot_theme():
axis = dict(showgrid=True, gridcolor="#cbcbcb",
linewidth=1.0, linecolor="#f0f0f0",
ticklen=0.0, tickcolor="#555555", ticks="outside",
titlefont={"size": 12, "color": "#555555"})
layout = go.Layout(dict(
plot_bgcolor="E5E5E5", paper_bgcolor="white",
font={"size": 10}, xaxis=axis, yaxis=axis, titlefont={"size": 14}
))
marker_color = Cycler(["#E24A33", "#348ABD", "#988ED5", "#777777",
"#FBC15E", "#8EBA42", "#FFB5B8"])
global_trace = dict(marker={
"color": marker_color,
"line": {"width": 0.5, "color": "#348ABD"}
})
return go.PlotlyTheme(global_trace=global_trace, layout=layout)


def fivethirtyeight_theme():
scatter = go.Scatter(line={"width": 4})
axis = dict(showgrid=True, gridcolor="#cbcbcb",
linewidth=1.0, linecolor="#f0f0f0",
ticklen=0.0, tickcolor="#555555", ticks="outside",
titlefont=dict(size=12, color="#555555"))
layout = go.Layout(
plot_bgcolor="#f0f0f0",
paper_bgcolor="#f0f0f0",
font=dict(size=14),
xaxis=axis,
yaxis=axis,
legend=dict(borderwidth=1.0, bgcolor="f0f0f0", bordercolor="f0f0f0"),
titlefont={"size": 14})
colors = ["#008fd5", "#fc4f30", "#e5ae38", "#6d904f",
"#8b8b8b", "#810f7c"]
global_trace = dict(marker={"color": Cycler(colors)})
return go.PlotlyTheme(
global_trace=global_trace, layout=layout, scatter=scatter
)


def seaborn_theme():
heatmap = go.Heatmap(colorscale="Greys")
scatter = go.Scatter(
marker=dict(size=9, line={"width": 0}),
line={"width": 1.75}
)
axis = dict(showgrid=True, gridcolor="white",
linewidth=1.0, linecolor="white",
ticklen=0.0, tickcolor="#555555", ticks="outside",
tickfont=dict(size=10),
titlefont=dict(size=12, color="#555555"))
# TODO: major vs minor ticks...
layout = go.Layout(
plot_bgcolor="EAEAF2",
paper_bgcolor="white",
width=800,
height=550,
font=dict(family="Arial", size=14, color=0.15),
xaxis=axis,
yaxis=axis,
legend=dict(font=dict(size=10),
bgcolor="white", bordercolor="white"),
titlefont=dict(size=14))
colors = ["#4C72B0", "#55A868", "#C44E52", "#8172B2", "#CCB974", "#64B5CD"]
global_trace = {"marker": {"color": Cycler(colors)}}
return go.PlotlyTheme(
global_trace=global_trace, layout=layout, scatter=scatter,
heatmap=heatmap
)


def tomorrow_night_eighties_theme():
bgcolor = "#2d2d2d" # Background
grid_color = "#515151" # Selection
label_color = "#cccccc" # Comment
colors = ["#cc99cc", "#66cccc", "#f2777a", "#ffcc66",
"#99cc99", "#f99157", "#6699cc"]

axis = dict(showgrid=True, gridcolor=grid_color, gridwidth=0.35,
linecolor=grid_color,
titlefont=dict(color=label_color, size=14),
linewidth=1.2, tickcolor=label_color)

layout = go.Layout(
plot_bgcolor=bgcolor,
paper_bgcolor=bgcolor,
xaxis=axis,
yaxis=axis,
font=dict(size=10, color=label_color),
titlefont=dict(size=14),
margin=dict(l=65, r=65, t=65, b=65)
)

global_trace = {"marker": {"color": Cycler(colors)}}
return go.PlotlyTheme(global_trace=global_trace, layout=layout)


THEMES = {
"ggplot": ggplot_theme(),
"tomorrow_night_eighties": tomorrow_night_eighties_theme(),
"seaborn": seaborn_theme(),
"fivethirtyeight": fivethirtyeight_theme(),
}
45,489 changes: 26,246 additions & 19,243 deletions plotly/package_data/default-schema.json

Large diffs are not rendered by default.

72 changes: 70 additions & 2 deletions update_graph_objs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import print_function
import textwrap

from plotly.graph_objs import graph_objs_tools
from plotly.graph_reference import ARRAYS, CLASSES
@@ -140,6 +141,68 @@ def append_trace(self, trace, row, col):
trace['xaxis'] = ref[0]
trace['yaxis'] = ref[1]
self['data'] += [trace]
def apply_theme(self, theme):
"""
Apply the ``PlotlyTheme`` in ``theme`` to the figure
Themes can be thought of as default values -- filling in figure
attributes only when they don't already exist on the figure
Theme application adheres to the following rules:
- Non-overwriting: a theme attribute will never be applied when a
Figure attribute is already defined
- Non-destructive: themes that specify only some of the valid figure
attributes (e.g. only `layout.font.size`) will not overwrite already
specified figure parent, sibling, or children attributes. For example
if the theme only has a value for `layout.font.size`, a figure's
`layout.font.family` or `layout.title` will not be altered.
- Attributes set on `theme.layout.(x|y|z)axis` will be applied to all
axes found in the figure. For example, to set the tick length for
every xaxis in the figure, you would define
``theme.layout.xaxis.ticklen``
For more details on how to construct a ``PlotlyTheme`` see the
associated docstring
"""
if not isinstance(theme, PlotlyTheme):
msg = ("Sorry, we only know how to apply themes contained in a"
"PlotlyTheme object. Checkout the docstring for PlotlyTheme"
"and try again!")
raise ValueError(msg)
theme._reset_cyclers()
is_3d = any("3d" in x.type for x in self.data)
if len(theme.layout) > 0:
graph_objs_tools._apply_theme_axis(self, theme, "x", not is_3d)
graph_objs_tools._apply_theme_axis(self, theme, "y", not is_3d)
graph_objs_tools._apply_theme_axis(self, theme, "z", False)
# now we can let PlotlyDict.update apply the rest
new = theme.layout.copy()
# need to remove (x|y|z)axis from the theme so it doesn't ruin what
# we did above
new.pop("xaxis", None)
new.pop("yaxis", None)
new.pop("zaxis", None)
# update theme with self, so theme takes precedence
new.update(self.layout)
self.layout = new
for trace in self.data:
if len(theme.global_trace) > 0:
for k, v in theme.global_trace.items():
graph_objs_tools._maybe_set_attr(trace, k, v)
if trace.type in theme.by_trace_type:
for k, v in theme.by_trace_type[trace.type].items():
graph_objs_tools._maybe_set_attr(trace, k, v)
return self
''', file=f, end=''
)

@@ -279,6 +342,7 @@ def print_class(name, f):
elif name == 'Frames':
print_frames_patch(f)


copied_lines = get_non_generated_file_lines()
with open('./plotly/graph_objs/graph_objs.py', 'w') as graph_objs_file:

@@ -294,5 +358,9 @@ def print_class(name, f):
print_class(class_name, graph_objs_file)

# Finish off the file by only exporting plot-schema names.
print('\n__all__ = [cls for cls in graph_reference.CLASSES.keys() '
'if cls in globals()]', file=graph_objs_file)
print(textwrap.dedent("""\n
__all__ = (
[cls for cls in graph_reference.CLASSES.keys() if cls in globals()]
+ ["PlotlyTheme"]
)
"""), file=graph_objs_file)