From ece427dc1e26e25ac8519c540df118cee5eec5f6 Mon Sep 17 00:00:00 2001 From: Nicolas Kruchten Date: Sun, 29 Mar 2020 14:51:12 -0400 Subject: [PATCH 1/4] px.IdentityMap --- .../python/plotly/plotly/express/__init__.py | 2 ++ .../python/plotly/plotly/express/_core.py | 25 ++++++++++++++++--- .../tests/test_core/test_px/test_px_input.py | 15 +++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/python/plotly/plotly/express/__init__.py b/packages/python/plotly/plotly/express/__init__.py index fb334c1b97..22a6914c84 100644 --- a/packages/python/plotly/plotly/express/__init__.py +++ b/packages/python/plotly/plotly/express/__init__.py @@ -53,6 +53,7 @@ set_mapbox_access_token, defaults, get_trendline_results, + IdentityMap, ) from . import data, colors # noqa: F401 @@ -95,4 +96,5 @@ "colors", "set_mapbox_access_token", "get_trendline_results", + "IdentityMap", ] diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 613920d05f..c6da88fbd9 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -41,6 +41,24 @@ def __init__(self): defaults = PxDefaults() del PxDefaults + +class IdentityMap(object): + """ + `dict`-like object which can be passed in to arguments like `color_discrete_map` to + use the provided data values as colors, rather than mapping them to colors cycled + from `color_discrete_sequence`. + """ + + def __getitem__(self, key): + return key + + def __contains__(self, key): + return True + + def copy(self): + return self + + MAPBOX_TOKEN = None @@ -1404,9 +1422,10 @@ def make_figure(args, constructor, trace_patch={}, layout_patch={}): for col, val, m in zip(grouper, group_name, grouped_mappings): if col != one_group: key = get_label(args, col) - mapping_labels[key] = str(val) - if m.show_in_trace_name: - trace_name_labels[key] = str(val) + if not isinstance(m.val_map, IdentityMap): + mapping_labels[key] = str(val) + if m.show_in_trace_name: + trace_name_labels[key] = str(val) if m.variable == "animation_frame": frame_name = val trace_name = ", ".join(trace_name_labels.values()) diff --git a/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py index e3786f6af9..eb18084978 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py @@ -323,3 +323,18 @@ def test_size_column(): df = px.data.tips() fig = px.scatter(df, x=df["size"], y=df.tip) assert fig.data[0].hovertemplate == "size=%{x}
tip=%{y}" + + +def test_identity_map(): + fig = px.scatter( + x=[1, 2], + y=[1, 2], + symbol=["a", "b"], + color=["red", "blue"], + color_discrete_map=px.IdentityMap(), + ) + assert fig.data[0].marker.color == "red" + assert fig.data[1].marker.color == "blue" + assert "color" not in fig.data[0].hovertemplate + assert "symbol" in fig.data[0].hovertemplate + assert fig.layout.legend.title.text == "symbol" From 53c18292182cad9a9a7d8bb13df92b91668832f8 Mon Sep 17 00:00:00 2001 From: Nicolas Kruchten Date: Sun, 29 Mar 2020 15:15:11 -0400 Subject: [PATCH 2/4] px.Constant --- .../python/plotly/plotly/express/__init__.py | 2 ++ .../python/plotly/plotly/express/_core.py | 20 ++++++++++- .../tests/test_core/test_px/test_px_input.py | 34 +++++++++++++++++-- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/python/plotly/plotly/express/__init__.py b/packages/python/plotly/plotly/express/__init__.py index 22a6914c84..d27ebcc506 100644 --- a/packages/python/plotly/plotly/express/__init__.py +++ b/packages/python/plotly/plotly/express/__init__.py @@ -54,6 +54,7 @@ defaults, get_trendline_results, IdentityMap, + Constant, ) from . import data, colors # noqa: F401 @@ -97,4 +98,5 @@ "set_mapbox_access_token", "get_trendline_results", "IdentityMap", + "Constant", ] diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index c6da88fbd9..a740df9f74 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -59,6 +59,12 @@ def copy(self): return self +class Constant(object): + def __init__(self, value, label=None): + self.value = value + self.label = label + + MAPBOX_TOKEN = None @@ -937,6 +943,8 @@ def build_dataframe(args, attrables, array_attrables): else: df_output[df_input.columns] = df_input[df_input.columns] + constants = dict() + # Loop over possible arguments for field_name in attrables: # Massaging variables @@ -968,8 +976,15 @@ def build_dataframe(args, attrables, array_attrables): "pandas MultiIndex is not supported by plotly express " "at the moment." % field ) + # ----------------- argument is a constant ---------------------- + if isinstance(argument, Constant): + col_name = _check_name_not_reserved( + str(argument.label) if argument.label is not None else field, + reserved_names, + ) + constants[col_name] = argument.value # ----------------- argument is a col name ---------------------- - if isinstance(argument, str) or isinstance( + elif isinstance(argument, str) or isinstance( argument, int ): # just a column name given as str or int if not df_provided: @@ -1050,6 +1065,9 @@ def build_dataframe(args, attrables, array_attrables): else: args[field_name][i] = str(col_name) + for col_name in constants: + df_output[col_name] = constants[col_name] + args["data_frame"] = df_output return args diff --git a/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py index eb18084978..d1c00a7682 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py @@ -335,6 +335,36 @@ def test_identity_map(): ) assert fig.data[0].marker.color == "red" assert fig.data[1].marker.color == "blue" - assert "color" not in fig.data[0].hovertemplate - assert "symbol" in fig.data[0].hovertemplate + assert "color=" not in fig.data[0].hovertemplate + assert "symbol=" in fig.data[0].hovertemplate + assert fig.layout.legend.title.text == "symbol" + + +def test_constants(): + fig = px.scatter(x=px.Constant(1), y=[1, 2]) + assert fig.data[0].x[0] == 1 + assert fig.data[0].x[1] == 1 + assert "x=" in fig.data[0].hovertemplate + + fig = px.scatter(x=px.Constant(1, label="time"), y=[1, 2]) + assert fig.data[0].x[0] == 1 + assert fig.data[0].x[1] == 1 + assert "x=" not in fig.data[0].hovertemplate + assert "time=" in fig.data[0].hovertemplate + + fig = px.scatter( + x=[1, 2], + y=[1, 2], + symbol=["a", "b"], + color=px.Constant("red", label="the_identity_label"), + hover_data=[px.Constant("data", label="the_data")], + color_discrete_map=px.IdentityMap(), + ) + assert fig.data[0].marker.color == "red" + assert fig.data[0].customdata[0][0] == "data" + assert fig.data[1].marker.color == "red" + assert "color=" not in fig.data[0].hovertemplate + assert "the_identity_label=" not in fig.data[0].hovertemplate + assert "symbol=" in fig.data[0].hovertemplate + assert "the_data=" in fig.data[0].hovertemplate assert fig.layout.legend.title.text == "symbol" From 5de8217f3c4d7ec23f8f37fa8e604715e9072a31 Mon Sep 17 00:00:00 2001 From: Nicolas Kruchten Date: Mon, 30 Mar 2020 09:14:22 -0400 Subject: [PATCH 3/4] PR feedback --- .../python/plotly/plotly/express/__init__.py | 3 ++ .../python/plotly/plotly/express/_core.py | 30 ++++--------------- .../plotly/plotly/express/_special_inputs.py | 29 ++++++++++++++++++ .../tests/test_core/test_px/test_px_input.py | 13 ++++++++ 4 files changed, 51 insertions(+), 24 deletions(-) create mode 100644 packages/python/plotly/plotly/express/_special_inputs.py diff --git a/packages/python/plotly/plotly/express/__init__.py b/packages/python/plotly/plotly/express/__init__.py index d27ebcc506..9df5b21ac8 100644 --- a/packages/python/plotly/plotly/express/__init__.py +++ b/packages/python/plotly/plotly/express/__init__.py @@ -53,6 +53,9 @@ set_mapbox_access_token, defaults, get_trendline_results, +) + +from ._special_inputs import ( # noqa: F401 IdentityMap, Constant, ) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index a740df9f74..f84d7f7515 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1,6 +1,7 @@ import plotly.graph_objs as go import plotly.io as pio from collections import namedtuple, OrderedDict +from ._special_inputs import IdentityMap, Constant from _plotly_utils.basevalidators import ColorscaleValidator from .colors import qualitative, sequential @@ -42,29 +43,6 @@ def __init__(self): del PxDefaults -class IdentityMap(object): - """ - `dict`-like object which can be passed in to arguments like `color_discrete_map` to - use the provided data values as colors, rather than mapping them to colors cycled - from `color_discrete_sequence`. - """ - - def __getitem__(self, key): - return key - - def __contains__(self, key): - return True - - def copy(self): - return self - - -class Constant(object): - def __init__(self, value, label=None): - self.value = value - self.label = label - - MAPBOX_TOKEN = None @@ -161,11 +139,15 @@ def make_mapping(args, variable): if variable == "dash": arg_name = "line_dash" vprefix = "line_dash" + if args[vprefix + "_map"] == "identity": + val_map = IdentityMap() + else: + val_map = args[vprefix + "_map"].copy() return Mapping( show_in_trace_name=True, variable=variable, grouper=args[arg_name], - val_map=args[vprefix + "_map"].copy(), + val_map=val_map, sequence=args[vprefix + "_sequence"], updater=lambda trace, v: trace.update({parent: {variable: v}}), facet=None, diff --git a/packages/python/plotly/plotly/express/_special_inputs.py b/packages/python/plotly/plotly/express/_special_inputs.py new file mode 100644 index 0000000000..8118dafe0a --- /dev/null +++ b/packages/python/plotly/plotly/express/_special_inputs.py @@ -0,0 +1,29 @@ + +class IdentityMap(object): + """ + `dict`-like object which acts as if the value for any key is the key itself. Objects + of this class can be passed in to arguments like `color_discrete_map` to + use the provided data values as colors, rather than mapping them to colors cycled + from `color_discrete_sequence`. This works for any `_map` argument to Plotly Express + functions, such as `line_dash_map` and `symbol_map`. + """ + + def __getitem__(self, key): + return key + + def __contains__(self, key): + return True + + def copy(self): + return self + + +class Constant(object): + """ + Objects of this class can be passed to Plotly Express functions that expect column + identifiers or list-like objects to indicate that this attribute should take on a + constant value. An optional label can be provided. + """ + def __init__(self, value, label=None): + self.value = value + self.label = label diff --git a/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py index d1c00a7682..b075e197d7 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py @@ -339,6 +339,19 @@ def test_identity_map(): assert "symbol=" in fig.data[0].hovertemplate assert fig.layout.legend.title.text == "symbol" + fig = px.scatter( + x=[1, 2], + y=[1, 2], + symbol=["a", "b"], + color=["red", "blue"], + color_discrete_map="identity", + ) + assert fig.data[0].marker.color == "red" + assert fig.data[1].marker.color == "blue" + assert "color=" not in fig.data[0].hovertemplate + assert "symbol=" in fig.data[0].hovertemplate + assert fig.layout.legend.title.text == "symbol" + def test_constants(): fig = px.scatter(x=px.Constant(1), y=[1, 2]) From 918b87b49a7093a709698742353a92d3895d4309 Mon Sep 17 00:00:00 2001 From: Nicolas Kruchten Date: Mon, 30 Mar 2020 09:34:26 -0400 Subject: [PATCH 4/4] black --- packages/python/plotly/plotly/express/_special_inputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/python/plotly/plotly/express/_special_inputs.py b/packages/python/plotly/plotly/express/_special_inputs.py index 8118dafe0a..3dfff0f3c8 100644 --- a/packages/python/plotly/plotly/express/_special_inputs.py +++ b/packages/python/plotly/plotly/express/_special_inputs.py @@ -1,4 +1,3 @@ - class IdentityMap(object): """ `dict`-like object which acts as if the value for any key is the key itself. Objects @@ -24,6 +23,7 @@ class Constant(object): identifiers or list-like objects to indicate that this attribute should take on a constant value. An optional label can be provided. """ + def __init__(self, value, label=None): self.value = value self.label = label