How to add Planes in a 3D Scatter Plot - python

Using Blender created this model
that can be seen in A-frame in this link
This model is great and it gives an overview of what I'm trying to accomplish here. Basically, instead of having the names, I'd have dots that symbolize one specific platform.
The best way to achieve it with current state of the art, at my sight, is through Plotly 3D Scatter Plots. I've got the following scatterplot
import plotly.express as px
import pandas as pd
df = pd.read_csv('https://raw.githubusercontent.com/tiago-peres/immersion/master/Platforms_dataset.csv')
fig = px.scatter_3d(df, x='Functionality ', y='Accessibility', z='Immersion', color='Platforms')
fig.show()
that by going to this link you'll be able to click a button and open it in Colab
This nearly looks like the model. Yet, still in need to add three planes to the plot in specific locations. More precisely, in x=?, y=? and z=? (I'm using question mark because the value can be anything stablished).
In other words, want to add three planes to that scatterplot
x = 10
y = 30
z = 40
In the documentation, what closely resembles what I want was 3D Surface Plots.
I've done research and found two similar questions with R
Insert 2D plane into a 3D Plotly scatter plot in R
Add Regression Plane to 3d Scatter Plot in Plotly

I think you might be looking for the add_trace function in plotly so you can just create the surfaces and then add them to the figure:
Also, note, there's definitely ways to simplify this code, but for a general idea:
import plotly.express as px
import pandas as pd
import plotly.graph_objects as go
import numpy as np
fig = px.scatter_3d(df, x='Functionality ', y='Accessibility', z='Immersion', color='Platforms')
bright_blue = [[0, '#7DF9FF'], [1, '#7DF9FF']]
bright_pink = [[0, '#FF007F'], [1, '#FF007F']]
light_yellow = [[0, '#FFDB58'], [1, '#FFDB58']]
# need to add starting point of 0 to each dimension so the plane extends all the way out
zero_pt = pd.Series([0])
z = zero_pt.append(df['Immersion'], ignore_index = True).reset_index(drop = True)
y = zero_pt.append(df['Accessibility'], ignore_index = True).reset_index(drop = True)
x = zero_pt.append(df['Functionality '], ignore_index = True).reset_index(drop = True)
length_data = len(z)
z_plane_pos = 40*np.ones((length_data,length_data))
fig.add_trace(go.Surface(x=x, y=y, z=z_plane_pos, colorscale=light_yellow, showscale=False))
fig.add_trace(go.Surface(x=x.apply(lambda x: 10), y=y, z = np.array([z]*length_data), colorscale= bright_blue, showscale=False))
fig.add_trace(go.Surface(x=x, y= y.apply(lambda x: 30), z = np.array([z]*length_data).transpose(), colorscale=bright_pink, showscale=False))

Related

Python Plotly hoverover information for two or more heatmaps

I am plotting 3 heatmaps in plotly on top of each other and would like to display the z value of all 3 when I hover over the (x,y) points.
I have seen that for scatter plots you can use unified x to display the info of all plots on hover. Is there a similar way to do a unified z for heatmap plots?
I have also seen that you can create a data frame of custom texts and use those as hoverlabels but that seems a little too excessive for what I'm trying to do.
Thanks
this is effectively the answer Plotly Python - Heatmap - Change Hovertext (x,y,z)
simulated 3 heat maps on top of each other
build text array which are the z values across all three layers
import pandas as pd
import numpy as np
import plotly.graph_objects as go
dfs = [pd.DataFrame(index=list("abcd"), columns=list("ab"),
data=np.where(np.random.randint(1, 8, [4, 2]) == 1,
np.nan, np.random.randint(1, 500, [4, 2]),)
)
for i in range(3)]
# create text array same shape as z
text = pd.concat(dfs).groupby(level=0).agg({c:lambda v: ", ".join(v.astype(str)) for c in dfs[0].columns}).values
# figure
go.Figure([go.Heatmap(z=df.values, x=df.columns, y=df.index, name=i, text=text, hoverinfo="text")
for i, df in enumerate(dfs)
])

how do we plot plotly surface plots with frequency values on Z axis python

I have data as given below and I would want to plot interactive surface plot with frequency of values in x and y on z-axis
**apples** **oranges**
0 1
10 2
20 1
10 1
10 1
I tried below code
import plotly.graph_objects as go
import pandas as pd
import numpy as np
#
z_data = df.groupby(['apples', 'oranges']).count()
x, y = df['apples'], df['oranges']
sh_0, sh_1 = z.shape
fig = go.Figure(data=[go.Surface(z=z, x=x, y=y)])
#fig.update_layout(title='histogram_trial', autosize=True)
fig.update_layout(title='Mt Bruno Elevation', autosize=False,
width=500, height=500,
margin=dict(l=65, r=50, b=65, t=90))
fig.show()
above code gives me blank output. Please let me know how do I get counts on z-axis
There is a couple of issues with your code.
DataFrameGroupBy.count won't calculate anything because you
don't have non-key columns to be subject to the aggregate. Use either
DataFrameGroupBy.size or DataFrame.value_counts.
plotly.graph_objects.Surface has special input requirements:
The data the describes the coordinates of the surface is set in z. Data in z should be a 2D list. Coordinates in x and y can either be 1D lists or 2D lists (e.g. to graph parametric surfaces). If not provided in x and y, the x and y coordinates are assumed to be linear starting at 0 with a unit step.
Without fulfilling (2) you can only plot Scatter3d or Mesh3d. But with DataFrame.pivot_table you can get the data in the right shape.
import numpy as np
import pandas as pd
import plotly.graph_objects as go
raw_df = pd.DataFrame(np.random.randint(0, 10, size=(50, 2)), columns=['x', 'y'])
df = raw_df.value_counts(['x', 'y']).reset_index(name='z')
surf_df = df.pivot_table(index=['y'], columns=['x'], values=['z'], fill_value=0)
fig = go.Figure()
fig.add_trace(go.Scatter3d(x=df['x'], y=df['y'], z=df['z'], mode='markers'))
fig.add_trace(go.Surface(z=surf_df))
fig

Plotly: How to add annotations to different intervals of the y-axis?

I'm trying to add annoation to y axis based on different inverval of y value
if y > 0, I want to give the annotation of Flexion
if y < 0, I want to give the annotation of Extension
I tried to use multicategory to specify the annotation
my code is show below
import plotly.graph_objects as go
import numpy as np
x = np.arange(-10,10,1)
y = np.arange(-10,10,1)
y_annotation = [ 'Flexion' if data > 0 else 'Extension' for data in y ]
fig = go.Figure( data= go.Scatter(x=x,y=[y_annotation,y]) )
fig.show()
This will produce
but I don't want the lines to seperate the Flexision and Extension
and this method will give detailed y values on the y axis, which is also I don't want to have
I'm wondering if there's another way to add annotation to y axis based on different interval?
Thanks !
If you're happy with the setup above besides the lines and detailed y-axis, then you can drop the multi index approach and just set up annotations at the appropriate positions using fig.add_annotation()
The following figure is produced with the snippet below that:
makes room for your annotations on the left side using fig.update_layout(margin=dict(l=150)),
stores interval names and data in a dict, and
calculates the middle values of each specified interval, and
places the annotations to the left of the y-axis using xref="paper", and
does not mess up the values of the y-axis tickmarks.
Plot
Complete code:
import plotly.graph_objects as go
import numpy as np
x = np.arange(-10,10,1)
y = np.arange(-10,10,1)
y_annotation = [ 'Flexion' if data > 0 else 'Extension' for data in y ]
intervals = {'Flexion':[0,10],
'Extension':[0, -10]}
# plotly setup
fig = go.Figure( data= go.Scatter(x=x,y=y) )
# make room for annotations
fig.update_layout(margin=dict(l=150))
for k in intervals.keys():
fig.add_annotation(dict(font=dict(color="green",size=14),
#x=x_loc,
x=-0.16,
y=(intervals[k][0]+intervals[k][1])/2,
showarrow=False,
text="<i>"+k+"</i>",
textangle=0,
xref="paper",
yref="y"
))
fig.show()

Ploting multiple curves (x, y1, y2, x, y3, y4) in the same plot

I'm trying to plot a graph with four different values on the "y" axis. So, I have 6 arrays, 2 of which have elements that represent the time values ​​of the "x" axis and the other 4 represent the corresponding elements (in the same position) in relation to the "y" axis.
Example:
LT_TIME = ['18:14:17.566 ', '18:14:17.570']
LT_RP = [-110,-113]
LT_RQ = [-3,-5]
GNR_TIME = ['18: 15: 42.489', '18:32:39.489']
GNR_RP = [-94, -94]
GNR_RQ = [-3, -7]
The coordinates of the "LT" graph are:
('18:14:17.566',-110), ('18:14:17.570',-113), ('18:14:17.566',-3), ('18:14:17.570',-5)
And with these coordinates, I can generate a graph with two "y" axes, which contains the points (-110,-113,-3,-5) and an "x" axis with the points ('18:14:17.566', '18:14:17.570').
Similarly, it is possible to do the same "GNR" arrays. So, how can I have all the Cartesian points on both the "LT" and "GNR" arrays on the same graph??? I mean, how to plot so that I have the following coordinates on the same graph:
('18:14:17.566',-110), ('18:14:17.570 ',-113), ('18:14:17.566',-3), ('18:14:17.570',-5),
('18:15:42.489',-94), ('18:32:39.489',-94), ('18:15:42.489',-3), ('18:32:39.489',-7)
It sounds like your problem has two parts: formatting the data in a way that visualisation libraries would understand and actually visualising it using a dual axis.
Your example screenshot includes some interactive controls so I suggest you use bokeh which gives you zoom and pan for "free" rather than matplotlib. Besides, I find that bokeh's way of adding dual axis is more straight-forward. If matplotlib is a must, here's another answer that should point you in the right direction.
For the first part, you can merge the data you have into a single dataframe, like so:
import pandas as pd
from bokeh.models import LinearAxis, Range1d, ColumnDataSource
from bokeh.plotting import figure, output_notebook, show
output_notebook() #if working in Jupyter Notebook, output_file() if not
LT_TIME = ['18:14:17.566 ', '18:14:17.570']
LT_RP = [-110,-113]
LT_RQ = [-3,-5]
GNR_TIME = ['18: 15: 42.489', '18:32:39.489']
GNR_RP = [-94, -94]
GNR_RQ = [-3, -7]
s1 = list(zip(LT_TIME, LT_RP)) + list(zip(GNR_TIME, GNR_RP))
s2 = list(zip(LT_TIME, LT_RQ)) + list(zip(GNR_TIME, GNR_RQ))
df1 = pd.DataFrame(s1, columns=["Date", "RP"])
df2 = pd.DataFrame(s2, columns=["Date", "RQ"])
df = df1.merge(df2, on="Date")
source = ColumnDataSource(df)
To visualise the data as a dual axis line chart, we just need to specify the extra y-axis and position it in the layout:
p = figure(x_range=df["Date"], y_range=(-90, -120))
p.line(x="Date", y="RP", color="cadetblue", line_width=2, source=source)
p.extra_y_ranges = {"RQ": Range1d(start=0, end=-10)}
p.line(x="Date", y="RQ", color="firebrick", line_width=2, y_range_name="RQ", source=source)
p.add_layout(LinearAxis(y_range_name="RQ"), 'right')
show(p)

Bokeh: linking a line plot and a scatter plot

I have a line plot and a scatter plot that are conceptually linked by sample IDs, i.e. each dot on the 2D scatter plot corresponds to a line on the line plot.
While I have done linked plotting before using scatter plots, I have not seen examples of this for the situation above - where I select dots and thus selectively view lines.
Is it possible to link dots on a scatter plot to a line on a line plot? If so, is there an example implementation available online?
Searching the web for bokeh link line and scatter plot yields no examples online, as of 14 August 2018.
I know this is a little late - but maybe this snippet of code will help?
import numpy as np
from bokeh.io import output_file, show
from bokeh.layouts import gridplot
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
from bokeh.models import Circle,MultiLine
def play():
x = np.linspace(0,10,100)
y = np.random.rand(100)
xs = np.random.rand(100,3)
ys = np.random.normal(size=(100,3))
xp = [list(xi) for xi in xs] # Because Multi-List does not like numpy arrays
yp = [list(yi) for yi in ys]
output_file('play.html')
source = ColumnDataSource(data=dict(x=x,y=y,xp=xp,yp=yp))
TOOLS = 'box_select'
left = figure(tools=TOOLS,plot_width=700,plot_height=700)
c1 = left.circle('x','y',source=source)
c1.nonselection_glyph = Circle(fill_color='gray',fill_alpha=0.4,
line_color=None)
c1.selection_glyph = Circle(fill_color='orange',line_color=None)
right = figure(tools=TOOLS,plot_width=700,plot_height=700)
c2 = right.multi_line(xs='xp',ys='yp',source=source)
c2.nonselection_glyph = MultiLine(line_color='gray',line_alpha=0.2)
c2.selection_glyph = MultiLine(line_color='orange')
p = gridplot([[left, right]])
show(p)
As things turn out, I was able to make this happen by using HoloViews rather than Bokeh. The relevant example for making this work comes from the Selection1d tap stream.
http://holoviews.org/reference/streams/bokeh/Selection1D_tap.html#selection1d-tap
I will do an annotated version of the example below.
First, we begin with imports. (Note: all of this assumes work is being done in the Jupyter notebook.)
import numpy as np
import holoviews as hv
from holoviews.streams import Selection1D
from scipy import stats
hv.extension('bokeh')
First off, we set some styling options for the charts. In my experience, I usually build the chart before styling it, though.
%%opts Scatter [color_index=2 tools=['tap', 'hover'] width=600] {+framewise} (marker='triangle' cmap='Set1' size=10)
%%opts Overlay [toolbar='above' legend_position='right'] Curve (line_color='black') {+framewise}
This function below generates data.
def gen_samples(N, corr=0.8):
xx = np.array([-0.51, 51.2])
yy = np.array([0.33, 51.6])
means = [xx.mean(), yy.mean()]
stds = [xx.std() / 3, yy.std() / 3]
covs = [[stds[0]**2 , stds[0]*stds[1]*corr],
[stds[0]*stds[1]*corr, stds[1]**2]]
return np.random.multivariate_normal(means, covs, N)
data = [('Week %d' % (i%10), np.random.rand(), chr(65+np.random.randint(5)), i) for i in range(100)]
sample_data = hv.NdOverlay({i: hv.Points(gen_samples(np.random.randint(1000, 5000), r2))
for _, r2, _, i in data})
The real magic begins here. First off, we set up a scatterplot using the hv.Scatter object.
points = hv.Scatter(data, ['Date', 'r2'], ['block', 'id']).redim.range(r2=(0., 1))
Then, we create a Selection1D stream. It pulls in points from the points object.
stream = Selection1D(source=points)
We then create a function to display the regression plot on the right. There's an empty plot that is the "default", and then there's a callback that hv.DynamicMap calls on.
empty = (hv.Points(np.random.rand(0, 2)) * hv.Curve(np.random.rand(0, 2))).relabel('No selection')
def regression(index):
if not index:
return empty
scatter = sample_data[index[0]]
xs, ys = scatter['x'], scatter['y']
slope, intercep, rval, pval, std = stats.linregress(xs, ys)
xs = np.linspace(*scatter.range(0)+(2,))
reg = slope*xs+intercep
return (scatter * hv.Curve((xs, reg))).relabel('r2: %.3f' % slope)
Now, we create the DynamicMap which dynamically loads the regression curve data.
reg = hv.DynamicMap(regression, kdims=[], streams=[stream])
# Ignoring annotation for average - it is not relevant here.
average = hv.Curve(points, 'Date', 'r2').aggregate(function=np.mean)
Finally, we display the plots.
points * average + reg
The most important thing I learned from building this is that the indices for the points have to be lined up with the indices for the regression curves.
I hope this helps others building awesome viz using HoloViews!

Categories