diff --git a/.circleci/create_conda_optional_env.sh b/.circleci/create_conda_optional_env.sh index d1d48f7e998..78c79940828 100755 --- a/.circleci/create_conda_optional_env.sh +++ b/.circleci/create_conda_optional_env.sh @@ -19,5 +19,5 @@ if [ ! -d $HOME/miniconda/envs/circle_optional ]; then requests nbformat six retrying psutil pandas decorator pytest mock nose poppler xarray scikit-image ipython jupyter ipykernel ipywidgets # Install orca into environment - $HOME/miniconda/bin/conda install --yes -n circle_optional -c plotly plotly-orca + $HOME/miniconda/bin/conda install --yes -n circle_optional -c plotly plotly-orca=1.2.1 fi diff --git a/doc/python/datashader.md b/doc/python/datashader.md index 9a9a3ba9f52..aa2c963c966 100644 --- a/doc/python/datashader.md +++ b/doc/python/datashader.md @@ -5,8 +5,8 @@ jupyter: text_representation: extension: .md format_name: markdown - format_version: "1.2" - jupytext_version: 1.3.1 + format_version: '1.2' + jupytext_version: 1.3.0 kernelspec: display_name: Python 3 language: python @@ -20,10 +20,9 @@ jupyter: name: python nbconvert_exporter: python pygments_lexer: ipython3 - version: 3.6.8 + version: 3.7.3 plotly: - description: - How to use datashader to rasterize large datasets, and visualize + description: How to use datashader to rasterize large datasets, and visualize the generated raster data with plotly. display_as: scientific language: python @@ -98,7 +97,7 @@ fig.show() ``` ```python -import plotly.graph_objects as go +import plotly.express as px import pandas as pd import numpy as np import datashader as ds @@ -106,22 +105,11 @@ df = pd.read_parquet('https://raw.githubusercontent.com/plotly/datasets/master/2 cvs = ds.Canvas(plot_width=100, plot_height=100) agg = cvs.points(df, 'SCHEDULED_DEPARTURE', 'DEPARTURE_DELAY') -x = np.array(agg.coords['SCHEDULED_DEPARTURE']) -y = np.array(agg.coords['DEPARTURE_DELAY']) - -# Assign nan to zero values so that the corresponding pixels are transparent -agg = np.array(agg.values, dtype=np.float) -agg[agg<1] = np.nan - -fig = go.Figure(go.Heatmap( - z=np.log10(agg), x=x, y=y, - hoverongaps=False, - hovertemplate='Scheduled departure: %{x:.1f}h
Depature delay: %{y}
Log10(Count): %{z}', - colorbar=dict(title='Count (Log)', tickprefix='1.e'))) -fig.update_xaxes(title_text='Scheduled departure') -fig.update_yaxes(title_text='Departure delay') +agg.values = np.log10(agg.values) +fig = px.imshow(agg, origin='lower', labels={'color':'Log10(count)'}) +fig.update_traces(hoverongaps=False) +fig.update_layout(coloraxis_colorbar=dict(title='Count', tickprefix='1.e')) fig.show() - ``` ```python diff --git a/doc/python/heatmaps.md b/doc/python/heatmaps.md index ec2735bf725..bc54043ec1a 100644 --- a/doc/python/heatmaps.md +++ b/doc/python/heatmaps.md @@ -36,9 +36,7 @@ jupyter: ### Heatmap with `plotly.express` and `px.imshow` -[Plotly Express](/python/plotly-express/) is the easy-to-use, high-level interface to Plotly. With `px.imshow`, each value of the input array is represented as a heatmap pixel. - -`px.imshow` makes opiniated choices for representing heatmaps, such as using square pixels. To override this behaviour, you can use `fig.update_layout` or use the `go.Heatmap` trace from `plotly.graph_objects` as described below. +[Plotly Express](/python/plotly-express/) is the easy-to-use, high-level interface to Plotly, which [operates on "tidy" data](/python/px-arguments/) and produces [easy-to-style figures](/python/styling-plotly-express/). With `px.imshow`, each value of the input array is represented as a heatmap pixel. For more examples using `px.imshow`, see the [tutorial on displaying image data with plotly](/python/imshow). @@ -51,6 +49,22 @@ fig = px.imshow([[1, 20, 30], fig.show() ``` +### Customizing the axes and labels on a heatmap + +You can use the `x`, `y` and `labels` arguments to customize the display of a heatmap, and use `.update_xaxes()` to move the x axis tick labels to the top: + +```python +import plotly.express as px +data=[[1, 25, 30, 50, 1], [20, 1, 60, 80, 30], [30, 60, 1, 5, 20]] +fig = px.imshow(data, + labels=dict(x="Day of Week", y="Time of Day", color="Productivity"), + x=['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], + y=['Morning', 'Afternoon', 'Evening'] + ) +fig.update_xaxes(side="top") +fig.show() +``` + ### Basic Heatmap with `plotly.graph_objects` If Plotly Express does not provide a good starting point, it is also possible to use the more generic `go.Heatmap` function from `plotly.graph_objects`. @@ -67,7 +81,7 @@ fig.show() ### Heatmap with Categorical Axis Labels -In this example we also show how to ignore [hovertext](https://plot.ly/python/hover-text-and-formatting/) when we have [missing values](https://plot.ly/python/missing_values) in the data by setting the [hoverongaps](https://plot.ly/python/reference/#heatmap-hoverongaps) to False. +In this example we also show how to ignore [hovertext](https://plot.ly/python/hover-text-and-formatting/) when we have [missing values](https://plot.ly/python/missing_values) in the data by setting the [hoverongaps](https://plot.ly/python/reference/#heatmap-hoverongaps) to False. ```python import plotly.graph_objects as go diff --git a/doc/python/imshow.md b/doc/python/imshow.md index ba5b0c20cb7..3fbcb98a7e5 100644 --- a/doc/python/imshow.md +++ b/doc/python/imshow.md @@ -6,7 +6,7 @@ jupyter: extension: .md format_name: markdown format_version: '1.2' - jupytext_version: 1.3.0 + jupytext_version: 1.3.1 kernelspec: display_name: Python 3 language: python @@ -20,7 +20,7 @@ jupyter: name: python nbconvert_exporter: python pygments_lexer: ipython3 - version: 3.7.3 + version: 3.6.8 plotly: description: How to display image data in Python with Plotly. display_as: scientific @@ -74,7 +74,7 @@ fig = px.imshow(img) fig.show() ``` -### Display single-channel 2D image as grayscale +### Display single-channel 2D data as a heatmap For a 2D image, `px.imshow` uses a colorscale to map scalar data to colors. The default colorscale is the one of the active template (see [the tutorial on templates](/python/templates/)). @@ -88,6 +88,17 @@ fig.show() ### Choose the colorscale to display a single-channel image +You can customize the [continuous color scale](/python/colorscales/) just like with any other Plotly Express function: + +```python +import plotly.express as px +import numpy as np +img = np.arange(100).reshape((10, 10)) +fig = px.imshow(img, color_continuous_scale='Viridis') +fig.show() +``` + +You can use this to make the image grayscale as well: ```python import plotly.express as px @@ -97,9 +108,9 @@ fig = px.imshow(img, color_continuous_scale='gray') fig.show() ``` -### Hiding the colorbar when displaying a single-channel image +### Hiding the colorbar and axis labels -See [the tutorial on coloraxis](/python/colorscales/#share-color-axis) for more details on coloraxis. +See the [continuous color](/python/colorscales/) and [cartesian axes](/python/axes/) pages for more details. ```python import plotly.express as px @@ -107,6 +118,50 @@ from skimage import data img = data.camera() fig = px.imshow(img, color_continuous_scale='gray') fig.update_layout(coloraxis_showscale=False) +fig.update_xaxes(showticklabels=False) +fig.update_yaxes(showticklabels=False) +fig.show() +``` + +### Customizing the axes and labels on a single-channel image + +You can use the `x`, `y` and `labels` arguments to customize the display of a heatmap, and use `.update_xaxes()` to move the x axis tick labels to the top: + +```python +import plotly.express as px +data=[[1, 25, 30, 50, 1], [20, 1, 60, 80, 30], [30, 60, 1, 5, 20]] +fig = px.imshow(data, + labels=dict(x="Day of Week", y="Time of Day", color="Productivity"), + x=['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], + y=['Morning', 'Afternoon', 'Evening'] + ) +fig.update_xaxes(side="top") +fig.show() +``` + +### Display an xarray image with px.imshow + +[xarrays](http://xarray.pydata.org/en/stable/) are labeled arrays (with labeled axes and coordinates). If you pass an xarray image to `px.imshow`, its axes labels and coordinates will be used for axis titles. If you don't want this behavior, you can pass `img.values` which is a NumPy array if `img` is an xarray. Alternatively, you can override axis titles hover labels and colorbar title using the `labels` attribute, as above. + +```python +import plotly.express as px +import xarray as xr +# Load xarray from dataset included in the xarray tutorial +airtemps = xr.tutorial.open_dataset('air_temperature').air.sel(lon=250.0) +fig = px.imshow(airtemps.T, color_continuous_scale='RdBu_r', origin='lower') +fig.show() +``` + +### Display an xarray image with square pixels + +For xarrays, by default `px.imshow` does not constrain pixels to be square, since axes often correspond to different physical quantities (e.g. time and space), contrary to a plain camera image where pixels are square (most of the time). If you want to impose square pixels, set the parameter `aspect` to "equal" as below. + +```python +import plotly.express as px +import xarray as xr +airtemps = xr.tutorial.open_dataset('air_temperature').air.isel(time=500) +colorbar_title = airtemps.attrs['var_desc'] + '
(%s)'%airtemps.attrs['units'] +fig = px.imshow(airtemps, color_continuous_scale='RdBu_r', aspect='equal') fig.show() ``` @@ -201,7 +256,7 @@ fig.show() ### imshow and datashader Arrays of rasterized values build by datashader can be visualized using -imshow. See the [plotly and datashader tutorial](/python/datashader/) for +imshow. See the [plotly and datashader tutorial](/python/datashader/) for examples on how to use plotly and datashader. diff --git a/packages/python/plotly/plotly/express/_doc.py b/packages/python/plotly/plotly/express/_doc.py index 3b153b0587a..41ea9e2f23d 100644 --- a/packages/python/plotly/plotly/express/_doc.py +++ b/packages/python/plotly/plotly/express/_doc.py @@ -6,13 +6,6 @@ except AttributeError: # python 2 getfullargspec = inspect.getargspec -# TODO contents of columns -# TODO explain categorical -# TODO handle color -# TODO handle details of box/violin/histogram -# TODO handle details of column selection with `dimensions` -# TODO document "or `None`, default `None`" in various places -# TODO standardize positioning and casing of 'default' colref_type = "str or int or Series or array-like" colref_desc = "Either a name of a column in `data_frame`, or a pandas Series or array_like object." @@ -325,11 +318,11 @@ ], title=["str", "The figure title."], template=[ - "or dict or plotly.graph_objects.layout.Template instance", - "The figure template name or definition.", + "str or dict or plotly.graph_objects.layout.Template instance", + "The figure template name (must be a key in plotly.io.templates) or definition.", ], width=["int (default `None`)", "The figure width in pixels."], - height=["int (default `600`)", "The figure height in pixels."], + height=["int (default `None`)", "The figure height in pixels."], labels=[ "dict with str keys and str values (default `{}`)", "By default, column names are used in the figure for axis titles, legend entries and hovers.", diff --git a/packages/python/plotly/plotly/express/_imshow.py b/packages/python/plotly/plotly/express/_imshow.py index 6f1bf77e877..9ffe6800676 100644 --- a/packages/python/plotly/plotly/express/_imshow.py +++ b/packages/python/plotly/plotly/express/_imshow.py @@ -3,6 +3,13 @@ from ._core import apply_default_cascade import numpy as np +try: + import xarray + + xarray_imported = True +except ImportError: + xarray_imported = False + _float_types = [] # Adapted from skimage.util.dtype @@ -61,6 +68,9 @@ def imshow( zmin=None, zmax=None, origin=None, + labels={}, + x=None, + y=None, color_continuous_scale=None, color_continuous_midpoint=None, range_color=None, @@ -68,6 +78,7 @@ def imshow( template=None, width=None, height=None, + aspect=None, ): """ Display an image, i.e. data on a 2D regular raster. @@ -75,7 +86,7 @@ def imshow( Parameters ---------- - img: array-like image + img: array-like image, or xarray The image data. Supported array shapes are - (M, N): an image with scalar data. The data is visualized @@ -90,11 +101,24 @@ def imshow( a multichannel image of floats, the max of the image is computed and zmax is the smallest power of 256 (1, 255, 65535) greater than this max value, with a 5% tolerance. For a single-channel image, the max of the image is used. + Overridden by range_color. origin : str, 'upper' or 'lower' (default 'upper') position of the [0, 0] pixel of the image array, in the upper left or lower left corner. The convention 'upper' is typically used for matrices and images. + labels : dict with str keys and str values (default `{}`) + Sets names used in the figure for axis titles (keys ``x`` and ``y``), + colorbar title and hoverlabel (key ``color``). The values should correspond + to the desired label to be displayed. If ``img`` is an xarray, dimension + names are used for axis titles, and long name for the colorbar title + (unless overridden in ``labels``). Possible keys are: x, y, and color. + + x, y: list-like, optional + x and y are used to label the axes of single-channel heatmap visualizations and + their lengths must match the lengths of the second and first dimensions of the + img argument. They are auto-populated if the input is an xarray. + color_continuous_scale : str or list of str colormap used to map scalar data to colors (for a 2D image). This parameter is not used for RGB or RGBA images. If a string is provided, it should be the name @@ -103,7 +127,7 @@ def imshow( color_continuous_midpoint : number If set, computes the bounds of the continuous color scale to have the desired - midpoint. + midpoint. Overridden by range_color or zmin and zmax. range_color : list of two numbers If provided, overrides auto-scaling on the continuous color scale, including @@ -120,7 +144,15 @@ def imshow( The figure width in pixels. height: number - The figure height in pixels, defaults to 600. + The figure height in pixels. + + aspect: 'equal', 'auto', or None + - 'equal': Ensures an aspect ratio of 1 or pixels (square pixels) + - 'auto': The axes is kept fixed and the aspect ratio of pixels is + adjusted so that the data fit in the axes. In general, this will + result in non-square pixels. + - if None, 'equal' is used for numpy arrays and 'auto' for xarrays + (which have typically heterogeneous coordinates) Returns ------- @@ -137,23 +169,66 @@ def imshow( In order to update and customize the returned figure, use `go.Figure.update_traces` or `go.Figure.update_layout`. + + If an xarray is passed, dimensions names and coordinates are used for + axes labels and ticks. """ args = locals() apply_default_cascade(args) + labels = labels.copy() + if xarray_imported and isinstance(img, xarray.DataArray): + y_label, x_label = img.dims[0], img.dims[1] + # np.datetime64 is not handled correctly by go.Heatmap + for ax in [x_label, y_label]: + if np.issubdtype(img.coords[ax].dtype, np.datetime64): + img.coords[ax] = img.coords[ax].astype(str) + if x is None: + x = img.coords[x_label] + if y is None: + y = img.coords[y_label] + if aspect is None: + aspect = "auto" + if labels.get("x", None) is None: + labels["x"] = x_label + if labels.get("y", None) is None: + labels["y"] = y_label + if labels.get("color", None) is None: + labels["color"] = xarray.plot.utils.label_from_attrs(img) + labels["color"] = labels["color"].replace("\n", "
") + else: + if labels.get("x", None) is None: + labels["x"] = "" + if labels.get("y", None) is None: + labels["y"] = "" + if labels.get("color", None) is None: + labels["color"] = "" + if aspect is None: + aspect = "equal" img = np.asanyarray(img) + # Cast bools to uint8 (also one byte) if img.dtype == np.bool: img = 255 * img.astype(np.uint8) # For 2d data, use Heatmap trace if img.ndim == 2: - trace = go.Heatmap(z=img, coloraxis="coloraxis1") + if y is not None and img.shape[0] != len(y): + raise ValueError( + "The length of the y vector must match the length of the first " + + "dimension of the img matrix." + ) + if x is not None and img.shape[1] != len(x): + raise ValueError( + "The length of the x vector must match the length of the second " + + "dimension of the img matrix." + ) + trace = go.Heatmap(x=x, y=y, z=img, coloraxis="coloraxis1") autorange = True if origin == "lower" else "reversed" - layout = dict( - xaxis=dict(scaleanchor="y", constrain="domain"), - yaxis=dict(autorange=autorange, constrain="domain"), - ) + layout = dict(yaxis=dict(autorange=autorange)) + if aspect == "equal": + layout["xaxis"] = dict(scaleanchor="y", constrain="domain") + layout["yaxis"]["constrain"] = "domain" colorscale_validator = ColorscaleValidator("colorscale", "imshow") if zmin is not None and zmax is None: zmax = img.max() @@ -168,6 +243,8 @@ def imshow( cmin=range_color[0], cmax=range_color[1], ) + if labels["color"]: + layout["coloraxis1"]["colorbar"] = dict(title=labels["color"]) # For 2D+RGB data, use Image trace elif img.ndim == 3 and img.shape[-1] in [3, 4]: @@ -185,12 +262,20 @@ def imshow( ) layout_patch = dict() - for v in ["title", "height", "width"]: - if args[v]: - layout_patch[v] = args[v] + for attr_name in ["title", "height", "width"]: + if args[attr_name]: + layout_patch[attr_name] = args[attr_name] if "title" not in layout_patch and args["template"].layout.margin.t is None: layout_patch["margin"] = {"t": 60} fig = go.Figure(data=trace, layout=layout) fig.update_layout(layout_patch) + fig.update_traces( + hovertemplate="%s: %%{x}
%s: %%{y}
%s: %%{z}" + % (labels["x"] or "x", labels["y"] or "y", labels["color"] or "color",) + ) + if labels["x"]: + fig.update_xaxes(title_text=labels["x"]) + if labels["y"]: + fig.update_yaxes(title_text=labels["y"]) fig.update_layout(template=args["template"], overwrite=True) return fig diff --git a/packages/python/plotly/plotly/tests/test_core/test_px/test_imshow.py b/packages/python/plotly/plotly/tests/test_core/test_px/test_imshow.py index e296a958f3a..7f8c2afd48b 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_px/test_imshow.py +++ b/packages/python/plotly/plotly/tests/test_core/test_px/test_imshow.py @@ -1,6 +1,7 @@ import plotly.express as px import numpy as np import pytest +import xarray as xr img_rgb = np.array([[[255, 0, 0], [0, 255, 0], [0, 0, 255]]], dtype=np.uint8) img_gray = np.arange(100).reshape((10, 10)) @@ -58,8 +59,9 @@ def test_colorscale(): def test_wrong_dimensions(): imgs = [1, np.ones((5,) * 3), np.ones((5,) * 4)] + msg = "px.imshow only accepts 2D single-channel, RGB or RGBA images." for img in imgs: - with pytest.raises(ValueError) as err_msg: + with pytest.raises(ValueError, match=msg): fig = px.imshow(img) @@ -114,3 +116,37 @@ def test_zmin_zmax_range_color(): fig = px.imshow(img, zmax=0.8) assert fig.layout.coloraxis.cmin == 0.0 assert fig.layout.coloraxis.cmax == 0.8 + + +def test_imshow_xarray(): + img = np.random.random((20, 30)) + da = xr.DataArray(img, dims=["dim_rows", "dim_cols"]) + fig = px.imshow(da) + # Dimensions are used for axis labels and coordinates + assert fig.layout.xaxis.title.text == "dim_cols" + assert fig.layout.yaxis.title.text == "dim_rows" + assert np.all(np.array(fig.data[0].x) == np.array(da.coords["dim_cols"])) + + +def test_imshow_labels_and_ranges(): + fig = px.imshow([[1, 2], [3, 4], [5, 6]],) + assert fig.layout.xaxis.title.text is None + assert fig.layout.yaxis.title.text is None + assert fig.layout.coloraxis.colorbar.title.text is None + assert fig.data[0].x is None + assert fig.data[0].y is None + fig = px.imshow( + [[1, 2], [3, 4], [5, 6]], + x=["a", "b"], + y=["c", "d", "e"], + labels=dict(x="the x", y="the y", color="the color"), + ) + # Dimensions are used for axis labels and coordinates + assert fig.layout.xaxis.title.text == "the x" + assert fig.layout.yaxis.title.text == "the y" + assert fig.layout.coloraxis.colorbar.title.text == "the color" + assert fig.data[0].x[0] == "a" + assert fig.data[0].y[0] == "c" + + with pytest.raises(ValueError): + fig = px.imshow([[1, 2], [3, 4], [5, 6]], x=["a"]) diff --git a/packages/python/plotly/tox.ini b/packages/python/plotly/tox.ini index b1f1cca2f71..095ec2fd3e9 100644 --- a/packages/python/plotly/tox.ini +++ b/packages/python/plotly/tox.ini @@ -58,6 +58,7 @@ deps= retrying==1.3.3 pytest==3.5.1 pandas==0.24.2 + xarray==0.10.9 backports.tempfile==1.0 optional: --editable=file:///{toxinidir}/../plotly-geo optional: numpy==1.16.5 @@ -71,7 +72,6 @@ deps= optional: pyshp==1.2.10 optional: pillow==5.2.0 optional: matplotlib==2.2.3 - optional: xarray==0.10.9 optional: scikit-image==0.14.4 ; CORE ENVIRONMENTS