diff --git a/CHANGELOG.md b/CHANGELOG.md index c92c3ea6d64..c676479a490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +### Added +- Add `px.scatter_smith` for plotting complex values (`real` and `imag`) on Smith charts, and support the `"smith"` subplot type in `make_subplots` [[#5824](https://github.com/plotly/plotly.py/pull/5824)], with thanks to @ybai08 for the contribution! + ### Fixed - Fix `mpl_to_plotly` tick marker mirroring, axis positioning, and trace visibility for twinned and overlaid subplots (such as `twinx` and `twiny`) [[#5310](https://github.com/plotly/plotly.py/pull/5310)], with thanks to @robertoffmoura for the contribution! - Fix `mpl_to_plotly` losing matplotlib axis line colors by exporting the color of each axis's displayed spine to the plotly x and y axis `linecolor` [[#5311](https://github.com/plotly/plotly.py/pull/5311)], with thanks to @robertoffmoura for the contribution! diff --git a/doc/apidoc/plotly.express.rst b/doc/apidoc/plotly.express.rst index 9a82acf4501..540b3ca3928 100644 --- a/doc/apidoc/plotly.express.rst +++ b/doc/apidoc/plotly.express.rst @@ -18,6 +18,7 @@ plotly's high-level API for rapid figure generation. :: scatter_3d scatter_polar scatter_ternary + scatter_smith scatter_map scatter_geo line diff --git a/doc/python/smith-charts.md b/doc/python/smith-charts.md index 0c6cc461489..61c305b133a 100644 --- a/doc/python/smith-charts.md +++ b/doc/python/smith-charts.md @@ -38,6 +38,24 @@ jupyter: A [Smith Chart](https://en.wikipedia.org/wiki/Smith_chart) is a specialized chart for visualizing [complex numbers](https://en.wikipedia.org/wiki/Complex_number): numbers with both a real and imaginary part. +### Smith Charts with Plotly Express + +[Plotly Express](/python/plotly-express/) is the easy-to-use, high-level interface to Plotly, which [operates on a variety of types of data](/python/px-arguments/) and produces [easy-to-style figures](/python/styling-plotly-express/). With `px.scatter_smith`, each row of the DataFrame is represented as a marker on the Smith chart, with `real` and `imag` giving the real and imaginary parts of its normalized impedance. + +```python +import plotly.express as px +import pandas as pd + +df = pd.DataFrame({ + "real": [0.2, 0.5, 1.0, 2.0, 0.2, 0.5, 1.0, 2.0], + "imag": [0.5, 1.0, 0.5, -1.0, -0.5, -1.0, 2.0, 0.0], + "antenna": ["A", "A", "A", "A", "B", "B", "B", "B"], +}) + +fig = px.scatter_smith(df, real="real", imag="imag", color="antenna") +fig.show() +``` + ### Smith Charts with Plotly Graph Objects ```python diff --git a/plotly/_subplots.py b/plotly/_subplots.py index 7983a994a05..8b8fd5c5467 100644 --- a/plotly/_subplots.py +++ b/plotly/_subplots.py @@ -8,7 +8,7 @@ # little differently. import collections -_single_subplot_types = {"scene", "geo", "polar", "ternary", "map"} +_single_subplot_types = {"scene", "geo", "polar", "ternary", "map", "smith"} _subplot_types = set.union(_single_subplot_types, {"xy", "domain"}) # For most subplot types, a trace is associated with a particular subplot @@ -20,7 +20,7 @@ # the trace property is just named `subplot`. For example setting # the `scatterpolar.subplot` property to `polar3` associates the scatterpolar # trace with the third polar subplot in the figure -_subplot_prop_named_subplot = {"polar", "ternary", "map"} +_subplot_prop_named_subplot = {"polar", "ternary", "map", "smith"} # Named tuple to hold an xaxis/yaxis pair that represent a single subplot @@ -152,6 +152,7 @@ def make_subplots( - 'polar': Polar subplot for scatterpolar, barpolar, etc. - 'ternary': Ternary subplot for scatterternary - 'map': Map subplot for scattermap, choroplethmap and densitymap + - 'smith': Smith chart subplot for scattersmith - 'domain': Subplot type for traces that are individually positioned. pie, parcoords, parcats, etc. - trace type: A trace type which will be used to determine diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index a7326da8e5a..f099980c7cc 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -2428,6 +2428,7 @@ def get_subplot(self, row, col, secondary_y=False): * plotly.graph_objs.layout.Polar: if subplot type is 'polar' * plotly.graph_objs.layout.Ternary: if subplot type is 'ternary' * plotly.graph_objs.layout.Map: if subplot type is 'map' + * plotly.graph_objs.layout.Smith: if subplot type is 'smith' * SubplotDomain namedtuple with `x` and `y` fields: if subplot type is 'domain'. - x: length 2 list of the subplot start and stop width diff --git a/plotly/express/__init__.py b/plotly/express/__init__.py index 9fc66c91ae4..e5e249f5be7 100644 --- a/plotly/express/__init__.py +++ b/plotly/express/__init__.py @@ -30,6 +30,7 @@ scatter_3d, scatter_polar, scatter_ternary, + scatter_smith, scatter_map, scatter_geo, line, @@ -79,6 +80,7 @@ "scatter_3d", "scatter_polar", "scatter_ternary", + "scatter_smith", "scatter_map", "scatter_geo", "scatter_matrix", diff --git a/plotly/express/_chart_types.py b/plotly/express/_chart_types.py index f78a257cabb..2e48f8a1b05 100644 --- a/plotly/express/_chart_types.py +++ b/plotly/express/_chart_types.py @@ -937,6 +937,47 @@ def line_ternary( line_ternary.__doc__ = make_docstring(line_ternary) +def scatter_smith( + data_frame=None, + real=None, + imag=None, + color=None, + symbol=None, + size=None, + text=None, + hover_name=None, + hover_data=None, + custom_data=None, + animation_frame=None, + animation_group=None, + category_orders=None, + labels=None, + color_discrete_sequence=None, + color_discrete_map=None, + color_continuous_scale=None, + range_color=None, + color_continuous_midpoint=None, + symbol_sequence=None, + symbol_map=None, + opacity=None, + size_max=None, + title=None, + subtitle=None, + template=None, + width=None, + height=None, +) -> go.Figure: + """ + In a Smith chart scatter plot, each row of `data_frame` is represented by + a symbol mark in Smith chart coordinates, positioned by its normalized + impedance (`real` + `imag` * j). + """ + return make_figure(args=locals(), constructor=go.Scattersmith) + + +scatter_smith.__doc__ = make_docstring(scatter_smith) + + def scatter_polar( data_frame=None, r=None, diff --git a/plotly/express/_core.py b/plotly/express/_core.py index 9994720b094..3723613d55a 100644 --- a/plotly/express/_core.py +++ b/plotly/express/_core.py @@ -30,10 +30,10 @@ # Declare all supported attributes, across all plot types direct_attrables = ( - ["base", "x", "y", "z", "a", "b", "c", "r", "theta", "size", "x_start", "x_end"] - + ["hover_name", "text", "names", "values", "parents", "wide_cross"] - + ["ids", "error_x", "error_x_minus", "error_y", "error_y_minus", "error_z"] - + ["error_z_minus", "lat", "lon", "locations", "animation_group"] + ["base", "x", "y", "z", "a", "b", "c", "r", "theta", "real", "imag", "size"] + + ["x_start", "x_end", "hover_name", "text", "names", "values", "parents"] + + ["wide_cross", "ids", "error_x", "error_x_minus", "error_y", "error_y_minus"] + + ["error_z", "error_z_minus", "lat", "lon", "locations", "animation_group"] ) array_attrables = ["dimensions", "custom_data", "hover_data", "path", "wide_variable"] group_attrables = ["animation_frame", "facet_row", "facet_col", "line_group"] @@ -2893,7 +2893,7 @@ def init_figure(args, subplot_type, frame_list, nrows, ncols, col_labels, row_la horizontal_spacing = args.get("facet_col_spacing") or 0.02 else: # Other subplot types: - # 'scene', 'geo', 'polar', 'ternary', 'map', 'domain', None + # 'scene', 'geo', 'polar', 'ternary', 'map', 'smith', 'domain', None # # We can customize subplot spacing per type once we enable faceting # for all plot types diff --git a/plotly/express/_doc.py b/plotly/express/_doc.py index 42cb991df18..fbca4df80f5 100644 --- a/plotly/express/_doc.py +++ b/plotly/express/_doc.py @@ -73,6 +73,16 @@ colref_desc, "Values from this column or array_like are used to position marks along the angular axis in polar coordinates.", ], + real=[ + colref_type, + colref_desc, + "Values from this column or array_like are used as the real part of the normalized impedance of marks in Smith chart coordinates.", + ], + imag=[ + colref_type, + colref_desc, + "Values from this column or array_like are used as the imaginary part of the normalized impedance of marks in Smith chart coordinates.", + ], values=[ colref_type, colref_desc, diff --git a/plotly/subplots.py b/plotly/subplots.py index 55fac2437ad..4217bee0e50 100644 --- a/plotly/subplots.py +++ b/plotly/subplots.py @@ -115,6 +115,7 @@ def make_subplots( - 'polar': Polar subplot for scatterpolar, barpolar, etc. - 'ternary': Ternary subplot for scatterternary - 'map': Map subplot for scattermap + - 'smith': Smith chart subplot for scattersmith - 'domain': Subplot type for traces that are individually positioned. pie, parcoords, parcats, etc. - trace type: A trace type which will be used to determine diff --git a/tests/test_core/test_subplots/test_get_subplot.py b/tests/test_core/test_subplots/test_get_subplot.py index 7c6d32337e5..99bbdc2be7f 100644 --- a/tests/test_core/test_subplots/test_get_subplot.py +++ b/tests/test_core/test_subplots/test_get_subplot.py @@ -159,6 +159,26 @@ def test_get_subplot(self): domain = fig.data[-1].domain self.assertEqual(subplot, SubplotDomain(x=domain.x, y=domain.y)) + def test_get_subplot_smith(self): + # "smith" and the "scattersmith" trace type both create smith subplots + fig = subplots.make_subplots( + rows=1, cols=2, specs=[[{"type": "smith"}, {"type": "scattersmith"}]] + ) + fig.add_trace(go.Scattersmith(real=[0.5, 1], imag=[1, 2]), row=1, col=1) + fig.add_trace(go.Scattersmith(real=[2, 3], imag=[-1, 0]), row=1, col=2) + + self.assertEqual(fig.layout.smith.domain.x, (0.0, 0.45)) + self.assertEqual(fig.layout.smith2.domain.x, (0.55, 1.0)) + self.assertEqual(fig.data[0].subplot, "smith") + self.assertEqual(fig.data[1].subplot, "smith2") + + self.assertEqual(fig.get_subplot(1, 1), fig.layout.smith) + self.assertEqual(fig.get_subplot(1, 2), fig.layout.smith2) + + fig.update_traces(marker_color="red", row=1, col=2) + self.assertIsNone(fig.data[0].marker.color) + self.assertEqual(fig.data[1].marker.color, "red") + def test_get_subplot_out_of_bounds(self): fig = subplots.make_subplots(rows=4, cols=2) diff --git a/tests/test_optional/test_px/test_px_functions.py b/tests/test_optional/test_px/test_px_functions.py index 84a451733b4..e1d75309337 100644 --- a/tests/test_optional/test_px/test_px_functions.py +++ b/tests/test_optional/test_px/test_px_functions.py @@ -688,3 +688,37 @@ def test_empty_histogram(): fig = px.histogram() assert len(fig.data) == 1 assert fig.data[0].type == "histogram" + + +def test_scatter_smith_px(): + real = np.array([0.5, 1.0, 2.0]) + imag = np.array([1.0, -0.5, 0.0]) + go_trace = go.Scattersmith(real=real, imag=imag, mode="markers") + fig = px.scatter_smith(real=real, imag=imag) + _compare_figures(go_trace, fig) + assert fig.data[0].subplot == "smith" + assert fig.layout.smith.domain.x == (0.0, 1.0) + + +def test_scatter_smith(constructor): + data = { + "r": [0.5, 1.0, 2.0, 0.2], + "x": [1.0, -0.5, 0.0, 3.0], + "band": ["low", "low", "high", "high"], + "f": [1, 2, 3, 4], + } + df = constructor(data) + fig = px.scatter_smith( + df, real="r", imag="x", color="band", text="f", hover_data=["f"] + ) + + assert [t.type for t in fig.data] == ["scattersmith", "scattersmith"] + assert [t.name for t in fig.data] == ["low", "high"] + assert_array_equal(fig.data[0].real, [0.5, 1.0]) + assert_array_equal(fig.data[0].imag, [1.0, -0.5]) + assert_array_equal(fig.data[1].real, [2.0, 0.2]) + assert_array_equal(fig.data[1].imag, [0.0, 3.0]) + assert fig.data[0].mode == "markers+text" + assert fig.data[0].hovertemplate == ( + "band=low
r=%{real}
x=%{imag}
f=%{customdata[0]}" + )