python/holoviz/panel/panel/tests/test_links.py

test_links.py
try:
    import holoviews as hv
except ImportError:
    hv = None

import pytest

from bokeh.plotting import figure
from panel.layout import Row
from panel.links import Link
from panel.pane import Bokeh, HoloViews
from panel.widgets import FloatSlider, RangeSlider, ColorPicker, TextInput, DatetimeInput
from panel.tests.util import hv_available


def test_widget_link_bidirectional():
    t1 = TextInput()
    t2 = TextInput()

    t1.link(t2, value='value', bidirectional=True)

    t1.value = 'ABC'
    assert t1.value == 'ABC'
    assert t2.value == 'ABC'

    t2.value = 'DEF'
    assert t1.value == 'DEF'
    assert t2.value == 'DEF'


def test_widget_jslink_bidirectional(document, comm):
    t1 = TextInput()
    t2 = TextInput()

    t1.jslink(t2, value='value', bidirectional=True)

    row = Row(t1, t2)

    model = row.get_root(document, comm)

    tm1, tm2 = model.children

    link1_customjs = tm1.js_property_callbacks['change:value'][-1]
    link2_customjs = tm2.js_property_callbacks['change:value'][-1]

    assert link1_customjs.args['source'] is tm1
    assert link2_customjs.args['source'] is tm2
    assert link1_customjs.args['target'] is tm2
    assert link2_customjs.args['target'] is tm1


def test_widget_link_source_param_not_found():
    t1 = TextInput()
    t2 = TextInput()

    with pytest.raises(ValueError) as excinfo:
        t1.jslink(t2, value1='value')
    assert "Could not jslink \'value1\' parameter" in str(excinfo)


def test_widget_link_target_param_not_found():
    t1 = TextInput()
    t2 = TextInput()

    with pytest.raises(ValueError) as excinfo:
        t1.jslink(t2, value='value1')
    assert "Could not jslink \'value1\' parameter" in str(excinfo)


def test_widget_link_no_transform_error():
    t1 = DatetimeInput()
    t2 = TextInput()

    with pytest.raises(ValueError) as excinfo:
        t1.jslink(t2, value='value')
    assert "Cannot jslink \'value\' parameter on DatetimeInput object" in str(excinfo)


def test_widget_link_no_target_transform_error():
    t1 = DatetimeInput()
    t2 = TextInput()

    with pytest.raises(ValueError) as excinfo:
        t2.jslink(t1, value='value')
    assert ("Cannot jslink \'value\' parameter on TextInput object "
            "to \'value\' parameter on DatetimeInput object") in str(excinfo)

    
@hv_available
def test_pnwidget_hvplot_links(document, comm):
    size_widget = FloatSlider(value=5, start=1, end=10)
    points1 = hv.Points([1, 2, 3])

    size_widget.jslink(points1, value='glyph.size')

    row = Row(points1, size_widget)
    model = row.get_root(document, comm=comm)
    hv_views = row.select(HoloViews)
    widg_views = row.select(FloatSlider)

    assert len(hv_views) == 1
    assert len(widg_views) == 1
    slider = widg_views[0]._models[model.ref['id']][0]
    scatter = hv_views[0]._plots[model.ref['id']][0].handles['glyph']

    link_customjs = slider.js_property_callbacks['change:value'][-1]
    assert link_customjs.args['source'] is slider
    assert link_customjs.args['target'] is scatter

    code = """
    var value = source['value'];
    value = value;
    value = value;
    try {
      var property = target.properties['size'];
      if (property !== undefined) { property.validate(value); }
    } catch(err) {
      console.log('WARNING: Could not set size on target, raised error: ' + err);
      return;
    }
    try {
      target['size'] = value;
    } catch(err) {
      console.log(err)
    }
    """
    assert link_customjs.code == code


@hv_available
def test_bkwidget_hvplot_links(document, comm):
    from bokeh.models import Slider
    bokeh_widget = Slider(value=5, start=1, end=10, step=1e-1)
    points1 = hv.Points([1, 2, 3])

    Link(bokeh_widget, points1, properties={'value': 'glyph.size'})

    row = Row(points1, bokeh_widget)
    model = row.get_root(document, comm=comm)
    hv_views = row.select(HoloViews)

    assert len(hv_views) == 1
    slider = bokeh_widget
    scatter = hv_views[0]._plots[model.ref['id']][0].handles['glyph']

    link_customjs = slider.js_property_callbacks['change:value'][-1]
    assert link_customjs.args['source'] is slider
    assert link_customjs.args['target'] is scatter

    code = """
    var value = source['value'];
    value = value;
    value = value;
    try {
      var property = target.properties['size'];
      if (property !== undefined) { property.validate(value); }
    } catch(err) {
      console.log('WARNING: Could not set size on target, raised error: ' + err);
      return;
    }
    try {
      target['size'] = value;
    } catch(err) {
      console.log(err)
    }
    """
    assert link_customjs.code == code


def test_bkwidget_bkplot_links(document, comm):
    from bokeh.models import Slider
    bokeh_widget = Slider(value=5, start=1, end=10, step=1e-1)
    bokeh_fig = figure()
    scatter = bokeh_fig.scatter([1, 2, 3], [1, 2, 3])

    Link(bokeh_widget, scatter, properties={'value': 'glyph.size'})

    row = Row(bokeh_fig, bokeh_widget)
    row.get_root(document, comm=comm)

    slider = bokeh_widget

    link_customjs = slider.js_property_callbacks['change:value'][-1]
    assert link_customjs.args['source'] is slider
    assert link_customjs.args['target'] is scatter.glyph

    code = """
    var value = source['value'];
    value = value;
    value = value;
    try {
      var property = target.properties['size'];
      if (property !== undefined) { property.validate(value); }
    } catch(err) {
      console.log('WARNING: Could not set size on target, raised error: ' + err);
      return;
    }
    try {
      target['size'] = value;
    } catch(err) {
      console.log(err)
    }
    """
    assert link_customjs.code == code


def test_widget_bkplot_link(document, comm):
    widget = ColorPicker(value='#ff00ff')
    bokeh_fig = figure()
    scatter = bokeh_fig.scatter([1, 2, 3], [1, 2, 3])

    widget.jslink(scatter.glyph, value='fill_color')

    row = Row(bokeh_fig, widget)
    model = row.get_root(document, comm=comm)

    link_customjs = model.children[1].js_property_callbacks['change:color'][-1]
    assert link_customjs.args['source'] is model.children[1]
    assert link_customjs.args['target'] is scatter.glyph
    assert scatter.glyph.fill_color == '#ff00ff'
    
    code = """
    var value = source['color'];
    value = value;
    value = value;
    try {
      var property = target.properties['fill_color'];
      if (property !== undefined) { property.validate(value); }
    } catch(err) {
      console.log('WARNING: Could not set fill_color on target, raised error: ' + err);
      return;
    }
    try {
      target['fill_color'] = value;
    } catch(err) {
      console.log(err)
    }
    """
    assert link_customjs.code == code


def test_bokeh_figure_jslink(document, comm):
    fig = figure()

    pane = Bokeh(fig)
    t1 = TextInput()
    
    pane.jslink(t1, **{'x_range.start': 'value'})
    row = Row(pane, t1)
    
    model = row.get_root(document, comm)

    link_customjs = fig.x_range.js_property_callbacks['change:start'][-1]
    assert link_customjs.args['source'] == fig.x_range
    assert link_customjs.args['target'] == model.children[1]
    assert link_customjs.code == """
    var value = source['start'];
    value = value;
    value = value;
    try {
      var property = target.properties['value'];
      if (property !== undefined) { property.validate(value); }
    } catch(err) {
      console.log('WARNING: Could not set value on target, raised error: ' + err);
      return;
    }
    try {
      target['value'] = value;
    } catch(err) {
      console.log(err)
    }
    """

def test_widget_jscallback(document, comm):
    widget = ColorPicker(value='#ff00ff')

    widget.jscallback(value='some_code')

    model = widget.get_root(document, comm=comm)

    customjs = model.js_property_callbacks['change:color'][-1]
    assert customjs.args['source'] is model
    assert customjs.code == "try { some_code } catch(err) { console.log(err) }"


def test_widget_jscallback_args_scalar(document, comm):
    widget = ColorPicker(value='#ff00ff')

    widget.jscallback(value='some_code', args={'scalar': 1})

    model = widget.get_root(document, comm=comm)

    customjs = model.js_property_callbacks['change:color'][-1]
    assert customjs.args['scalar'] == 1


def test_widget_jscallback_args_model(document, comm):
    widget = ColorPicker(value='#ff00ff')
    widget2 = ColorPicker(value='#ff00ff')

    widget.jscallback(value='some_code', args={'widget': widget2})

    model = Row(widget, widget2).get_root(document, comm=comm)

    customjs = model.children[0].js_property_callbacks['change:color'][-1]
    assert customjs.args['source'] is model.children[0]
    assert customjs.args['widget'] is model.children[1]
    assert customjs.code == "try { some_code } catch(err) { console.log(err) }"


@hv_available
def test_hvplot_jscallback(document, comm):
    points1 = hv.Points([1, 2, 3])

    hvplot = HoloViews(points1)

    hvplot.jscallback(**{'x_range.start': "some_code"})

    model = hvplot.get_root(document, comm=comm)
    x_range = hvplot._plots[model.ref['id']][0].handles['x_range']

    customjs = x_range.js_property_callbacks['change:start'][-1]
    assert customjs.args['source'] is x_range
    assert customjs.code == "try { some_code } catch(err) { console.log(err) }"


@hv_available
def test_link_with_customcode(document, comm):
    range_widget = RangeSlider(start=0., end=1.)
    curve = hv.Curve([])
    code = """
      x_range.start = source.value[0]
      x_range.end = source.value[1]
    """
    range_widget.jslink(curve, code={'value': code})
    row = Row(curve, range_widget)

    range_widget.value = (0.5, 0.7)
    model = row.get_root(document, comm=comm)
    hv_views = row.select(HoloViews)
    widg_views = row.select(RangeSlider)

    assert len(hv_views) == 1
    assert len(widg_views) == 1
    range_slider = widg_views[0]._models[model.ref['id']][0]
    x_range = hv_views[0]._plots[model.ref['id']][0].handles['x_range']

    link_customjs = range_slider.js_property_callbacks['change:value'][-1]
    assert link_customjs.args['source'] is range_slider
    assert link_customjs.args['x_range'] is x_range
    assert link_customjs.code == "try { %s } catch(err) { console.log(err) }" % code