Can a condition be based on both a selection and a predicate? - altair

Is it possible to combine a selection and a predicate in a condition? I would like to color points on a scatterplot only if the group is selected and above a certain value.
import altair as alt
from vega_datasets import data
source = data.cars()
selection = alt.selection_multi(fields=['Origin'])
color = alt.condition(
selection & (alt.datum.Miles_per_Gallon > 18),
alt.Color('Origin:N'),
alt.value('lightgray')
)
alt.Chart(source).mark_circle().encode(
x='Horsepower',
y='Miles_per_Gallon',
color=color,
tooltip=['Name', 'Origin', 'Horsepower', 'Miles_per_Gallon']
).add_selection(
selection
)
Trying to compound the selection and the predicate raises:
Javascript Error: Cannot find a selection named "(datum.Miles_per_Gallon > 18)".
The code works with either just the selection or the condition, but not both. The only solution I can think of is layering a scatterplot on top with all the data points below the threshold colored gray. Appreciate any help, thanks!

It looks like the & operator does not work properly between a selection and an expression (tracked by this issue in the Altair repository). You can work around this by using the underlying schema object instead:
color = alt.condition(
alt.LogicalAndPredicate(**{'and': [selection, '(datum.Miles_per_Gallon > 18)']}),
alt.Color('Origin:N'),
alt.value('lightgray')
)
The resulting chart looks like this when the selection is empty:

Related

Is it possible to display one faceted chart in Altair at a time and toggle between different charts?

I'm working on a project for a class where I've been creating faceted scatterplots in Altair where each chart is a different type of food. I was wondering if it would be possible to still use these faceted charts but only display one at a time and give the user the ability to toggle between graphs instead of having each graph displayed one after the other?
Here is a rough diagram of what I'm trying to do and here's the code I have now (and what the output looks like at the moment):
import altair as alt
from vega_datasets import data
hover = alt.selection_single(on='mouseover', nearest=True, empty='none')
base = alt.Chart("Food Nutrition Info Compiled.csv").encode(
x='Energ_Kcal:N',
y='Water_(g):Q',
color=alt.condition(hover, 'Type:N', alt.value('lightgray'))
).properties(
width=180,
height=180,
)
points = base.mark_point().add_selection(
hover
)
text = base.mark_text(dy=-5).encode(
text = 'Shrt_Desc:N',
opacity = alt.condition(hover, alt.value(1), alt.value(0))
)
alt.layer(points, text).facet(
'Type:N',
)
Thanks and I hope this makes sense!
Displaying one faceted chart at a time sounds the same as showing a single subset of the data at any one point. This is currently possible by creating a selection that is bound to e.g. a dropdown and the using that with transform_filter:
import altair as alt
from vega_datasets import data
source = data.cars()
dropdown_options = source['Origin'].unique().tolist()
dropdown = alt.binding_select(
options=dropdown_options,
name='Origin '
)
selection = alt.selection_multi(
fields=['Origin'],
value=[{'Origin': dropdown_options[0]}],
bind=dropdown
)
alt.Chart(source).mark_circle().encode(
x=alt.X('Weight_in_lbs:Q', title=''),
y='Horsepower',
color='Origin',
).add_selection(
selection
).transform_filter(
selection
)

How to sort values based on selection in an Altair chart?

Given an interactive area chart like this:
import altair as alt
from vega_datasets import data
source = data.iowa_electricity()
selection = alt.selection(type='multi', fields=['source'], bind='legend')
alt.Chart(source).mark_area().encode(
x="year:T",
y="net_generation:Q",
color="source:N",
opacity=alt.condition(selection, alt.value(1), alt.value(0.1))
).add_selection(selection)
I would like to sort the selected values first so they stack up from the bottom and don't "hang in thin air" like in the example below:
However, I can't see how I would express this in a transformation. The only thing that works is transform_filter(selection) but that completely removes the values that are not selected.
Is this not possible or am I missing something?
One way you can do this is to access the contents of the selection within a calculate transform, using a vega expression to find whether the current column is in the selection. At this point, you can set the order to this encoding:
import altair as alt
from vega_datasets import data
source = data.iowa_electricity()
selection = alt.selection(type='multi', fields=['source'], bind='legend')
alt.Chart(source).add_selection(
selection
).transform_calculate(
order=f"indexof({selection.name}.source || [], datum.source)",
).mark_area().encode(
x="year:T",
y="net_generation:Q",
color="source:N",
opacity=alt.condition(selection, alt.value(1), alt.value(0.1)),
order=alt.Order("order:N", sort='descending'),
).add_selection(selection)

Is there a way to show tooltips in ALL the sub charts of a faceted chart or concatenated charts, where sub charts are "joined" by selection?

Just like we can highlight point(s) in one of the charts of a concatenated chart or a faceted chart and the corresponding point(s) will also get highlighted in the other chart, I was wondering if the same can be done with a tooltip.
I have been able to come up with a demo using mark_text as you can see below. But the biggest challenge is not being able to show multiple encodings as text. Tooltips make that really easy by just mentioning all the encodings in a list. So I was thinking if there is a way to do that in Altair.
Or is this not the expected behavior of Tooltips, and they are ONLY supposed to highlight the point over which the mouse is hovering even if other charts may have a common selection?
Whatever I have tried with Tooltips only works in a single chart, the other point despite being highlighted does not also show a tooltip.
Code:
import altair as alt
from vega_datasets import data
source = data.cars()
highlight = alt.selection(type='single', on="mouseover", empty='none')
base = alt.Chart(source).encode(
y='Miles_per_Gallon',
color=alt.condition(highlight, 'Origin', alt.ColorValue('gray')),
)
mpg = base.mark_point().encode(
x='Horsepower',
size=alt.condition(highlight, alt.value(150), alt.value(50))).add_selection(
highlight
)
acc = base.mark_point().encode(
x='Acceleration',
size=alt.condition(highlight, alt.value(150), alt.value(50))).add_selection(
highlight
)
text_mpg = base.mark_text(dx=5, dy=-10, size=15).encode(
x='Horsepower',
y='Miles_per_Gallon',
text=alt.condition(highlight, 'Horsepower:Q', alt.value('')),
)
text_acc = base.mark_text(dx=5, dy=-10, size=15).encode(
x='Acceleration',
y='Miles_per_Gallon',
text=alt.condition(highlight, 'Acceleration:Q', alt.value('')),
)
(mpg+text_mpg)|(acc+text_acc)
Demo:
Expected Output:
Show tooltips instead of text, with multiple encodings
I am however starting to feel that this is not the intended behavior of tooltip.

Altair color bar chart by value not presented

Trying to color a bar chart using a condition based on a value that is not presented in the chart.
I got this dataframe:
I would like to color the bar green if row.presented_value > row.coloring_value , else color red.
I saw examples of conditions by constant values and by displayed values, but couldn't make it work for me.
In the code example below I would like both foo and bar to be red.
import pandas as pd
df = pd.DataFrame({'name':['bar','foo'],
'presented_value':[10,20],
'coloring_value':[15,25]})
(alt.Chart(df, height=250, width=375).mark_bar()
.encode(x='name', y=alt.Y('presented_value', axis=alt.Axis(orient='right')),
color=alt.condition(alt.datum['presented_value'] > df.loc[df.name==alt.datum.x,
'coloring_value'].values[0],
alt.value('lightgreen'),alt.value('darkred'))
)
)
Changing the first value of coloring_value to <10 both bars will be green even though I would expect only bar to be green.
df = pd.DataFrame({'name':['bar','foo'],
'presented_value':[10,20],
'coloring_value':[5,25]})
(alt.Chart(df, height=250, width=375).mark_bar()
.encode(x='name', y=alt.Y('presented_value', axis=alt.Axis(orient='right')),
color=alt.condition(alt.datum['presented_value'] > df.loc[df.name==alt.datum.x,
'coloring_value'].values[0],
alt.value('lightgreen'),alt.value('darkred'))))
Still not coloring by the correct values. Any idea on how to get it done?
Thanks in advance!
Condition expressions cannot use pandas constructs; they must map to vega expressions. Altair provides the alt.datum and alt.expr object as convenience wrappers for this.
In your case, when you want to compare two values in the row, the best way to do that is to compare them directly:
(alt.Chart(df, height=250, width=375).mark_bar()
.encode(
x='name',
y=alt.Y('presented_value', axis=alt.Axis(orient='right')),
color=alt.condition(
alt.datum.presented_value > alt.datum.coloring_value,
alt.value('lightgreen'),
alt.value('darkred')
)
)
)

Conditionally Color Table Cells with ReportLab

I've created a table using ReportLab. I would like to conditionally color the cells, depending on their contents (in my case, I want negative numbers to be red). To be clear, I have the conditional code working, I can't figure out how to add color. What I've tried:
using the <font color="..."> tag. Instead, the tags is included verbatim in the output.
wrapping each cell in Paragraph(...) (suggested in this answer). In this case, the cell text is linewrapped after each letter.
wrapping the table in Paragraph(...). In this case, reportlab errors out (I believe the resulting error was TypeError: split() missing required positional argument: 'availHeight')
I found reportlab.platypus.tables.CellStyle in the reportlab source code, but can't figure out make use of it. Google turns up nothing useful and it's not mentioned in the reportlab documentation.
I guess TableStyle(...) rules could be used, but the cells aren't in a predetermined position within the table (which is what all the examples assume).
Help appreciated!
Using TableStyle() would be an acceptable solution. You could loop through the data and add a style command when the condition is met.
Here is an example:
import random
from reportlab.lib.pagesizes import letter
from reportlab.lib.colors import red
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle
# Generate random data with positive and negative values as list of lists.
data = []
for _ in range(20):
data.append(random.sample(range(-10, 10), 5))
table_style = TableStyle([('ALIGN', (0, 0), (-1, -1), 'RIGHT')])
# Loop through list of lists creating styles for cells with negative value.
for row, values, in enumerate(data):
for column, value in enumerate(values):
if value < 0:
table_style.add('TEXTCOLOR', (column, row), (column, row), red)
table = Table(data)
table.setStyle(table_style)
pdf = SimpleDocTemplate('example.pdf', pagesize=letter)
pdf.build([table])

Resources