diff --git a/contributing.md b/contributing.md
index 09989e2296e..82ed16b20ba 100644
--- a/contributing.md
+++ b/contributing.md
@@ -213,7 +213,7 @@ This is the release process for releasing `plotly.py` version `X.Y.Z` with
`plotlywidget` version `A.B.C`.
Note: The `plotlywidget` instructions must be followed if any change
-has been made in the `js/` directory source code, OR if the version of
+has been made in the `packages/javascript` directory source code, OR if the version of
plotly.js has been updated. If neither of these is the case, there's no need
to increment the `plotlywidget` version or to publish a new version to npm.
@@ -253,7 +253,9 @@ Note that the conda installation instructions must include
"-c plotly/lable/test" rather than "-c plotly" in order to install the
release candidate version.
-Commit Changelog and README updates.
+Update the `doc/python/getting-started.md` file with the same version numbers.
+
+Commit Changelog, README and getting-started updates.
### Bump to release candidate version
1) Manually update the plotlywidget version to `A.B.C-rc.1` in the files
@@ -297,17 +299,19 @@ And, you'll need the credentials file `~/.pypirc`. Request access from
(plotly_dev) $ twine upload dist/plotly-X.Y.Zrc1*
```
-### Publish release candidate of `plotlywidget` to NPM
+### Publish release candidate of `plotlywidget` and `jupyterlab-plotly` to NPM
Now, publish the release candidate of the `plotlywidget` NPM package.
```bash
-cd ./js
+cd ./packages/javascript/plotlywidget
npm publish --access public --tag next
```
The `--tag next` part ensures that users won't install this version unless
they explicitly ask for the version or for the version wtih the `next` tag.
+Do the same in the `jupyterlab-plotly` directory.
+
### Publish release candidate to plotly anaconda channel
To publish package to the plotly anaconda channel you'll need to have the
anaconda or miniconda distribution installed, and you'll need to have the
@@ -361,7 +365,9 @@ release candidate suffix from the following version strings:
- `plotly/_widget_version.py`:
+ Update `__frontend_version__` to `^A.B.C` (Note the `^` prefix)
- - `js/package.json`
+ - `packages/javascript/plotlywidget/package.json`
+ + Update `"version"` to `A.B.C`
+ - `packages/javascript/jupyterlab-plotly/package.json`
+ Update `"version"` to `A.B.C`
Commit and push to the release branch.
@@ -431,6 +437,19 @@ Make "Release title" the same string as the tag.
Copy changelog section for this version as the "Describe this release"
+### Upgrade doc requirements and API doc
+
+Files to be updated:
+- `doc/apidoc/conf.py` with new version number
+- `doc/requirements.txt`
+- `binder/requirements.txt`
+
+### Synchronize master and doc-prod branches
+
+doc-prod should already have been merged on a regular basis into master, but
+start doing it first. Then merge master into doc-prod to deploy the doc related
+to features in the release.
+
### Post announcement
Post a simple announcement to the Plotly Python forum, with links to the
README installation instructions and to the CHANGELOG.
diff --git a/doc/python/marker-style.md b/doc/python/marker-style.md
index e456d7f41f9..57f80b57fcc 100644
--- a/doc/python/marker-style.md
+++ b/doc/python/marker-style.md
@@ -5,8 +5,8 @@ jupyter:
text_representation:
extension: .md
format_name: markdown
- format_version: "1.1"
- jupytext_version: 1.1.7
+ format_version: '1.2'
+ jupytext_version: 1.3.2
kernelspec:
display_name: Python 3
language: python
@@ -20,7 +20,7 @@ jupyter:
name: python
nbconvert_exporter: python
pygments_lexer: ipython3
- version: 3.6.5
+ version: 3.7.0
plotly:
description: How to style markers in Python with Plotly.
display_as: file_settings
@@ -305,6 +305,115 @@ fig.show()
```
+### Custom Marker Symbols
+
+The `marker_symbol` attribute allows you to choose from a wide array of symbols to represent markers in your figures.
+
+The basic symbols are: `circle`, `square`, `diamond`, `cross`, `x`, `triangle`, `pentagon`, `hexagram`, `star`, `diamond`, `hourglass`, `bowtie`, `asterisk`, `hash`, `y`, and `line`.
+
+Each basic symbol is also represented by a number. Adding 100 to that number is equivalent to appending the suffix "-open" to a symbol name. Adding 200 is equivalent to appending "-dot" to a symbol name. Adding 300 is equivalent to appending "-open-dot" or "dot-open" to a symbol name.
+
+In the following figures, hover over a symbol to see its name or number. Set the `marker_symbol` attribute equal to that name or number to change the marker symbol in your figure.
+
+#### Basic Symbols
+
+```python
+import plotly.graph_objects as go
+fig = go.Figure()
+fig.update_layout(title="Basic Symbols")
+fig.update_xaxes(showticklabels=False)
+fig.update_yaxes(showticklabels=False)
+
+for index in range(27):
+ fig.add_trace(go.Scatter(x=[(index % 30)], y=[index // 30],
+ marker_symbol=index, marker_color='black',
+ marker_size=10, showlegend=False, hovertext=index))
+
+fig.show()
+```
+
+#### Custom Symbols
+
+```python
+import plotly.graph_objects as go
+symbols = [0, 'circle', 100, 'circle-open', 200, 'circle-dot', 300,
+ 'circle-open-dot', 1, 'square', 101, 'square-open', 201,
+ 'square-dot', 301, 'square-open-dot', 2, 'diamond', 102,
+ 'diamond-open', 202, 'diamond-dot', 302,
+ 'diamond-open-dot', 3, 'cross', 103, 'cross-open', 203,
+ 'cross-dot', 303, 'cross-open-dot', 4, 'x', 104, 'x-open',
+ 204, 'x-dot', 304, 'x-open-dot', 5, 'triangle-up', 105,
+ 'triangle-up-open', 205, 'triangle-up-dot', 305,
+ 'triangle-up-open-dot', 6, 'triangle-down', 106,
+ 'triangle-down-open', 206, 'triangle-down-dot', 306,
+ 'triangle-down-open-dot', 7, 'triangle-left', 107,
+ 'triangle-left-open', 207, 'triangle-left-dot', 307,
+ 'triangle-left-open-dot', 8, 'triangle-right', 108,
+ 'triangle-right-open', 208, 'triangle-right-dot', 308,
+ 'triangle-right-open-dot', 9, 'triangle-ne', 109,
+ 'triangle-ne-open', 209, 'triangle-ne-dot', 309,
+ 'triangle-ne-open-dot', 10, 'triangle-se', 110,
+ 'triangle-se-open', 210, 'triangle-se-dot', 310,
+ 'triangle-se-open-dot', 11, 'triangle-sw', 111,
+ 'triangle-sw-open', 211, 'triangle-sw-dot', 311,
+ 'triangle-sw-open-dot', 12, 'triangle-nw', 112,
+ 'triangle-nw-open', 212, 'triangle-nw-dot', 312,
+ 'triangle-nw-open-dot', 13, 'pentagon', 113,
+ 'pentagon-open', 213, 'pentagon-dot', 313,
+ 'pentagon-open-dot', 14, 'hexagon', 114, 'hexagon-open',
+ 214, 'hexagon-dot', 314, 'hexagon-open-dot', 15,
+ 'hexagon2', 115, 'hexagon2-open', 215, 'hexagon2-dot',
+ 315, 'hexagon2-open-dot', 16, 'octagon', 116,
+ 'octagon-open', 216, 'octagon-dot', 316,
+ 'octagon-open-dot', 17, 'star', 117, 'star-open', 217,
+ 'star-dot', 317, 'star-open-dot', 18, 'hexagram', 118,
+ 'hexagram-open', 218, 'hexagram-dot', 318,
+ 'hexagram-open-dot', 19, 'star-triangle-up', 119,
+ 'star-triangle-up-open', 219, 'star-triangle-up-dot', 319,
+ 'star-triangle-up-open-dot', 20, 'star-triangle-down',
+ 120, 'star-triangle-down-open', 220,
+ 'star-triangle-down-dot', 320,
+ 'star-triangle-down-open-dot', 21, 'star-square', 121,
+ 'star-square-open', 221, 'star-square-dot', 321,
+ 'star-square-open-dot', 22, 'star-diamond', 122,
+ 'star-diamond-open', 222, 'star-diamond-dot', 322,
+ 'star-diamond-open-dot', 23, 'diamond-tall', 123,
+ 'diamond-tall-open', 223, 'diamond-tall-dot', 323,
+ 'diamond-tall-open-dot', 24, 'diamond-wide', 124,
+ 'diamond-wide-open', 224, 'diamond-wide-dot', 324,
+ 'diamond-wide-open-dot', 25, 'hourglass', 125,
+ 'hourglass-open', 26, 'bowtie', 126, 'bowtie-open', 27,
+ 'circle-cross', 127, 'circle-cross-open', 28, 'circle-x',
+ 128, 'circle-x-open', 29, 'square-cross', 129,
+ 'square-cross-open', 30, 'square-x', 130, 'square-x-open',
+ 31, 'diamond-cross', 131, 'diamond-cross-open', 32,
+ 'diamond-x', 132, 'diamond-x-open', 33, 'cross-thin', 133,
+ 'cross-thin-open', 34, 'x-thin', 134, 'x-thin-open', 35,
+ 'asterisk', 135, 'asterisk-open', 36, 'hash', 136,
+ 'hash-open', 236, 'hash-dot', 336, 'hash-open-dot', 37,
+ 'y-up', 137, 'y-up-open', 38, 'y-down', 138,
+ 'y-down-open', 39, 'y-left', 139, 'y-left-open', 40,
+ 'y-right', 140, 'y-right-open', 41, 'line-ew', 141,
+ 'line-ew-open', 42, 'line-ns', 142, 'line-ns-open', 43,
+ 'line-ne', 143, 'line-ne-open', 44, 'line-nw', 144,
+ 'line-nw-open']
+
+fig = go.Figure()
+fig.update_layout(title="Custom Marker Symbols")
+fig.update_xaxes(showticklabels=False)
+fig.update_yaxes(showticklabels=False)
+
+for index, symbol in enumerate(symbols[::2]):
+ fig.add_trace(go.Scatter(x=[(index % 30)], y=[index // 30],
+ marker_symbol=symbol, marker_color='black',
+ marker_size=10, showlegend=False, hovertext=symbols[2*index + 1],
+ name=''))
+
+
+fig.show()
+```
+
+
### Reference
See https://plot.ly/python/reference/ for more information and chart attribute options!
diff --git a/packages/python/plotly/plotly/express/__init__.py b/packages/python/plotly/plotly/express/__init__.py
index 1d7c10d8cf2..fb334c1b973 100644
--- a/packages/python/plotly/plotly/express/__init__.py
+++ b/packages/python/plotly/plotly/express/__init__.py
@@ -4,7 +4,6 @@
"""
from __future__ import absolute_import
from plotly import optional_imports
-from ._imshow import imshow
pd = optional_imports.get_module("pandas")
if pd is None:
@@ -13,6 +12,7 @@
Plotly express requires pandas to be installed."""
)
+from ._imshow import imshow
from ._chart_types import ( # noqa: F401
scatter,
scatter_3d,
diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py
index e94a79d3954..1f9e2706af7 100644
--- a/packages/python/plotly/plotly/express/_core.py
+++ b/packages/python/plotly/plotly/express/_core.py
@@ -16,6 +16,17 @@
class PxDefaults(object):
+ __slots__ = [
+ "template",
+ "width",
+ "height",
+ "color_discrete_sequence",
+ "color_continuous_scale",
+ "symbol_sequence",
+ "line_dash_sequence",
+ "size_max",
+ ]
+
def __init__(self):
self.template = None
self.width = None
@@ -137,7 +148,7 @@ def make_mapping(args, variable):
)
-def make_trace_kwargs(args, trace_spec, g, mapping_labels, sizeref):
+def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref):
"""Populates a dict with arguments to update trace
Parameters
@@ -147,7 +158,7 @@ def make_trace_kwargs(args, trace_spec, g, mapping_labels, sizeref):
trace_spec : NamedTuple
which kind of trace to be used (has constructor, marginal etc.
attributes)
- g : pandas DataFrame
+ trace_data : pandas DataFrame
data
mapping_labels : dict
to be used for hovertemplate
@@ -156,87 +167,92 @@ def make_trace_kwargs(args, trace_spec, g, mapping_labels, sizeref):
Returns
-------
- result : dict
+ trace_patch : dict
dict to be used to update trace
fit_results : dict
fit information to be used for trendlines
"""
if "line_close" in args and args["line_close"]:
- g = g.append(g.iloc[0])
- result = trace_spec.trace_patch.copy() or {}
+ trace_data = trace_data.append(trace_data.iloc[0])
+ trace_patch = trace_spec.trace_patch.copy() or {}
fit_results = None
hover_header = ""
custom_data_len = 0
- for k in trace_spec.attrs:
- v = args[k]
- v_label = get_decorated_label(args, v, k)
- if k == "dimensions":
+ for attr_name in trace_spec.attrs:
+ attr_value = args[attr_name]
+ attr_label = get_decorated_label(args, attr_value, attr_name)
+ if attr_name == "dimensions":
dims = [
(name, column)
- for (name, column) in g.iteritems()
- if ((not v) or (name in v))
+ for (name, column) in trace_data.iteritems()
+ if ((not attr_value) or (name in attr_value))
and (
trace_spec.constructor != go.Parcoords
or args["data_frame"][name].dtype.kind in "bifc"
)
and (
trace_spec.constructor != go.Parcats
- or (v is not None and name in v)
+ or (attr_value is not None and name in attr_value)
or len(args["data_frame"][name].unique())
<= args["dimensions_max_cardinality"]
)
]
- result["dimensions"] = [
+ trace_patch["dimensions"] = [
dict(label=get_label(args, name), values=column.values)
for (name, column) in dims
]
if trace_spec.constructor == go.Splom:
- for d in result["dimensions"]:
+ for d in trace_patch["dimensions"]:
d["axis"] = dict(matches=True)
mapping_labels["%{xaxis.title.text}"] = "%{x}"
mapping_labels["%{yaxis.title.text}"] = "%{y}"
elif (
- v is not None
- or (trace_spec.constructor == go.Histogram and k in ["x", "y"])
+ attr_value is not None
+ or (trace_spec.constructor == go.Histogram and attr_name in ["x", "y"])
or (
trace_spec.constructor in [go.Histogram2d, go.Histogram2dContour]
- and k == "z"
+ and attr_name == "z"
)
):
- if k == "size":
- if "marker" not in result:
- result["marker"] = dict()
- result["marker"]["size"] = g[v]
- result["marker"]["sizemode"] = "area"
- result["marker"]["sizeref"] = sizeref
- mapping_labels[v_label] = "%{marker.size}"
- elif k == "marginal_x":
+ if attr_name == "size":
+ if "marker" not in trace_patch:
+ trace_patch["marker"] = dict()
+ trace_patch["marker"]["size"] = trace_data[attr_value]
+ trace_patch["marker"]["sizemode"] = "area"
+ trace_patch["marker"]["sizeref"] = sizeref
+ mapping_labels[attr_label] = "%{marker.size}"
+ elif attr_name == "marginal_x":
if trace_spec.constructor == go.Histogram:
mapping_labels["count"] = "%{y}"
- elif k == "marginal_y":
+ elif attr_name == "marginal_y":
if trace_spec.constructor == go.Histogram:
mapping_labels["count"] = "%{x}"
- elif k == "trendline":
- if v in ["ols", "lowess"] and args["x"] and args["y"] and len(g) > 1:
+ elif attr_name == "trendline":
+ if (
+ attr_value in ["ols", "lowess"]
+ and args["x"]
+ and args["y"]
+ and len(trace_data) > 1
+ ):
import statsmodels.api as sm
# sorting is bad but trace_specs with "trendline" have no other attrs
- g2 = g.sort_values(by=args["x"])
- y = g2[args["y"]]
- x = g2[args["x"]]
- result["x"] = x
+ sorted_trace_data = trace_data.sort_values(by=args["x"])
+ y = sorted_trace_data[args["y"]]
+ x = sorted_trace_data[args["x"]]
+ trace_patch["x"] = x
if x.dtype.type == np.datetime64:
x = x.astype(int) / 10 ** 9 # convert to unix epoch seconds
- if v == "lowess":
+ if attr_value == "lowess":
trendline = sm.nonparametric.lowess(y, x)
- result["y"] = trendline[:, 1]
+ trace_patch["y"] = trendline[:, 1]
hover_header = "LOWESS trendline
"
- elif v == "ols":
+ elif attr_value == "ols":
fit_results = sm.OLS(y.values, sm.add_constant(x.values)).fit()
- result["y"] = fit_results.predict()
+ trace_patch["y"] = fit_results.predict()
hover_header = "OLS trendline
"
hover_header += "%s = %g * %s + %g
" % (
args["y"],
@@ -250,120 +266,127 @@ def make_trace_kwargs(args, trace_spec, g, mapping_labels, sizeref):
mapping_labels[get_label(args, args["x"])] = "%{x}"
mapping_labels[get_label(args, args["y"])] = "%{y} (trend)"
- elif k.startswith("error"):
- error_xy = k[:7]
- arr = "arrayminus" if k.endswith("minus") else "array"
- if error_xy not in result:
- result[error_xy] = {}
- result[error_xy][arr] = g[v]
- elif k == "custom_data":
- result["customdata"] = g[v].values
- custom_data_len = len(v) # number of custom data columns
- elif k == "hover_name":
+ elif attr_name.startswith("error"):
+ error_xy = attr_name[:7]
+ arr = "arrayminus" if attr_name.endswith("minus") else "array"
+ if error_xy not in trace_patch:
+ trace_patch[error_xy] = {}
+ trace_patch[error_xy][arr] = trace_data[attr_value]
+ elif attr_name == "custom_data":
+ trace_patch["customdata"] = trace_data[attr_value].values
+ custom_data_len = len(attr_value) # number of custom data columns
+ elif attr_name == "hover_name":
if trace_spec.constructor not in [
go.Histogram,
go.Histogram2d,
go.Histogram2dContour,
]:
- result["hovertext"] = g[v]
+ trace_patch["hovertext"] = trace_data[attr_value]
if hover_header == "":
hover_header = "%{hovertext}
"
- elif k == "hover_data":
+ elif attr_name == "hover_data":
if trace_spec.constructor not in [
go.Histogram,
go.Histogram2d,
go.Histogram2dContour,
]:
- for col in v:
+ for col in attr_value:
try:
position = args["custom_data"].index(col)
except (ValueError, AttributeError, KeyError):
position = custom_data_len
custom_data_len += 1
- if "customdata" in result:
- result["customdata"] = np.hstack(
- (result["customdata"], g[col].values[:, None])
+ if "customdata" in trace_patch:
+ trace_patch["customdata"] = np.hstack(
+ (
+ trace_patch["customdata"],
+ trace_data[col].values[:, None],
+ )
)
else:
- result["customdata"] = g[col].values[:, None]
- v_label_col = get_decorated_label(args, col, None)
- mapping_labels[v_label_col] = "%%{customdata[%d]}" % (position)
- elif k == "color":
+ trace_patch["customdata"] = trace_data[col].values[
+ :, None
+ ]
+ attr_label_col = get_decorated_label(args, col, None)
+ mapping_labels[attr_label_col] = "%%{customdata[%d]}" % (
+ position
+ )
+ elif attr_name == "color":
if trace_spec.constructor in [go.Choropleth, go.Choroplethmapbox]:
- result["z"] = g[v]
- result["coloraxis"] = "coloraxis1"
- mapping_labels[v_label] = "%{z}"
+ trace_patch["z"] = trace_data[attr_value]
+ trace_patch["coloraxis"] = "coloraxis1"
+ mapping_labels[attr_label] = "%{z}"
elif trace_spec.constructor in [
go.Sunburst,
go.Treemap,
go.Pie,
go.Funnelarea,
]:
- if "marker" not in result:
- result["marker"] = dict()
+ if "marker" not in trace_patch:
+ trace_patch["marker"] = dict()
if args.get("color_is_continuous"):
- result["marker"]["colors"] = g[v]
- result["marker"]["coloraxis"] = "coloraxis1"
- mapping_labels[v_label] = "%{color}"
+ trace_patch["marker"]["colors"] = trace_data[attr_value]
+ trace_patch["marker"]["coloraxis"] = "coloraxis1"
+ mapping_labels[attr_label] = "%{color}"
else:
- result["marker"]["colors"] = []
+ trace_patch["marker"]["colors"] = []
mapping = {}
- for cat in g[v]:
+ for cat in trace_data[attr_value]:
if mapping.get(cat) is None:
mapping[cat] = args["color_discrete_sequence"][
len(mapping) % len(args["color_discrete_sequence"])
]
- result["marker"]["colors"].append(mapping[cat])
+ trace_patch["marker"]["colors"].append(mapping[cat])
else:
colorable = "marker"
if trace_spec.constructor in [go.Parcats, go.Parcoords]:
colorable = "line"
- if colorable not in result:
- result[colorable] = dict()
- result[colorable]["color"] = g[v]
- result[colorable]["coloraxis"] = "coloraxis1"
- mapping_labels[v_label] = "%%{%s.color}" % colorable
- elif k == "animation_group":
- result["ids"] = g[v]
- elif k == "locations":
- result[k] = g[v]
- mapping_labels[v_label] = "%{location}"
- elif k == "values":
- result[k] = g[v]
- _label = "value" if v_label == "values" else v_label
+ if colorable not in trace_patch:
+ trace_patch[colorable] = dict()
+ trace_patch[colorable]["color"] = trace_data[attr_value]
+ trace_patch[colorable]["coloraxis"] = "coloraxis1"
+ mapping_labels[attr_label] = "%%{%s.color}" % colorable
+ elif attr_name == "animation_group":
+ trace_patch["ids"] = trace_data[attr_value]
+ elif attr_name == "locations":
+ trace_patch[attr_name] = trace_data[attr_value]
+ mapping_labels[attr_label] = "%{location}"
+ elif attr_name == "values":
+ trace_patch[attr_name] = trace_data[attr_value]
+ _label = "value" if attr_label == "values" else attr_label
mapping_labels[_label] = "%{value}"
- elif k == "parents":
- result[k] = g[v]
- _label = "parent" if v_label == "parents" else v_label
+ elif attr_name == "parents":
+ trace_patch[attr_name] = trace_data[attr_value]
+ _label = "parent" if attr_label == "parents" else attr_label
mapping_labels[_label] = "%{parent}"
- elif k == "ids":
- result[k] = g[v]
- _label = "id" if v_label == "ids" else v_label
+ elif attr_name == "ids":
+ trace_patch[attr_name] = trace_data[attr_value]
+ _label = "id" if attr_label == "ids" else attr_label
mapping_labels[_label] = "%{id}"
- elif k == "names":
+ elif attr_name == "names":
if trace_spec.constructor in [
go.Sunburst,
go.Treemap,
go.Pie,
go.Funnelarea,
]:
- result["labels"] = g[v]
- _label = "label" if v_label == "names" else v_label
+ trace_patch["labels"] = trace_data[attr_value]
+ _label = "label" if attr_label == "names" else attr_label
mapping_labels[_label] = "%{label}"
else:
- result[k] = g[v]
+ trace_patch[attr_name] = trace_data[attr_value]
else:
- if v:
- result[k] = g[v]
- mapping_labels[v_label] = "%%{%s}" % k
+ if attr_value:
+ trace_patch[attr_name] = trace_data[attr_value]
+ mapping_labels[attr_label] = "%%{%s}" % attr_name
if trace_spec.constructor not in [
go.Parcoords,
go.Parcats,
]:
hover_lines = [k + "=" + v for k, v in mapping_labels.items()]
- result["hovertemplate"] = hover_header + "
".join(hover_lines)
- return result, fit_results
+ trace_patch["hovertemplate"] = hover_header + "
".join(hover_lines)
+ return trace_patch, fit_results
def configure_axes(args, constructor, fig, orders):
@@ -1012,11 +1035,12 @@ def build_dataframe(args, attrables, array_attrables):
def _check_dataframe_all_leaves(df):
df_sorted = df.sort_values(by=list(df.columns))
null_mask = df_sorted.isnull()
+ df_sorted = df_sorted.astype(str)
null_indices = np.nonzero(null_mask.any(axis=1).values)[0]
for null_row_index in null_indices:
row = null_mask.iloc[null_row_index]
- indices = np.nonzero(row.values)[0]
- if not row[indices[0] :].all():
+ i = np.nonzero(row.values)[0][0]
+ if not row[i:].all():
raise ValueError(
"None entries cannot have not-None children",
df_sorted.iloc[null_row_index],
@@ -1043,8 +1067,9 @@ def process_dataframe_hierarchy(args):
if args["color"] and args["color"] in path:
series_to_copy = df[args["color"]]
- args["color"] = str(args["color"]) + "additional_col_for_px"
- df[args["color"]] = series_to_copy
+ new_col_name = args["color"] + "additional_col_for_color"
+ path = [new_col_name if x == args["color"] else x for x in path]
+ df[new_col_name] = series_to_copy
if args["hover_data"]:
for col_name in args["hover_data"]:
if col_name == args["color"]:
@@ -1058,6 +1083,7 @@ def process_dataframe_hierarchy(args):
path = [new_col_name if x == col_name else x for x in path]
df[new_col_name] = series_to_copy
# ------------ Define aggregation functions --------------------------------
+
def aggfunc_discrete(x):
uniques = x.unique()
if len(uniques) == 1:
@@ -1147,6 +1173,11 @@ def aggfunc_continuous(x):
args["ids"] = "id"
args["names"] = "labels"
args["parents"] = "parent"
+ if args["color"]:
+ if not args["hover_data"]:
+ args["hover_data"] = [args["color"]]
+ else:
+ args["hover_data"].append(args["color"])
return args
diff --git a/packages/python/plotly/plotly/tests/test_core/test_px/test_px.py b/packages/python/plotly/plotly/tests/test_core/test_px/test_px.py
index 9ace6a7b4a9..b4880899e95 100644
--- a/packages/python/plotly/plotly/tests/test_core/test_px/test_px.py
+++ b/packages/python/plotly/plotly/tests/test_core/test_px/test_px.py
@@ -1,5 +1,6 @@
import plotly.express as px
import numpy as np
+import pytest
def test_scatter():
@@ -232,3 +233,9 @@ def assert_orderings(days_order, days_check, times_order, times_check):
for days in permutations(df["day"].unique()):
for times in permutations(df["time"].unique()):
assert_orderings(days, days, times, times)
+
+
+def test_permissive_defaults():
+ msg = "'PxDefaults' object has no attribute 'should_not_work'"
+ with pytest.raises(AttributeError, match=msg):
+ px.defaults.should_not_work = "test"
diff --git a/packages/python/plotly/plotly/tests/test_core/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_functions.py
index 0393931af11..0fc38c94d4d 100644
--- a/packages/python/plotly/plotly/tests/test_core/test_px/test_px_functions.py
+++ b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_functions.py
@@ -209,14 +209,22 @@ def test_sunburst_treemap_with_path_color():
# Hover info
df["hover"] = [el.lower() for el in vendors]
fig = px.sunburst(df, path=path, color="calls", hover_data=["hover"])
- custom = fig.data[0].customdata.ravel()
- assert np.all(custom[:8] == df["hover"])
- assert np.all(custom[8:] == "(?)")
+ custom = fig.data[0].customdata
+ assert np.all(custom[:8, 0] == df["hover"])
+ assert np.all(custom[8:, 0] == "(?)")
+ assert np.all(custom[:8, 1] == df["calls"])
# Discrete color
fig = px.sunburst(df, path=path, color="vendors")
assert len(np.unique(fig.data[0].marker.colors)) == 9
+ # Numerical column in path
+ df["regions"] = df["regions"].map({"North": 1, "South": 2})
+ path = ["total", "regions", "sectors", "vendors"]
+ fig = px.sunburst(df, path=path, values="values", color="calls")
+ colors = fig.data[0].marker.colors
+ assert np.all(np.array(colors[:8]) == np.array(calls))
+
def test_sunburst_treemap_with_path_non_rectangular():
vendors = ["A", "B", "C", "D", None, "E", "F", "G", "H", None]