vega / vegafusion

Serverside scaling for Vega and Altair visualizations
https://vegafusion.io
BSD 3-Clause "New" or "Revised" License
317 stars 17 forks source link

Vega spec. converted from vega-lite spec which includes selection_point with selection_interval not working on VegaFusionWidget #495

Open cosmicfarmers opened 3 months ago

cosmicfarmers commented 3 months ago

Hello,

I can't use combination of selection_point and selection_interval on VegaFusionWidget ONLY WHEN I convert vega-lite spec. to vega spec and input the vega spec to VegaFusionWidget.

Please refer to the below. (jupyter notebook cells converted to markdown. ipynb file is not uploadable.)

import altair as alt
import vegafusion as vf
import vl_convert as vlc
import pandas as pd
alt.__version__, vf.__version__, vlc.__version__, pd.__version__
('5.3.0', '1.6.9', '1.5.0', '2.1.2')
buttons_pdf = pd.DataFrame({'greeting':['hello','world']})

values_pdf = pd.DataFrame({'vals':[1,2,3,4,5]})

buttons_picker = alt.selection_point(fields=['greeting'], name='buttons_picker')
buttons_ap = (
    alt.Chart(buttons_pdf)
        .encode(
            y='greeting:N',
            opacity=alt.condition(buttons_picker, alt.value(1), alt.value(0.2))
        )
        .mark_circle(size=200, color='crimson')
        .add_params(buttons_picker)
)

values_brush = alt.selection_interval(encodings=['x'], name='values_brush')
values_ap = (
    alt.Chart(values_pdf)
        .encode(
            x='vals:O',
            opacity=alt.condition(values_brush, alt.value(1), alt.value(0.2))
        )
        .mark_tick(thickness=5, color='royalblue')
        .add_params(values_brush)
)

Working: Altair (vega-lite)

final_ap = alt.hconcat(buttons_ap, values_ap)
final_ap

altair

Working: Altair on VegaFusionWidget

vf.jupyter.VegaFusionWidget(final_ap)

altair_vf

Not Working: Vega Spec. on VegaFusionWidget

Only selection_interval is working; selection_point is not clickable.

vl_spec = final_ap.to_json()
vg_spec = vlc.vegalite_to_vega(vl_spec)

# I want to add some more vega spec to the vega spec. converted from the vega-lite spec. created from altair api.
# Here, I didn't add any vega spec yet but selection_point is already not working.

vf.jupyter.VegaFusionWidget(vg_spec, verbose=True)
# greeting button is not clickable. no relative error messages on web console and jupyterlab console.

vg_spec_vf

VegaFusionWidget(spec='{\n  "$schema": "https://vega.github.io/schema/vega/v5.json",\n  "background": "white",…
# I also tested vega spec converted from https://vega.github.io/editor/ on VegaFusionWidget, which is not working.
# Pasting the following output to https://vega.github.io/editor/ works.
import json
print(json.dumps(vg_spec))
{"$schema": "https://vega.github.io/schema/vega/v5.json", "background": "white", "padding": 5, "data": [{"name": "buttons_picker_store"}, {"name": "values_brush_store"}, {"name": "data-608367a698199bd17d43a4fe7ad087c2", "values": [{"greeting": "hello"}, {"greeting": "world"}]}, {"name": "data-48acafe826497c8a0e7547b6ac2f1594", "values": [{"vals": 1}, {"vals": 2}, {"vals": 3}, {"vals": 4}, {"vals": 5}]}], "signals": [{"name": "view_1_width", "value": 20}, {"name": "view_1_y_step", "value": 20}, {"name": "view_1_height", "update": "bandspace(domain('view_1_y').length, 1, 0.5) * view_1_y_step"}, {"name": "view_2_x_step", "value": 20}, {"name": "view_2_width", "update": "bandspace(domain('view_2_x').length, 1, 0.5) * view_2_x_step"}, {"name": "view_2_height", "value": 20}, {"name": "unit", "value": {}, "on": [{"events": "pointermove", "update": "isTuple(group()) ? group() : unit"}]}, {"name": "buttons_picker", "update": "vlSelectionResolve(\"buttons_picker_store\", \"union\", true, true)"}, {"name": "values_brush", "update": "vlSelectionResolve(\"values_brush_store\", \"union\")"}], "layout": {"padding": 20, "bounds": "full", "align": "each"}, "marks": [{"type": "group", "name": "view_1_group", "style": "cell", "encode": {"update": {"width": {"signal": "view_1_width"}, "height": {"signal": "view_1_height"}}}, "signals": [{"name": "width", "update": "view_1_width"}, {"name": "buttons_picker_tuple", "on": [{"events": [{"source": "scope", "type": "click"}], "update": "datum && item().mark.marktype !== 'group' && indexof(item().mark.role, 'legend') < 0 ? {unit: \"view_1\", fields: buttons_picker_tuple_fields, values: [(item().isVoronoi ? datum.datum : datum)[\"greeting\"]]} : null", "force": true}, {"events": [{"source": "view", "type": "dblclick"}], "update": "null"}]}, {"name": "buttons_picker_tuple_fields", "value": [{"type": "E", "field": "greeting"}]}, {"name": "buttons_picker_toggle", "value": false, "on": [{"events": [{"source": "scope", "type": "click"}], "update": "event.shiftKey"}, {"events": [{"source": "view", "type": "dblclick"}], "update": "false"}]}, {"name": "buttons_picker_modify", "on": [{"events": {"signal": "buttons_picker_tuple"}, "update": "modify(\"buttons_picker_store\", buttons_picker_toggle ? null : buttons_picker_tuple, buttons_picker_toggle ? null : true, buttons_picker_toggle ? buttons_picker_tuple : null)"}]}], "marks": [{"name": "view_1_marks", "type": "symbol", "style": ["circle"], "interactive": true, "from": {"data": "data-608367a698199bd17d43a4fe7ad087c2"}, "encode": {"update": {"opacity": [{"test": "!length(data(\"buttons_picker_store\")) || vlSelectionTest(\"buttons_picker_store\", datum)", "value": 1}, {"value": 0.2}], "size": {"value": 200}, "fill": {"value": "crimson"}, "ariaRoleDescription": {"value": "circle"}, "description": {"signal": "\"greeting: \" + (isValid(datum[\"greeting\"]) ? datum[\"greeting\"] : \"\"+datum[\"greeting\"])"}, "x": {"signal": "view_1_width", "mult": 0.5}, "y": {"scale": "view_1_y", "field": "greeting"}, "shape": {"value": "circle"}}}}], "axes": [{"scale": "view_1_y", "orient": "left", "grid": false, "title": "greeting", "zindex": 0}]}, {"type": "group", "name": "view_2_group", "style": "cell", "encode": {"update": {"width": {"signal": "view_2_width"}, "height": {"signal": "view_2_height"}}}, "signals": [{"name": "height", "update": "view_2_height"}, {"name": "values_brush_x", "value": [], "on": [{"events": {"source": "scope", "type": "pointerdown", "filter": ["!event.item || event.item.mark.name !== \"values_brush_brush\""]}, "update": "[x(unit), x(unit)]"}, {"events": {"source": "window", "type": "pointermove", "consume": true, "between": [{"source": "scope", "type": "pointerdown", "filter": ["!event.item || event.item.mark.name !== \"values_brush_brush\""]}, {"source": "window", "type": "pointerup"}]}, "update": "[values_brush_x[0], clamp(x(unit), 0, view_2_width)]"}, {"events": {"signal": "values_brush_scale_trigger"}, "update": "[0, 0]"}, {"events": [{"source": "view", "type": "dblclick"}], "update": "[0, 0]"}, {"events": {"signal": "values_brush_translate_delta"}, "update": "clampRange(panLinear(values_brush_translate_anchor.extent_x, values_brush_translate_delta.x / span(values_brush_translate_anchor.extent_x)), 0, view_2_width)"}, {"events": {"signal": "values_brush_zoom_delta"}, "update": "clampRange(zoomLinear(values_brush_x, values_brush_zoom_anchor.x, values_brush_zoom_delta), 0, view_2_width)"}]}, {"name": "values_brush_vals", "on": [{"events": {"signal": "values_brush_x"}, "update": "values_brush_x[0] === values_brush_x[1] ? null : invert(\"view_2_x\", values_brush_x)"}]}, {"name": "values_brush_scale_trigger", "value": {}, "on": [{"events": [{"scale": "view_2_x"}], "update": "(!isArray(values_brush_vals) || (invert(\"view_2_x\", values_brush_x)[0] === values_brush_vals[0] && invert(\"view_2_x\", values_brush_x)[1] === values_brush_vals[1])) ? values_brush_scale_trigger : {}"}]}, {"name": "values_brush_tuple", "on": [{"events": [{"signal": "values_brush_vals"}], "update": "values_brush_vals ? {unit: \"view_2\", fields: values_brush_tuple_fields, values: [values_brush_vals]} : null"}]}, {"name": "values_brush_tuple_fields", "value": [{"field": "vals", "channel": "x", "type": "E"}]}, {"name": "values_brush_translate_anchor", "value": {}, "on": [{"events": [{"source": "scope", "type": "pointerdown", "markname": "values_brush_brush"}], "update": "{x: x(unit), y: y(unit), extent_x: slice(values_brush_x)}"}]}, {"name": "values_brush_translate_delta", "value": {}, "on": [{"events": [{"source": "window", "type": "pointermove", "consume": true, "between": [{"source": "scope", "type": "pointerdown", "markname": "values_brush_brush"}, {"source": "window", "type": "pointerup"}]}], "update": "{x: values_brush_translate_anchor.x - x(unit), y: values_brush_translate_anchor.y - y(unit)}"}]}, {"name": "values_brush_zoom_anchor", "on": [{"events": [{"source": "scope", "type": "wheel", "consume": true, "markname": "values_brush_brush"}], "update": "{x: x(unit), y: y(unit)}"}]}, {"name": "values_brush_zoom_delta", "on": [{"events": [{"source": "scope", "type": "wheel", "consume": true, "markname": "values_brush_brush"}], "force": true, "update": "pow(1.001, event.deltaY * pow(16, event.deltaMode))"}]}, {"name": "values_brush_modify", "on": [{"events": {"signal": "values_brush_tuple"}, "update": "modify(\"values_brush_store\", values_brush_tuple, true)"}]}], "marks": [{"name": "values_brush_brush_bg", "type": "rect", "clip": true, "encode": {"enter": {"fill": {"value": "#333"}, "fillOpacity": {"value": 0.125}}, "update": {"x": [{"test": "data(\"values_brush_store\").length && data(\"values_brush_store\")[0].unit === \"view_2\"", "signal": "values_brush_x[0]"}, {"value": 0}], "y": [{"test": "data(\"values_brush_store\").length && data(\"values_brush_store\")[0].unit === \"view_2\"", "value": 0}, {"value": 0}], "x2": [{"test": "data(\"values_brush_store\").length && data(\"values_brush_store\")[0].unit === \"view_2\"", "signal": "values_brush_x[1]"}, {"value": 0}], "y2": [{"test": "data(\"values_brush_store\").length && data(\"values_brush_store\")[0].unit === \"view_2\"", "field": {"group": "height"}}, {"value": 0}]}}}, {"name": "view_2_marks", "type": "rect", "style": ["tick"], "interactive": true, "from": {"data": "data-48acafe826497c8a0e7547b6ac2f1594"}, "encode": {"update": {"opacity": [{"test": "!length(data(\"values_brush_store\")) || vlSelectionTest(\"values_brush_store\", datum)", "value": 1}, {"value": 0.2}], "fill": {"value": "royalblue"}, "ariaRoleDescription": {"value": "tick"}, "description": {"signal": "\"vals: \" + (isValid(datum[\"vals\"]) ? datum[\"vals\"] : \"\"+datum[\"vals\"])"}, "xc": {"scale": "view_2_x", "field": "vals"}, "yc": {"signal": "view_2_height", "mult": 0.5}, "height": {"value": 15}, "width": {"value": 5}}}}, {"name": "values_brush_brush", "type": "rect", "clip": true, "encode": {"enter": {"cursor": {"value": "move"}, "fill": {"value": "transparent"}}, "update": {"x": [{"test": "data(\"values_brush_store\").length && data(\"values_brush_store\")[0].unit === \"view_2\"", "signal": "values_brush_x[0]"}, {"value": 0}], "y": [{"test": "data(\"values_brush_store\").length && data(\"values_brush_store\")[0].unit === \"view_2\"", "value": 0}, {"value": 0}], "x2": [{"test": "data(\"values_brush_store\").length && data(\"values_brush_store\")[0].unit === \"view_2\"", "signal": "values_brush_x[1]"}, {"value": 0}], "y2": [{"test": "data(\"values_brush_store\").length && data(\"values_brush_store\")[0].unit === \"view_2\"", "field": {"group": "height"}}, {"value": 0}], "stroke": [{"test": "values_brush_x[0] !== values_brush_x[1]", "value": "white"}, {"value": null}]}}}], "axes": [{"scale": "view_2_x", "orient": "bottom", "grid": false, "title": "vals", "labelAlign": "right", "labelAngle": 270, "labelBaseline": "middle", "zindex": 0}]}], "scales": [{"name": "view_1_y", "type": "point", "domain": {"data": "data-608367a698199bd17d43a4fe7ad087c2", "field": "greeting", "sort": true}, "range": {"step": {"signal": "view_1_y_step"}}, "padding": 0.5}, {"name": "view_2_x", "type": "point", "domain": {"data": "data-48acafe826497c8a0e7547b6ac2f1594", "field": "vals", "sort": true}, "range": {"step": {"signal": "view_2_x_step"}}, "padding": 0.5}]}
jonmmease commented 2 months ago

Hi @cosmicfarmers, thanks for opening an issue and apologies for the delay in getting back to you (I was on holiday and at a conference the past couple of weeks).

I can reproduce what you're seeing, and will try to dig in soon.

I had been planning to deprecate VegaFusionWidget and direct folks to use Altair's JupyterChart with the "vegafusion" data transformer. This has the same performance characteristics as VegaFusionWidget and supports accessing selections and params from Python. But the JupyterChart approach only works on Altair charts, not on Vega specs. So this issue is a good reminder of that use case. I might rework VegaFusionWidget to work like JupyterChart, but focus exclusively on Vega specs.

cosmicfarmers commented 2 months ago

Hi @cosmicfarmers, thanks for opening an issue and apologies for the delay in getting back to you (I was on holiday and at a conference the past couple of weeks).

I can reproduce what you're seeing, and will try to dig in soon.

I had been planning to deprecate VegaFusionWidget and direct folks to use Altair's JupyterChart with the "vegafusion" data transformer. This has the same performance characteristics as VegaFusionWidget and supports accessing selections and params from Python. But the JupyterChart approach only works on Altair charts, not on Vega specs. So this issue is a good reminder of that use case. I might rework VegaFusionWidget to work like JupyterChart, but focus exclusively on Vega specs.

Hi Jon,

Thanks for getting back to me!

What I love about VegaFusion is that even though it doesn’t support all data transformations (like contour and kde2d), I can still use server-side support for a lot of transformations. This is super helpful when my pipeline includes both supported and unsupported operations.

I’m working on creating contoured shot frequency maps by pitch location from football games, which involves handling really large datasets. The process works smoothly because I first aggregate by location coordinates and then apply unsupported Vega transformations like kde2d and contour (which will work in web browsers if I understood correctly.). Real-time transformations are key since I offer a bunch of interactive options for viewing the contours.

Given all this, supporting Vega specs is essential for my work, even though you’re planning to deprecate VegaFusionWidget, which I currently rely on.

Employing JupyterChart will help users feel more accessible, but I really support this project supporting Vega specs as well. After all, the project name is not Vega-liteFusion. :)

By the way, currently, I am not combining selection_point and selection_interval as a workaround. This is an awesome project, and I've been using VegaFusion for almost all of my work recently.

Thanks so much!

P.S. one quick question: sometimes all of data transformations are sent to my browser. I haven't investigated deeply because now it works like separating heavy parts work in server-side fortunately. Is there a way that I can check what part will work in server-side and which part will work in client-side before rendering it?

jonmmease commented 2 months ago

Thanks for the kind words, it's great to hear that VegaFusion has been helpful for you!

Given all this, supporting Vega specs is essential for my work, even though you’re planning to deprecate VegaFusionWidget, which I currently rely on.

We'll make sure that there continues to be a widget solution for Vega specs. At this point I'm picturing a small rewrite of VegaFusionWidget that uses AnyWidget (so it's much easier to maintain), and removes support for Vega-Lite and Vega-Altair charts. (All of the dedicated Altair support in VegaFusion has been upstreamed to Altair itself, and Vega-Lite to Vega conversions are easy with vl-convert, so I'd like to remove the Vega-Lite and Altair dependencies from VegaFusion). We could also make it possible to access and set signals and datasets from Python.

What I love about VegaFusion is that even though it doesn’t support all data transformations (like contour and kde2d), I can still use server-side support for a lot of transformations. This is super helpful when my pipeline includes both supported and unsupported operations.

We'd like to support more Vega transforms over time (like contour and kde2d), but since this will take a while VegaFusion is designed to avoid extracting unsupported transforms (and all of their dependencies) to the server.

one quick question: sometimes all of data transformations are sent to my browser. I haven't investigated deeply because now it works like separating heavy parts work in server-side fortunately. Is there a way that I can check what part will work in server-side and which part will work in client-side before rendering it?

The best way right now is to look at the server_vega_spec property of the VegaFusionWidget instance. This will contain a Vega spec that includes the data specifications that were eligible for evaluation on the server. client_vega_spec contains the Vega spec that is actually rendered on the client (with the extracted data transforms removed), and comm_plan contains the specification of which datasets will be transferred between the client and server to maintain interactivity. This is described in https://vegafusion.io/planner_results.html. We need to add a compatibility table to the docs, but you can look at https://github.com/vega/vegafusion/tree/main/vegafusion-runtime/src/transform to see the collection of transforms that are currently (at least partially) implemented.

Does that help?