Skip to content

Commit 556236f

Browse files
authored
Filling support for quak; add quak/mosaic to examples (#155)
1 parent af3f310 commit 556236f

File tree

6 files changed

+152
-31
lines changed

6 files changed

+152
-31
lines changed

examples/outputs/app.py

+65-19
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
from pathlib import Path
2+
13
import numpy as np
24
from shiny import *
35

46
from shinywidgets import *
57

8+
app_dir = Path(__file__).parent
9+
610
app_ui = ui.page_sidebar(
711
ui.sidebar(
812
ui.input_radio_buttons(
@@ -13,6 +17,8 @@
1317
"plotly",
1418
"ipyleaflet",
1519
"pydeck",
20+
"quak",
21+
"mosaic",
1622
"ipysigma",
1723
"bokeh",
1824
"bqplot",
@@ -48,22 +54,26 @@ def _():
4854

4955
source = data.stocks()
5056

51-
return alt.Chart(source).transform_filter(
52-
'datum.symbol==="GOOG"'
53-
).mark_area(
54-
tooltip=True,
55-
line={'color': '#0281CD'},
56-
color=alt.Gradient(
57-
gradient='linear',
58-
stops=[alt.GradientStop(color='white', offset=0),
59-
alt.GradientStop(color='#0281CD', offset=1)],
60-
x1=1, x2=1, y1=1, y2=0
57+
return (
58+
alt.Chart(source)
59+
.transform_filter('datum.symbol==="GOOG"')
60+
.mark_area(
61+
tooltip=True,
62+
line={"color": "#0281CD"},
63+
color=alt.Gradient(
64+
gradient="linear",
65+
stops=[
66+
alt.GradientStop(color="white", offset=0),
67+
alt.GradientStop(color="#0281CD", offset=1),
68+
],
69+
x1=1,
70+
x2=1,
71+
y1=1,
72+
y2=0,
73+
),
6174
)
62-
).encode(
63-
alt.X('date:T'),
64-
alt.Y('price:Q')
65-
).properties(
66-
title={"text": ["Google's stock price over time"]}
75+
.encode(alt.X("date:T"), alt.Y("price:Q"))
76+
.properties(title={"text": ["Google's stock price over time"]})
6777
)
6878

6979
@output(id="plotly")
@@ -73,8 +83,10 @@ def _():
7383

7484
return px.density_heatmap(
7585
px.data.tips(),
76-
x="total_bill", y="tip",
77-
marginal_x="histogram", marginal_y="histogram"
86+
x="total_bill",
87+
y="tip",
88+
marginal_x="histogram",
89+
marginal_y="histogram",
7890
)
7991

8092
@output(id="ipyleaflet")
@@ -119,14 +131,48 @@ def _():
119131
# Combined all of it and render a viewport
120132
return pdk.Deck(layers=[layer], initial_view_state=view_state)
121133

134+
@output(id="quak")
135+
@render_widget
136+
def _():
137+
import polars as pl
138+
import quak
139+
140+
df = pl.read_parquet(
141+
"https://github.com/uwdata/mosaic/raw/main/data/athletes.parquet"
142+
)
143+
return quak.Widget(df)
144+
145+
@output(id="mosaic")
146+
@render_widget
147+
def _():
148+
import polars as pl
149+
import yaml
150+
from mosaic_widget import MosaicWidget
151+
152+
flights = pl.read_parquet(
153+
"https://github.com/uwdata/mosaic/raw/main/data/flights-200k.parquet"
154+
)
155+
156+
# Load weather spec, remove data key to ensure load from Pandas
157+
with open(app_dir / "flights.yaml") as f:
158+
spec = yaml.safe_load(f)
159+
_ = spec.pop("data")
160+
161+
return MosaicWidget(spec, data={"flights": flights})
162+
122163
@output(id="ipysigma")
123164
@render_widget
124165
def _():
125166
import igraph as ig
126167
from ipysigma import Sigma
127-
g = ig.Graph.Famous('Zachary')
128-
return Sigma(g, node_size=g.degree, node_color=g.betweenness(), node_color_gradient='Viridis')
129168

169+
g = ig.Graph.Famous("Zachary")
170+
return Sigma(
171+
g,
172+
node_size=g.degree,
173+
node_color=g.betweenness(),
174+
node_color_gradient="Viridis",
175+
)
130176

131177
@output(id="bokeh")
132178
@render_widget

examples/outputs/flights.yaml

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
meta:
2+
title: Cross-Filter Flights (200k)
3+
description: >
4+
Histograms showing arrival delay, departure time, and distance flown for over 200,000 flights.
5+
Select a histogram region to cross-filter the charts.
6+
Each plot uses an `intervalX` interactor to populate a shared Selection
7+
with `crossfilter` resolution.
8+
data:
9+
flights: { file: data/flights-200k.parquet }
10+
params:
11+
brush: { select: crossfilter }
12+
vconcat:
13+
- plot:
14+
- mark: rectY
15+
data: { from: flights, filterBy: $brush }
16+
x: { bin: delay }
17+
y: { count: }
18+
fill: steelblue
19+
inset: 0.5
20+
- select: intervalX
21+
as: $brush
22+
xDomain: Fixed
23+
yTickFormat: s
24+
width: 1200
25+
height: 250
26+
- plot:
27+
- mark: rectY
28+
data: { from: flights, filterBy: $brush }
29+
x: { bin: time }
30+
y: { count: }
31+
fill: steelblue
32+
inset: 0.5
33+
- select: intervalX
34+
as: $brush
35+
xDomain: Fixed
36+
yTickFormat: s
37+
width: 1200
38+
height: 250
39+
- plot:
40+
- mark: rectY
41+
data: { from: flights, filterBy: $brush }
42+
x: { bin: distance }
43+
y: { count: }
44+
fill: steelblue
45+
inset: 0.5
46+
- select: intervalX
47+
as: $brush
48+
xDomain: Fixed
49+
yTickFormat: s
50+
width: 1200
51+
height: 250

examples/outputs/requirements.txt

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ shinywidgets
33
ipywidgets
44
numpy
55
pandas
6-
qgrid
76
vega_datasets
87
bokeh
98
jupyter_bokeh
109
ipyleaflet
11-
pydeck
10+
pydeck==0.8.0
1211
altair
1312
plotly
1413
bqplot
1514
ipychart
1615
ipywebrtc
1716
vega
17+
quak
18+
mosaic-widget
19+
polars

js/src/output.ts

+21-5
Original file line numberDiff line numberDiff line change
@@ -110,24 +110,40 @@ class IPyWidgetOutput extends Shiny.OutputBinding {
110110
// The ipywidgets container (.lmWidget)
111111
const lmWidget = el.children[0] as HTMLElement;
112112

113-
this._maybeResize(lmWidget);
113+
if (fill) {
114+
this._onImplementation(lmWidget, () => this._doAddFillClasses(lmWidget));
115+
}
116+
this._onImplementation(lmWidget, this._doResize);
114117
}
115-
_maybeResize(lmWidget: HTMLElement): void {
118+
_onImplementation(lmWidget: HTMLElement, callback: () => void): void {
116119
if (this._hasImplementation(lmWidget)) {
117-
return this._doResize();
120+
callback();
121+
return;
118122
}
119123

120124
// Some widget implementation (e.g., ipyleaflet, pydeck) won't actually
121125
// have rendered to the DOM at this point, so wait until they do
122126
const mo = new MutationObserver((mutations) => {
123127
if (this._hasImplementation(lmWidget)) {
124128
mo.disconnect();
125-
this._doResize();
129+
callback();
126130
}
127131
});
128132

129133
mo.observe(lmWidget, {childList: true});
130134
}
135+
// In most cases, we can get widgets to fill through Python/CSS, but some widgets
136+
// (e.g., quak) don't have a Python API and use shadow DOM, which can only access
137+
// from JS
138+
_doAddFillClasses(lmWidget: HTMLElement): void {
139+
const impl = lmWidget.children[0];
140+
const isQuakWidget = impl && !!impl.shadowRoot?.querySelector(".quak");
141+
if (isQuakWidget) {
142+
impl.classList.add("html-fill-container", "html-fill-item");
143+
const quakWidget = impl.shadowRoot.querySelector(".quak") as HTMLElement;
144+
quakWidget.style.maxHeight = "unset";
145+
}
146+
}
131147
_doResize(): void {
132148
// Trigger resize event to force layout (setTimeout() is needed for altair)
133149
// TODO: debounce this call?
@@ -137,7 +153,7 @@ class IPyWidgetOutput extends Shiny.OutputBinding {
137153
}
138154
_hasImplementation(lmWidget: HTMLElement): boolean {
139155
const impl = lmWidget.children[0];
140-
return impl && impl.children.length > 0;
156+
return impl && (impl.children.length > 0 || impl.shadowRoot?.children.length > 0);
141157
}
142158
}
143159

0 commit comments

Comments
 (0)