Keep altair sliders with plots when concatenating - python

When concatenating 2 charts with their own sliders, the sliders are grouped together at the end. Is there a way to have the sliders remain with each plot?
Here is an example, modified from the docs
import altair.vegalite.v3 as alt
import pandas as pd
import numpy as np
rand = np.random.RandomState(42)
df = pd.DataFrame({"xval": range(100), "yval": rand.randn(100).cumsum()})
slider1 = alt.binding_range(min=0, max=100, step=1, name="cutoff1:")
selector1 = alt.selection_single(
name="SelectorName1", fields=["cutoff1"], bind=slider1, init={"cutoff1": 50}
)
slider2 = alt.binding_range(min=0, max=100, step=1, name="cutoff2:")
selector2 = alt.selection_single(
name="SelectorName2", fields=["cutoff2"], bind=slider2, init={"cutoff2": 50}
)
ch_base = (
alt.Chart(df)
.mark_point()
.encode(
x="xval",
y="yval",
color=alt.condition(
alt.datum.xval < selector1.cutoff1, alt.value("red"), alt.value("blue")
),
)
)
ch1 = ch_base.add_selection(selector1)
ch2 = ch_base.encode(
color=alt.condition(
alt.datum.xval < selector2.cutoff2, alt.value("red"), alt.value("blue")
)
).add_selection(selector2)
ch1 & ch2
As seen in the image, the sliders are by default grouped next to each other:

Sliders always appear at the bottom of the full chart. There is currently no way to change this.
If you would like this feature to exist in the future, I would suggest submitting a feature request in Vega-Lite.
As a workaround, you can create two charts, and embed them in a single document using vega-embed, although when you do this it is not trivial to pass signals between the two charts.

Related

Bokeh hover special variable `$data_x` shows number instead of FactorRange category label for multi-line glyph

I am using Bokeh multi_line to show several lines using a categorical x_range,
and would like hover to display the x category hovered. I thought $data_x might help, but it shows numerical values related to category indexes rather than the category labels. I can use CustomJSHover with special_vars["segment_index"] to display what I want, but is there a simpler way?
To demonstrate, this code creates a figure with multi_line():
from collections import defaultdict
import pandas as pd
from bokeh import palettes
from bokeh.plotting import show, figure
from bokeh.models import CustomJSHover, HoverTool
# Substantive data.
df_data = pd.DataFrame.from_records([
dict(date="2001 Q1", output=100, inputs=100),
dict(date="2001 Q2", output=105, inputs=102),
dict(date="2001 Q3", output=110, inputs=105),
])
# Make list of lists for multi_line(), with metadata.
lines_data = defaultdict(list)
for var in ["inputs", "output"]:
lines_data["variable"].append(var)
lines_data["date"].append(df_data["date"])
lines_data["value"].append(df_data[var])
lines_data["color"] = palettes.Category10_10[:2]
fig = figure(
x_range = df_data["date"],
plot_height=400,
)
fig.multi_line(
source = lines_data,
xs = "date",
ys = "value",
color = "color",
legend_group = "variable",
line_width = 5,
line_alpha = 0.6,
hover_line_alpha = 1.0, # Highlight hover line.
)
The hover I want can be created like this using CustomJSHover:
# Custom hover formatting #date.
hover_date = CustomJSHover(
# Show value[$segment_index].
code="""
console.log("> Show value[$segment_index] hover", value);
return "" + value[special_vars["segment_index"]];
""")
fig.add_tools(HoverTool(
tooltips=[
('variable', '#variable'),
('date', '#date{custom}'), # Show hovered date only.
('value', '$data_y'),
],
formatters={'#date': hover_date},
))
show(fig)
Potentially a more straightforward hover specification would use something like $data_x without a custom format, except $data_x itself apparently does not look up the label in the FactorRange (applying this HoverTool instead of the one above):
# Simple hover showing $data_x.
fig.add_tools(HoverTool(
tooltips=[
('variable', '#variable'),
('date', '$data_x'), # Does not show x_range value!
('value', '$data_y'),
]))
show(fig)
Now, hovering over a line shows a 'date' like "1.500" instead of "2001 Q2" etc.
Am I missing a trick, or is CustomJSHover the best way to show the x category?

How to remove points from a dataframe based on a selected area on a plot

I have some experimental data that is often flawed with artifacts exemplified with something like this:
I need a quick way to manually select these random spikes and remove them from datasets.
I figured that any plotting library with a focus on interactive plots should have an easy way to do this but so far I keep struggling with finding a simple way to do what I want.
I'm a Matplotlib/Seaborn guy and this calls for interactive solution. I briefly checked Plotly, Bokeh and Altair and decided to go with the first one. My first attempt looks like this:
import pandas as pd
import plotly.graph_objects as go
from ipywidgets import interactive, HBox, VBox, Button
url='https://drive.google.com/file/d/1hCX8Bn_y30aXVN_TyHTTx015u44pO9yB/view?usp=sharing'
url='https://drive.google.com/uc?id=' + url.split('/')[-2]
df = pd.read_csv(url, index_col=0)
f = go.FigureWidget()
for col in df.columns[-1:]:
f.add_scatter(x = df.index, y=df[col], mode='markers+lines',
selected_marker=dict(size=5, color='red'),
marker=dict(size=1, color='lightgrey', line=dict(width=1, color='lightgrey')))
t = go.FigureWidget([go.Table(
header=dict(values=['selector range'],
fill = dict(color='#C2D4FF'),
align = ['left'] * 5),
cells=dict(values=['None selected' for col in ['ID']],
fill = dict(color='#F5F8FF'),
align = ['left'] * 5)
)])
def selection_fn(trace,points,selector):
t.data[0].cells.values = [selector.xrange]
def update_axes(dataset):
scatter = f.data[0]
scatter.x = df.index
scatter.y = df[dataset]
f.data[0].on_selection(selection_fn)
axis_dropdowns = interactive(update_axes, dataset = df.columns)
button1 = Button(description="Remove points")
button2 = Button(description="Reset")
button3 = Button(description="Fit data")
VBox((HBox((axis_dropdowns.children)), HBox((button1, button2, button3)), f,t))
Which gives:
So I managed to get Selector Box x coordinates (and temporarily print them inside the table widget). But what I couldn't figure out is how to easily bind a function to button1 that would take as an argument Box Selector coordinates and remove selected points from a dataframe and replot the data. So something like this:
def on_button_click_remove(scatter.selector.xrange):
mask = (df.index >= scatter.selector.xrange[0]) & (df.index <= scatter.selector.xrange[1])
clean_df = df.drop(df.index[mask])
scatter(data = clean_df...) #update scatter plot
button1 = Button(description="Remove points", on_click = on_button_click_remove)
I checked https://plotly.com/python/custom-buttons/ but I am still not sure how to use it for my purpose.
I suggest to use Holoviews and Panel.
They are high level visualization tools that facilitate the creation and control of low level bokeh, matplotlib or plotly figures.
Here are an example:
import panel as pn
import holoviews as hv
import pandas as pd
from bokeh.models import ColumnDataSource
# This example use bokeh as backend.
# You can try plotly or matplotlib with minor modification on the codes below.
# For example you can use on_selection callback from Plotly
# https://plotly.com/python/v3/selection-events/
hv.extension('bokeh')
display( pn.extension( ) ) # activate panel
df=pd.read_csv('spiked_data.csv',index_col=0).reset_index()
pt = hv.Points(
data=df, kdims=['index', 'A' ]
).options( marker='x', size=2,
tools=['hover', 'box_select', 'lasso_select', 'reset'],
height=250, width=600
)
fig = hv.render(pt)
source = fig.select({'type':ColumnDataSource})
bt = pn.widgets.Button(name='remove selected')
def rm_sel(evt):
i = df.iloc[source.selected.indices].index # get index to delete
df.drop(i, inplace=True, errors='ignore') # modify dataframe
source.data = df # update data source
source.selected.indices=[] # clear selection
pn.io.push_notebook(app) # update figure
bt.on_click(rm_sel)
app=pn.Column(fig,'Click to delete the selected points', bt)
display(app)
A related example can be found in this SO answer

How to make the mark_rule in Altair change based on user input?

I would like to make the mark_rule (significance level) to be adjustable. I have tried to do it using user input code and change the value in the rule from 0.05 to 'user input' but the chart turned out weird.
There are two things that I would like to ask for help with:
Make the mark_rule change through user input (top priority)
Make the color of the bars (factors) below the mark_rule change (optional)
I have tried many codes in this, by far, I can only make the mark_rule move using mouseover but it is not exactly what I want to do.
Any help would be very much appreciated.
import pandas as pd
import altair as alt
Sheet2 = 'P-value'
df = pd.read_excel('Life Expectancy Data- Clean.xlsx', sheet_name=Sheet2)
highlight = alt.selection(type='single', on='mouseover',
fields=['Factor'], nearest=True, empty="none")
bar = alt.Chart(df).mark_bar(strokeWidth=5, stroke="steelblue", strokeOpacity=0.1).encode(
x = alt.X('Factor:O', sort='y'),
y = alt.Y('P-value:Q'),
tooltip = [alt.Tooltip('Factor:O'),alt.Tooltip('P-value:Q',format='.4f')],
color= alt.condition(
highlight,
alt.value("orange"),
alt.value("steelblue"))
).add_selection(
highlight
)
rule = alt.Chart(pd.DataFrame({'y': [0.05]})).mark_rule(color='red').encode(y='y')
alt.layer(
bar, rule
).properties(
title='Factors that Contribute to Life Expectancy in Malaysia',
width=500, height=300
)
Current graph
Building upon the example in the Altair docs, you could do something like this, which gives you a slider that controls the position of the rule and highlights the bars in different colors depending on if they are above or below the slider value:
import altair as alt
import pandas as pd
import numpy as np
rand = np.random.RandomState(42)
df = pd.DataFrame({
'xval': range(10),
'yval': rand.randn(10).cumsum()
})
slider = alt.binding_range(min=0, max=5, step=0.5, name='cutoff:')
selector = alt.selection_single(name="SelectorName", bind=slider, init={'cutoff': 2.5})
rule = alt.Chart().mark_rule().transform_calculate(
rule='SelectorName.cutoff'
).encode(
# Take the mean to avoid creating multiple lines on top of eachother
y='mean(rule):Q',
)
bars = alt.Chart(df).mark_bar().encode(
x='xval:O',
y='yval',
color=alt.condition(
alt.datum.yval < selector.cutoff,
alt.value('coral'), alt.value('steelblue')
)
).add_selection(
selector
)
bars + rule

Add index as dropdown menu in plotly

I am plotting chart using below code:
fig = px.line(df, x='Time', y=['one','two'], color= df.index)
fig['layout']['xaxis']['autorange'] = "reversed"
fig.update_layout(legend_title="Price")
fig.show()
Dataframe i am working with like is below:
Time one two
100 9:30 129 243
110 10:30 234 453
120 11:00 155 234
Want to add dropdown menu to select from index and show one row at a time in chart.
example if i select 110 from drop down it should only show chart for that row.
Is there any easy fix for it.
Thank you in adavance.
Here's my solution:
In order to set the proper options for the dropdown menu, it would be helpful to have a function that creates the list of options (shown below)
# Create proper buttons list
def makeButtonsList(idxs):
buttons = []
for i, idx in enumerate(idxs):
visibleArr = np.full((2*df.index.shape[0],),
False, dtype=bool) # 2x number of booleans since one/two vals are separate plots
visibleArr[2*i] = True # Set two booleans next to each other (representing one & two) to true
visibleArr[(2*i)+1] = True
buttons.append(dict(label=str(idx),
method='update',
args=[{'visible': list(visibleArr)}])) # 'Visible' arg determines which plots are shown
# depending on which dropdown is selected
return buttons
Next create the traces for the data (with your sample data, I created a bar chart but you could easily modify this)
traces = []
for i in range(df.Time.shape[0]):
rowData = df.iloc[i, :]
time = rowData.Time
one = rowData.one
two = rowData.two
traces.append(go.Bar(x=[time], y=[one], name='One'))
traces.append(go.Bar(x=[time], y=[two], name='Two'))
where df is the dataframe you are working with.
Finally put it all together and create the Plotly plot!
# Import packages
import pandas as pd
import numpy as np
import plotly.graph_objs as go
import plotly.express as px
# Create proper buttons list
def makeButtonsList(idxs):
buttons = []
for i, idx in enumerate(idxs):
visibleArr = np.full((2*df.index.shape[0],),
False, dtype=bool) # 2x number of booleans since one/two vals are separate plots
visibleArr[2*i] = True # Set two booleans next to each other (representing one & two) to true
visibleArr[(2*i)+1] = True
buttons.append(dict(label=str(idx),
method='update',
args=[{'visible': list(visibleArr)}])) # 'Visible' arg determines which plots are shown
# depending on which dropdown is selected
return buttons
# Create traces
traces = []
for i in range(df.Time.shape[0]):
rowData = df.iloc[i, :]
time = rowData.Time
one = rowData.one
two = rowData.two
traces.append(go.Bar(x=[time], y=[one], name='One'))
traces.append(go.Bar(x=[time], y=[two], name='Two'))
# Create figure
fig = go.Figure(data=traces)
# Add dropdown options
fig.update_layout(
updatemenus=[
dict(
buttons=makeButtonsList(df.index),
direction="down",
pad={"r": 10, "t": 10},
showactive=True,
x=0.55,
xanchor="left",
y=1.2,
yanchor="top"
),
]
)
# Add annotation for index selected
fig.update_layout(
annotations=[
dict(text="Index:", showarrow=False,
x=0, y=1.15, yref="paper", align="left")
],
xaxis_title = 'Time',
yaxis_title = 'Value',
)
# Show the plot
fig.show()
Here is a sample plot:
BONUS:
If you think this method is tedious, and a slider bar would do the job just fine, Plotly supports animation of bar charts. Here is the following code you could use:
fig = px.bar(df, x='Time', y=['one','two'], animation_frame=df.index)
fig.update_layout(title='Data', barmode='group')
fig.show()
Here is the resulting plot:

Is there a better way to use Jupyter IntSlider with Python Plotly?

In the following code block I use a Jupyter IntSlider to adjust the number of dots visualized in a Plotly express scatter 3d plot. The example already fits my use case, but I noticed that Plotly has built-in slider functionalities that could improve the performance.
As a Plotly beginner I find it quite hard to map the slider example from Plotly to my use case.
Any suggestions?
import numpy as np
import plotly.express as px
import pandas as pd
from ipywidgets import interact, widgets
NUM_DOTS = 100
NUM_DIMS = 3
random_data = pd.DataFrame(np.random.random((NUM_DOTS,NUM_DIMS) ), columns=['x_1','x_2','x_3'])
def update_plotly(x):
fig = px.scatter_3d(random_data[:x], x='x_1', y='x_2', z='x_3')
fig.show()
interact(update_plotly, x=widgets.IntSlider(min=1, max=NUM_DOTS, step=1, value=NUM_DOTS))
Actually it's not that hard to build the slider, just follow the path of the example shown by plotly:
import plotly.graph_objects as go
import numpy as np
NUM_DOTS = 100
NUM_DIMS = 3
# Create figure
fig = go.Figure()
# Add traces, one for each slider step
for step in np.arange(1, NUM_DOTS, 1):
#Random data
random_data = pd.DataFrame(np.random.random((step, NUM_DIMS)), columns=['x_1','x_2','x_3'])
fig.add_trace(
go.Scatter3d(
visible=False,
line=dict(color="#00CED1", width=6),
name="𝜈 = " + str(step),
z=random_data['x_3'],
x=random_data['x_1'],
y=random_data['x_2']))
# Make 10th trace visible
fig.data[10].visible = True
# Create and add slider
steps = []
for i in range(len(fig.data)):
step = dict(
method="restyle",
args=["visible", [False] * len(fig.data)],
)
step["args"][1][i] = True # Toggle i'th trace to "visible"
steps.append(step)
sliders = [dict(
active=10,
currentvalue={"prefix": "Frequency: "},
pad={"t": 50},
steps=steps
)]
fig.update_layout(
sliders=sliders
)
fig.show()
resulting:
or with more points:
As you correctly figured out, it is way more performant than the widget slider, because with this method, you just toggle the trace visibility in the 3D Scatter chart.

Categories

Resources