Skip to content

Commit

Permalink
Filling support for quak; add quak/mosaic to examples (#155)
Browse files Browse the repository at this point in the history
  • Loading branch information
cpsievert authored Jul 31, 2024
1 parent af3f310 commit 556236f
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 31 deletions.
84 changes: 65 additions & 19 deletions examples/outputs/app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from pathlib import Path

import numpy as np
from shiny import *

from shinywidgets import *

app_dir = Path(__file__).parent

app_ui = ui.page_sidebar(
ui.sidebar(
ui.input_radio_buttons(
Expand All @@ -13,6 +17,8 @@
"plotly",
"ipyleaflet",
"pydeck",
"quak",
"mosaic",
"ipysigma",
"bokeh",
"bqplot",
Expand Down Expand Up @@ -48,22 +54,26 @@ def _():

source = data.stocks()

return alt.Chart(source).transform_filter(
'datum.symbol==="GOOG"'
).mark_area(
tooltip=True,
line={'color': '#0281CD'},
color=alt.Gradient(
gradient='linear',
stops=[alt.GradientStop(color='white', offset=0),
alt.GradientStop(color='#0281CD', offset=1)],
x1=1, x2=1, y1=1, y2=0
return (
alt.Chart(source)
.transform_filter('datum.symbol==="GOOG"')
.mark_area(
tooltip=True,
line={"color": "#0281CD"},
color=alt.Gradient(
gradient="linear",
stops=[
alt.GradientStop(color="white", offset=0),
alt.GradientStop(color="#0281CD", offset=1),
],
x1=1,
x2=1,
y1=1,
y2=0,
),
)
).encode(
alt.X('date:T'),
alt.Y('price:Q')
).properties(
title={"text": ["Google's stock price over time"]}
.encode(alt.X("date:T"), alt.Y("price:Q"))
.properties(title={"text": ["Google's stock price over time"]})
)

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

return px.density_heatmap(
px.data.tips(),
x="total_bill", y="tip",
marginal_x="histogram", marginal_y="histogram"
x="total_bill",
y="tip",
marginal_x="histogram",
marginal_y="histogram",
)

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

@output(id="quak")
@render_widget
def _():
import polars as pl
import quak

df = pl.read_parquet(
"https://github.com/uwdata/mosaic/raw/main/data/athletes.parquet"
)
return quak.Widget(df)

@output(id="mosaic")
@render_widget
def _():
import polars as pl
import yaml
from mosaic_widget import MosaicWidget

flights = pl.read_parquet(
"https://github.com/uwdata/mosaic/raw/main/data/flights-200k.parquet"
)

# Load weather spec, remove data key to ensure load from Pandas
with open(app_dir / "flights.yaml") as f:
spec = yaml.safe_load(f)
_ = spec.pop("data")

return MosaicWidget(spec, data={"flights": flights})

@output(id="ipysigma")
@render_widget
def _():
import igraph as ig
from ipysigma import Sigma
g = ig.Graph.Famous('Zachary')
return Sigma(g, node_size=g.degree, node_color=g.betweenness(), node_color_gradient='Viridis')

g = ig.Graph.Famous("Zachary")
return Sigma(
g,
node_size=g.degree,
node_color=g.betweenness(),
node_color_gradient="Viridis",
)

@output(id="bokeh")
@render_widget
Expand Down
51 changes: 51 additions & 0 deletions examples/outputs/flights.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
meta:
title: Cross-Filter Flights (200k)
description: >
Histograms showing arrival delay, departure time, and distance flown for over 200,000 flights.
Select a histogram region to cross-filter the charts.
Each plot uses an `intervalX` interactor to populate a shared Selection
with `crossfilter` resolution.
data:
flights: { file: data/flights-200k.parquet }
params:
brush: { select: crossfilter }
vconcat:
- plot:
- mark: rectY
data: { from: flights, filterBy: $brush }
x: { bin: delay }
y: { count: }
fill: steelblue
inset: 0.5
- select: intervalX
as: $brush
xDomain: Fixed
yTickFormat: s
width: 1200
height: 250
- plot:
- mark: rectY
data: { from: flights, filterBy: $brush }
x: { bin: time }
y: { count: }
fill: steelblue
inset: 0.5
- select: intervalX
as: $brush
xDomain: Fixed
yTickFormat: s
width: 1200
height: 250
- plot:
- mark: rectY
data: { from: flights, filterBy: $brush }
x: { bin: distance }
y: { count: }
fill: steelblue
inset: 0.5
- select: intervalX
as: $brush
xDomain: Fixed
yTickFormat: s
width: 1200
height: 250
6 changes: 4 additions & 2 deletions examples/outputs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ shinywidgets
ipywidgets
numpy
pandas
qgrid
vega_datasets
bokeh
jupyter_bokeh
ipyleaflet
pydeck
pydeck==0.8.0
altair
plotly
bqplot
ipychart
ipywebrtc
vega
quak
mosaic-widget
polars
26 changes: 21 additions & 5 deletions js/src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,24 +110,40 @@ class IPyWidgetOutput extends Shiny.OutputBinding {
// The ipywidgets container (.lmWidget)
const lmWidget = el.children[0] as HTMLElement;

this._maybeResize(lmWidget);
if (fill) {
this._onImplementation(lmWidget, () => this._doAddFillClasses(lmWidget));
}
this._onImplementation(lmWidget, this._doResize);
}
_maybeResize(lmWidget: HTMLElement): void {
_onImplementation(lmWidget: HTMLElement, callback: () => void): void {
if (this._hasImplementation(lmWidget)) {
return this._doResize();
callback();
return;
}

// Some widget implementation (e.g., ipyleaflet, pydeck) won't actually
// have rendered to the DOM at this point, so wait until they do
const mo = new MutationObserver((mutations) => {
if (this._hasImplementation(lmWidget)) {
mo.disconnect();
this._doResize();
callback();
}
});

mo.observe(lmWidget, {childList: true});
}
// In most cases, we can get widgets to fill through Python/CSS, but some widgets
// (e.g., quak) don't have a Python API and use shadow DOM, which can only access
// from JS
_doAddFillClasses(lmWidget: HTMLElement): void {

This comment has been minimized.

Copy link
@manzt

manzt Aug 2, 2024

Contributor

Perhaps this could be a discussion point for external APIs for controlling anywidgets.

This comment has been minimized.

Copy link
@cpsievert

cpsievert Aug 2, 2024

Author Collaborator

Yes, I would love that! There is definitely a lot of hacking taking place to get widgets to follow our approach to filling layout; which in brief, means that if a widget doesn't have a specified height and is in a "fillable" container (which has display:flex; flex-direction: column), then it receives flex: 1 1 auto. Here's an example:

import polars as pl
import quak
import shiny.express
from shinywidgets import render_widget

ui.page_opts(fillable=True)

df = pl.read_parquet("https://github.com/uwdata/mosaic/raw/main/data/athletes.parquet")

@render_widget
def df_widget():
    return quak.Widget(df)

This comment has been minimized.

Copy link
@manzt

manzt Aug 2, 2024

Contributor

I bet we could learn a lot from Shiny's needs (and other dashboards) to introduce anywidget APIs for consumers.

const impl = lmWidget.children[0];
const isQuakWidget = impl && !!impl.shadowRoot?.querySelector(".quak");
if (isQuakWidget) {
impl.classList.add("html-fill-container", "html-fill-item");
const quakWidget = impl.shadowRoot.querySelector(".quak") as HTMLElement;
quakWidget.style.maxHeight = "unset";
}
}
_doResize(): void {
// Trigger resize event to force layout (setTimeout() is needed for altair)
// TODO: debounce this call?
Expand All @@ -137,7 +153,7 @@ class IPyWidgetOutput extends Shiny.OutputBinding {
}
_hasImplementation(lmWidget: HTMLElement): boolean {
const impl = lmWidget.children[0];
return impl && impl.children.length > 0;
return impl && (impl.children.length > 0 || impl.shadowRoot?.children.length > 0);
}
}

Expand Down
Loading

0 comments on commit 556236f

Please sign in to comment.