Plotly: Create a Scatter with categorical x-axis jitter and multi level axis
Asked Answered
N

3

14

I would like to make a graph with a multi-level x axis like in the following picture: Multi-level scatter

import plotly.graph_objects as go
fig = go.Figure()
fig.add_trace(
  go.Scatter(
    x = [df['x'], df['x1']],
    y = df['y'],
    mode='markers'
  )
)

But also I would like to put jitter on the x-axis like in the next picture: enter image description here

So far I can make each graph independently using the next code:

import plotly.express as px
fig = px.strip(df,
               x=[df["x"], df['x1']], 
               y="y",
               stripmode='overlay') 

Is it possible to combine the jitter and the multi-level axis in one plot?

Here is a code to reproduce the dataset:

import numpy as np
import pandas as pd
import random

'''Create DataFrame'''
price = np.append(
  np.random.normal(20, 5, size=(1, 50)), np.random.normal(40, 2, size=(1, 10))
)
quantity = np.append(
  np.random.randint(1, 5, size=(50)), np.random.randint(8, 12, size=(10))
)

firstLayerList = ['15 in', '16 in']
secondLayerList = ['1/2', '3/8']
vendorList = ['Vendor1','Vendor2','Vendor3']

data = {
  'Width':  [random.choice(firstLayerList) for i in range(len(price))],
  'Length': [random.choice(secondLayerList) for i in range(len(price))],
  'Vendor': [random.choice(vendorList) for i in range(len(price))],
  'Quantity': quantity,
  'Price':  price
}
df = pd.DataFrame.from_dict(data)
Nonmoral answered 27/11, 2020 at 22:19 Comment(8)
Please provide a data sample to reproduce your figuresAugustineaugustinian
And there is @Augustineaugustinian - always a step ahead on Plotly questions. Was actually just about to tag you on this one ... but I see there’s no need. :-)Wealth
@S3DEV Haha =) This turned out to be a bit too tricky for a saturday evening. Gonna have to put it on hold for a while. Unless the OP provides a data sample of course. Cause that's the thing... I always spend more time recreating a problem than actually solving it, so good questions with proper data samples are always harder to stay away from. Btw, I really enoyed your recent work on the y-axes.Augustineaugustinian
@Augustineaugustinian - Agreed! Sometimes those synthetic datasets are tricky. Much appreciated, very kind of you. The graph was a mess in the end, but solved the question. Later mate.Wealth
I added some code to create a sample data. Thanks!Nonmoral
Thank you Daniel. To make life easier on ourselves, would combining the xaxis labels be an option? For example '15 3/8', '15 1/2', '16', ... ?Wealth
@S3DEV I would prefer not to but I don't mind if it turns out to be the only option.Nonmoral
Daniel - Thank you so much for your patience on this one. I've popped an answer on for you; hope it helps you out.Wealth
W
20

Firstly - thanks for the challenge! There aren't many challenging Plotly questions these days.

The key elements to creating a scatter graph with jitter are:

  • Using mode: 'box' - to create a box-plot, not a scatter plot.
  • Setting 'boxpoints': 'all' - so all points are plotted.
  • Using 'pointpos': 0 - to center the points on the x-axis.
  • Removing (hiding!) the whisker boxes using:
    • 'fillcolor': 'rgba(255,255,255,0)'
    • 'line': {'color': 'rgba(255,255,255,0)'}

DataFrame preparation:

This code simply splits the main DataFrame into a frame for each vendor, thus allowing a trace to be created for each, with their own colour.

df1 = df[df['Vendor'] == 'Vendor1']
df2 = df[df['Vendor'] == 'Vendor2']
df3 = df[df['Vendor'] == 'Vendor3']

Plotting code:

The plotting code could use a for-loop if you like. However, I've intentionally kept it more verbose, so as to increase clarity.

import plotly.io as pio

layout = {'title': 'Categorical X-Axis, with Jitter'}
traces = []

traces.append({'x': [df1['Width'], df1['Length']], 'y': df1['Price'], 'name': 'Vendor1', 'marker': {'color': 'green'}})
traces.append({'x': [df2['Width'], df2['Length']], 'y': df2['Price'], 'name': 'Vendor2', 'marker': {'color': 'blue'}})
traces.append({'x': [df3['Width'], df3['Length']], 'y': df3['Price'], 'name': 'Vendor3', 'marker': {'color': 'orange'}})

# Update (add) trace elements common to all traces.
for t in traces:
    t.update({'type': 'box',
              'boxpoints': 'all',
              'fillcolor': 'rgba(255,255,255,0)',
              'hoveron': 'points',
              'hovertemplate': 'value=%{x}<br>Price=%{y}<extra></extra>',
              'line': {'color': 'rgba(255,255,255,0)'},
              'pointpos': 0,
              'showlegend': True})

pio.show({'data': traces, 'layout': layout})

Graph:

The data behind this graph was generated using np.random.seed(73), against the dataset creation code posted in the question.

enter image description here

Comments (TL;DR):

The example code shown here uses the lower-level Plotly API, rather than a convenience wrapper such as graph_objects or express. The reason is that I (personally) feel it's helpful to users to show what is occurring 'under the hood', rather than masking the underlying code logic with a convenience wrapper.

This way, when the user needs to modify a finer detail of the graph, they will have a better understanding of the lists and dicts which Plotly is constructing for the underlying graphing engine (orca).

And this use-case is a prime example of this reasoning, as it’s edging Plotly past its (current) design point.

Wealth answered 29/11, 2020 at 20:47 Comment(4)
This is exactly what I needed! Thanks so much @S3DEV for your help!Nonmoral
@DanielZapata - My pleasure. Glad it’s working for you.Wealth
This doesn't seem to work, as it also hides the points. However, if I change 'line': {'color': 'rgba(255,255,255,0)'} to 'line': {'width': 0} then it works.Fenian
any creative idea to achieve the same using a scatter plot? The reason is that it allows to also configure the error bars per point (which is needed for my use-case). I'm using plotly.express and looking for some post-processing to add a horizontal jitter to the points within each category....Probate
A
3

An alternative straightforward solution might be using: plotly.express.strip with stripmode="overlay" (more info about parameters)

Here I show you an example with the Iris data. Plotly version 4.4.1.

import plotly.express as px
df = px.data.iris()
fig = px.strip(df, 
               x="species", y="sepal_width", color="species", 
               title="This is a stripplot!", 
               stripmode = "overlay"   # Select between "group" or "overlay" mode
)
fig.show()

This is the result (run the code snippet)

<div>
        
                <script type="text/javascript">window.PlotlyConfig = {MathJaxConfig: 'local'};</script>
        <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>    
            <div id="70e0d94a-4a4c-40fc-af77-95274959151b" class="plotly-graph-div" style="height:100%; width:100%;"></div>
            <script type="text/javascript">
                
                    window.PLOTLYENV=window.PLOTLYENV || {};
                    
                if (document.getElementById("70e0d94a-4a4c-40fc-af77-95274959151b")) {
                    Plotly.newPlot(
                        '70e0d94a-4a4c-40fc-af77-95274959151b',
                        [{"alignmentgroup": "True", "boxpoints": "all", "fillcolor": "rgba(255,255,255,0)", "hoverlabel": {"namelength": 0}, "hoveron": "points", "hovertemplate": "species=%{x}<br>sepal_width=%{y}", "legendgroup": "species=setosa", "line": {"color": "rgba(255,255,255,0)"}, "marker": {"color": "#636efa"}, "name": "species=setosa", "offsetgroup": "species=setosa", "orientation": "v", "pointpos": 0, "showlegend": true, "type": "box", "x": ["setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa", "setosa"], "x0": " ", "xaxis": "x", "y": [3.5, 3.0, 3.2, 3.1, 3.6, 3.9, 3.4, 3.4, 2.9, 3.1, 3.7, 3.4, 3.0, 3.0, 4.0, 4.4, 3.9, 3.5, 3.8, 3.8, 3.4, 3.7, 3.6, 3.3, 3.4, 3.0, 3.4, 3.5, 3.4, 3.2, 3.1, 3.4, 4.1, 4.2, 3.1, 3.2, 3.5, 3.1, 3.0, 3.4, 3.5, 2.3, 3.2, 3.5, 3.8, 3.0, 3.8, 3.2, 3.7, 3.3], "y0": " ", "yaxis": "y"}, {"alignmentgroup": "True", "boxpoints": "all", "fillcolor": "rgba(255,255,255,0)", "hoverlabel": {"namelength": 0}, "hoveron": "points", "hovertemplate": "species=%{x}<br>sepal_width=%{y}", "legendgroup": "species=versicolor", "line": {"color": "rgba(255,255,255,0)"}, "marker": {"color": "#EF553B"}, "name": "species=versicolor", "offsetgroup": "species=versicolor", "orientation": "v", "pointpos": 0, "showlegend": true, "type": "box", "x": ["versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor"], "x0": " ", "xaxis": "x", "y": [3.2, 3.2, 3.1, 2.3, 2.8, 2.8, 3.3, 2.4, 2.9, 2.7, 2.0, 3.0, 2.2, 2.9, 2.9, 3.1, 3.0, 2.7, 2.2, 2.5, 3.2, 2.8, 2.5, 2.8, 2.9, 3.0, 2.8, 3.0, 2.9, 2.6, 2.4, 2.4, 2.7, 2.7, 3.0, 3.4, 3.1, 2.3, 3.0, 2.5, 2.6, 3.0, 2.6, 2.3, 2.7, 3.0, 2.9, 2.9, 2.5, 2.8], "y0": " ", "yaxis": "y"}, {"alignmentgroup": "True", "boxpoints": "all", "fillcolor": "rgba(255,255,255,0)", "hoverlabel": {"namelength": 0}, "hoveron": "points", "hovertemplate": "species=%{x}<br>sepal_width=%{y}", "legendgroup": "species=virginica", "line": {"color": "rgba(255,255,255,0)"}, "marker": {"color": "#00cc96"}, "name": "species=virginica", "offsetgroup": "species=virginica", "orientation": "v", "pointpos": 0, "showlegend": true, "type": "box", "x": ["virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica", "virginica"], "x0": " ", "xaxis": "x", "y": [3.3, 2.7, 3.0, 2.9, 3.0, 3.0, 2.5, 2.9, 2.5, 3.6, 3.2, 2.7, 3.0, 2.5, 2.8, 3.2, 3.0, 3.8, 2.6, 2.2, 3.2, 2.8, 2.8, 2.7, 3.3, 3.2, 2.8, 3.0, 2.8, 3.0, 2.8, 3.8, 2.8, 2.8, 2.6, 3.0, 3.4, 3.1, 3.0, 3.1, 3.1, 3.1, 2.7, 3.2, 3.3, 3.0, 2.5, 3.0, 3.4, 3.0], "y0": " ", "yaxis": "y"}],
                        {"boxmode": "overlay", "legend": {"tracegroupgap": 0}, "template": {"data": {"bar": [{"error_x": {"color": "#2a3f5f"}, "error_y": {"color": "#2a3f5f"}, "marker": {"line": {"color": "#E5ECF6", "width": 0.5}}, "type": "bar"}], "barpolar": [{"marker": {"line": {"color": "#E5ECF6", "width": 0.5}}, "type": "barpolar"}], "carpet": [{"aaxis": {"endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f"}, "baxis": {"endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f"}, "type": "carpet"}], "choropleth": [{"colorbar": {"outlinewidth": 0, "ticks": ""}, "type": "choropleth"}], "contour": [{"colorbar": {"outlinewidth": 0, "ticks": ""}, "colorscale": [[0.0, "#0d0887"], [0.1111111111111111, "#46039f"], [0.2222222222222222, "#7201a8"], [0.3333333333333333, "#9c179e"], [0.4444444444444444, "#bd3786"], [0.5555555555555556, "#d8576b"], [0.6666666666666666, "#ed7953"], [0.7777777777777778, "#fb9f3a"], [0.8888888888888888, "#fdca26"], [1.0, "#f0f921"]], "type": "contour"}], "contourcarpet": [{"colorbar": {"outlinewidth": 0, "ticks": ""}, "type": "contourcarpet"}], "heatmap": [{"colorbar": {"outlinewidth": 0, "ticks": ""}, "colorscale": [[0.0, "#0d0887"], [0.1111111111111111, "#46039f"], [0.2222222222222222, "#7201a8"], [0.3333333333333333, "#9c179e"], [0.4444444444444444, "#bd3786"], [0.5555555555555556, "#d8576b"], [0.6666666666666666, "#ed7953"], [0.7777777777777778, "#fb9f3a"], [0.8888888888888888, "#fdca26"], [1.0, "#f0f921"]], "type": "heatmap"}], "heatmapgl": [{"colorbar": {"outlinewidth": 0, "ticks": ""}, "colorscale": [[0.0, "#0d0887"], [0.1111111111111111, "#46039f"], [0.2222222222222222, "#7201a8"], [0.3333333333333333, "#9c179e"], [0.4444444444444444, "#bd3786"], [0.5555555555555556, "#d8576b"], [0.6666666666666666, "#ed7953"], [0.7777777777777778, "#fb9f3a"], [0.8888888888888888, "#fdca26"], [1.0, "#f0f921"]], "type": "heatmapgl"}], "histogram": [{"marker": {"colorbar": {"outlinewidth": 0, "ticks": ""}}, "type": "histogram"}], "histogram2d": [{"colorbar": {"outlinewidth": 0, "ticks": ""}, "colorscale": [[0.0, "#0d0887"], [0.1111111111111111, "#46039f"], [0.2222222222222222, "#7201a8"], [0.3333333333333333, "#9c179e"], [0.4444444444444444, "#bd3786"], [0.5555555555555556, "#d8576b"], [0.6666666666666666, "#ed7953"], [0.7777777777777778, "#fb9f3a"], [0.8888888888888888, "#fdca26"], [1.0, "#f0f921"]], "type": "histogram2d"}], "histogram2dcontour": [{"colorbar": {"outlinewidth": 0, "ticks": ""}, "colorscale": [[0.0, "#0d0887"], [0.1111111111111111, "#46039f"], [0.2222222222222222, "#7201a8"], [0.3333333333333333, "#9c179e"], [0.4444444444444444, "#bd3786"], [0.5555555555555556, "#d8576b"], [0.6666666666666666, "#ed7953"], [0.7777777777777778, "#fb9f3a"], [0.8888888888888888, "#fdca26"], [1.0, "#f0f921"]], "type": "histogram2dcontour"}], "mesh3d": [{"colorbar": {"outlinewidth": 0, "ticks": ""}, "type": "mesh3d"}], "parcoords": [{"line": {"colorbar": {"outlinewidth": 0, "ticks": ""}}, "type": "parcoords"}], "pie": [{"automargin": true, "type": "pie"}], "scatter": [{"marker": {"colorbar": {"outlinewidth": 0, "ticks": ""}}, "type": "scatter"}], "scatter3d": [{"line": {"colorbar": {"outlinewidth": 0, "ticks": ""}}, "marker": {"colorbar": {"outlinewidth": 0, "ticks": ""}}, "type": "scatter3d"}], "scattercarpet": [{"marker": {"colorbar": {"outlinewidth": 0, "ticks": ""}}, "type": "scattercarpet"}], "scattergeo": [{"marker": {"colorbar": {"outlinewidth": 0, "ticks": ""}}, "type": "scattergeo"}], "scattergl": [{"marker": {"colorbar": {"outlinewidth": 0, "ticks": ""}}, "type": "scattergl"}], "scattermapbox": [{"marker": {"colorbar": {"outlinewidth": 0, "ticks": ""}}, "type": "scattermapbox"}], "scatterpolar": [{"marker": {"colorbar": {"outlinewidth": 0, "ticks": ""}}, "type": "scatterpolar"}], "scatterpolargl": [{"marker": {"colorbar": {"outlinewidth": 0, "ticks": ""}}, "type": "scatterpolargl"}], "scatterternary": [{"marker": {"colorbar": {"outlinewidth": 0, "ticks": ""}}, "type": "scatterternary"}], "surface": [{"colorbar": {"outlinewidth": 0, "ticks": ""}, "colorscale": [[0.0, "#0d0887"], [0.1111111111111111, "#46039f"], [0.2222222222222222, "#7201a8"], [0.3333333333333333, "#9c179e"], [0.4444444444444444, "#bd3786"], [0.5555555555555556, "#d8576b"], [0.6666666666666666, "#ed7953"], [0.7777777777777778, "#fb9f3a"], [0.8888888888888888, "#fdca26"], [1.0, "#f0f921"]], "type": "surface"}], "table": [{"cells": {"fill": {"color": "#EBF0F8"}, "line": {"color": "white"}}, "header": {"fill": {"color": "#C8D4E3"}, "line": {"color": "white"}}, "type": "table"}]}, "layout": {"annotationdefaults": {"arrowcolor": "#2a3f5f", "arrowhead": 0, "arrowwidth": 1}, "coloraxis": {"colorbar": {"outlinewidth": 0, "ticks": ""}}, "colorscale": {"diverging": [[0, "#8e0152"], [0.1, "#c51b7d"], [0.2, "#de77ae"], [0.3, "#f1b6da"], [0.4, "#fde0ef"], [0.5, "#f7f7f7"], [0.6, "#e6f5d0"], [0.7, "#b8e186"], [0.8, "#7fbc41"], [0.9, "#4d9221"], [1, "#276419"]], "sequential": [[0.0, "#0d0887"], [0.1111111111111111, "#46039f"], [0.2222222222222222, "#7201a8"], [0.3333333333333333, "#9c179e"], [0.4444444444444444, "#bd3786"], [0.5555555555555556, "#d8576b"], [0.6666666666666666, "#ed7953"], [0.7777777777777778, "#fb9f3a"], [0.8888888888888888, "#fdca26"], [1.0, "#f0f921"]], "sequentialminus": [[0.0, "#0d0887"], [0.1111111111111111, "#46039f"], [0.2222222222222222, "#7201a8"], [0.3333333333333333, "#9c179e"], [0.4444444444444444, "#bd3786"], [0.5555555555555556, "#d8576b"], [0.6666666666666666, "#ed7953"], [0.7777777777777778, "#fb9f3a"], [0.8888888888888888, "#fdca26"], [1.0, "#f0f921"]]}, "colorway": ["#636efa", "#EF553B", "#00cc96", "#ab63fa", "#FFA15A", "#19d3f3", "#FF6692", "#B6E880", "#FF97FF", "#FECB52"], "font": {"color": "#2a3f5f"}, "geo": {"bgcolor": "white", "lakecolor": "white", "landcolor": "#E5ECF6", "showlakes": true, "showland": true, "subunitcolor": "white"}, "hoverlabel": {"align": "left"}, "hovermode": "closest", "mapbox": {"style": "light"}, "paper_bgcolor": "white", "plot_bgcolor": "#E5ECF6", "polar": {"angularaxis": {"gridcolor": "white", "linecolor": "white", "ticks": ""}, "bgcolor": "#E5ECF6", "radialaxis": {"gridcolor": "white", "linecolor": "white", "ticks": ""}}, "scene": {"xaxis": {"backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white"}, "yaxis": {"backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white"}, "zaxis": {"backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white"}}, "shapedefaults": {"line": {"color": "#2a3f5f"}}, "ternary": {"aaxis": {"gridcolor": "white", "linecolor": "white", "ticks": ""}, "baxis": {"gridcolor": "white", "linecolor": "white", "ticks": ""}, "bgcolor": "#E5ECF6", "caxis": {"gridcolor": "white", "linecolor": "white", "ticks": ""}}, "title": {"x": 0.05}, "xaxis": {"automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": {"standoff": 15}, "zerolinecolor": "white", "zerolinewidth": 2}, "yaxis": {"automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": {"standoff": 15}, "zerolinecolor": "white", "zerolinewidth": 2}}}, "title": {"text": "This is a stripplot!"}, "xaxis": {"anchor": "y", "categoryarray": ["setosa", "versicolor", "virginica"], "categoryorder": "array", "domain": [0.0, 1.0], "title": {"text": "species"}}, "yaxis": {"anchor": "x", "domain": [0.0, 1.0], "title": {"text": "sepal_width"}}},
                        {"responsive": true}
                    )
                };
                
            </script>
        </div>
Atory answered 15/7, 2021 at 8:16 Comment(0)
D
0

Some plotly.express functions provide a facet argument which allows you to create a plot that closely resembles the multi-level x-axis from your example, without calls to the low-level API.

Starting wiht some sample data and imports:

import plotly.express as px
import pandas as pd
import numpy as np

# Create data
df = pd.DataFrame(data={"y": np.random.uniform(low=0, high=45, size=100)})
df["x"] = [*["3/8"] * 25, *["1/2"] * 25, *["3/8"] * 25, *["1/2"] * 25]
df["x1"] = [*["16 in"] * 50, *["15 in"] * 50]
print(df.head().to_markdown())
y x x1
0 27.1279 3/8 16 in
1 13.8564 3/8 16 in
2 11.8026 3/8 16 in
3 25.3769 3/8 16 in
4 12.1194 3/8 16 in

You can provide one of the columns from the dataframe to the facet_col argument:

px.strip(df, x=["x", "x1"], y="y", stripmode='overlay', facet_col="x1")

Which results in:

px.strip plot with facet_col

If you like, you can further tweak the appearance of the subplot titles.

Detective answered 24/11, 2022 at 10:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.