{ "cells": [ { "cell_type": "code", "execution_count": null, "id": "6098380b", "metadata": {}, "outputs": [], "source": [ "from os.path import join, dirname\n", "from os import listdir\n", "\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# GUI library\n", "import panel as pn\n", "import panel.widgets as pnw\n", "\n", "# Chart libraries\n", "from bokeh.plotting import figure\n", "from bokeh.models import ColumnDataSource, Legend\n", "from bokeh.palettes import Spectral5, Set2\n", "from bokeh.events import SelectionGeometry\n", "\n", "# Dimensionality reduction\n", "from sklearn.decomposition import PCA\n", "from sklearn.manifold import MDS\n", "from sklearn.preprocessing import StandardScaler, LabelEncoder\n", "# from umap import UMAP\n", "\n", "#\n", "from shapely.geometry import MultiPoint, MultiLineString, Polygon, MultiPolygon, LineString\n", "from shapely.ops import unary_union\n", "from shapely.ops import triangulate\n", "\n", "# local scripts\n", "from embedding.Rangeset import Rangeset\n", "from embedding.ProjectionQuality import projection_quality\n", "\n", "pn.extension()" ] }, { "cell_type": "markdown", "id": "0d7367ba", "metadata": {}, "source": [ "# Parameters" ] }, { "cell_type": "code", "execution_count": null, "id": "9b390ef1", "metadata": {}, "outputs": [], "source": [ "dataset_name = 'Betterlife'\n", "\n", "bins = 5\n", "\n", "show_labels = True\n", "labels_column = 'index'\n", "\n", "overview_height = 700\n", "\n", "small_multiples_ncols = 4\n", "histogram_width = 200\n", "show_numpy_histogram = True\n", "\n", "rangeset_threshold = 3" ] }, { "cell_type": "markdown", "id": "1a627638", "metadata": {}, "source": [ "# Load data" ] }, { "cell_type": "code", "execution_count": null, "id": "ec35e1f2", "metadata": {}, "outputs": [], "source": [ "df1 = pd.read_csv('data/BLI_30102020171001105.csv')\n", "df = df1[df1.INEQUALITY == 'TOT'].groupby(['Country', 'Indicator']).Value.sum().unstack(level=-1)\n", "df = df.fillna(df.mean())\n", "df['Household net adj. disposable income'] = [v / 1000 for v in df['Household net adjusted disposable income']]\n", "df['Household net wealth'] = [v / 1000 for v in df['Household net wealth']]\n", "df['Personal earnings'] = [v / 1000 for v in df['Personal earnings']]" ] }, { "cell_type": "code", "execution_count": null, "id": "024d3747", "metadata": {}, "outputs": [], "source": [ "label_encoders = {}\n", "\n", "for var in []:\n", " label_encoders[var] = LabelEncoder().fit(df[var])\n", " df.loc[:,var] = label_encoders[var].transform(df[var]) + 1" ] }, { "cell_type": "markdown", "id": "63038c54", "metadata": {}, "source": [ "# Preprocessing" ] }, { "cell_type": "code", "execution_count": null, "id": "2be524be", "metadata": {}, "outputs": [], "source": [ "print(list(df))" ] }, { "cell_type": "code", "execution_count": null, "id": "737f2ac0", "metadata": {}, "outputs": [], "source": [ "# attributes to be included\n", "selected_var = ['Feeling safe walking alone at night',\n", " 'Household net adj. disposable income',\n", " 'Household net wealth',\n", " 'Labour market insecurity',\n", " 'Life satisfaction',\n", " 'Long-term unemployment rate',\n", " 'Personal earnings',\n", " 'Quality of support network',\n", " 'Self-reported health',\n", " 'Student skills',\n", " 'Voter turnout']\n", "#selected_var = list(df)\n", "\n", "# maximal slider range and step size\n", "# {'variable_name': (min,max,stepsize)}\n", "custom_range = {'Life satisfaction': (0,10,.1),\n", " 'Feeling safe walking alone at night': (0,100,1),\n", " 'Long-term unemployment rate': (0,18,.5),\n", " 'Self-reported health': (0,100,1),\n", " 'Voter turnout': (0,100,1),\n", " 'Household net adj. disposable income': (0, 50, .5),\n", " 'Household net wealth': (0, 800, 5),\n", " 'Labour market insecurity': (0,30,.5),\n", " 'Personal earnings': (0,70, 1),\n", " 'Quality of support network': (75, 100, 1),\n", " 'Student skills': (350,550,5),\n", " 'projection quality': (0,1,0.01)}\n", "\n", "# custom min/max settings for sliders\n", "# {'variable_name': (min,max)}\n", "default_range = {'Life satisfaction': (5.3,7.6),\n", " 'Feeling safe walking alone at night': (36,90),\n", " 'Long-term unemployment rate': (0,8.5),\n", " 'Self-reported health': (33,88),\n", " 'Voter turnout': (47,91),\n", " 'Household net adj. disposable income': (16, 40),\n", " 'Household net wealth': (65,560),\n", " 'Labour market insecurity': (0.5,15.5),\n", " 'Personal earnings': (15,65),\n", " 'Quality of support network': (80, 98),\n", " 'Student skills': (430,530),\n", " 'projection quality': (0.65,1)}" ] }, { "cell_type": "code", "execution_count": null, "id": "1f32c502", "metadata": {}, "outputs": [], "source": [ "# which variables to use for the embedding\n", "selected_var_embd = selected_var.copy()\n", "# selected_var_embd = []\n", "\n", "# set up embedding\n", "#embedding = PCA(n_components=2)\n", "embedding = MDS(n_components=2, random_state=42)\n", "#embedding = UMAP(random_state=42)\n", "\n", "scaler = StandardScaler()\n", "X_scaled = scaler.fit_transform(df[selected_var_embd])\n", "\n", "# some projections change the original data, so we make a copy\n", "# this can cost a lot of memory for large data\n", "X = X_scaled.copy()\n", "pp = embedding.fit_transform(X)" ] }, { "cell_type": "code", "execution_count": null, "id": "e8f792c4", "metadata": {}, "outputs": [], "source": [ "x_range = pp[:,0].max() - pp[:,0].min()\n", "y_range = pp[:,1].max() - pp[:,1].min()\n", "\n", "# keep the aspect ration of the projected data\n", "overview_width = min(1000,int(overview_height * x_range / y_range))\n", "histogram_height = min(200,int(histogram_width * y_range / x_range))" ] }, { "cell_type": "code", "execution_count": null, "id": "f40ccca7", "metadata": {}, "outputs": [], "source": [ "# add projection quality\n", "df['projection quality'] = projection_quality(X_scaled, pp)\n", "selected_var += ['projection quality']\n", "\n", "print('mean projection quality', df['projection quality'].mean())" ] }, { "cell_type": "code", "execution_count": null, "id": "1dd1d0ec", "metadata": {}, "outputs": [], "source": [ "rangeset = Rangeset(pp, df)\n", "rangeset.threshold = rangeset_threshold\n", "rangeset.size_inside = 5\n", "rangeset.size_outside = 13" ] }, { "cell_type": "code", "execution_count": null, "id": "c4701a0a", "metadata": {}, "outputs": [], "source": [ "from scipy.sparse import csr_matrix\n", "from scipy.sparse.csgraph import minimum_spanning_tree\n", "from sklearn.metrics import pairwise_distances\n", "\n", "D = csr_matrix(pairwise_distances(pp))\n", "MST = minimum_spanning_tree(D)\n", "MST = MST[MST.nonzero()].A1\n", "print('max edge in MST: {:.2f}'.format(MST.max()))" ] }, { "cell_type": "code", "execution_count": null, "id": "9b55eea8", "metadata": {}, "outputs": [], "source": [ "eps = np.quantile(MST, .75) + 1.5*(np.quantile(MST, .75) - np.quantile(MST, .25))" ] }, { "cell_type": "code", "execution_count": null, "id": "2218af61", "metadata": {}, "outputs": [], "source": [ "print('Wilkinson epsilon: {:.2f}'.format(eps))" ] }, { "cell_type": "code", "execution_count": null, "id": "164cf4eb", "metadata": {}, "outputs": [], "source": [ "# sorted(MST[MST.nonzero()].A[0])" ] }, { "cell_type": "markdown", "id": "02ec2dfe", "metadata": {}, "source": [ "# GUI\n", "\n", "## Vis elements\n", "\n", "**overview chart** shows a large version of the embedding" ] }, { "cell_type": "code", "execution_count": null, "id": "80d986dc", "metadata": {}, "outputs": [], "source": [ "TOOLS = \"pan,wheel_zoom,box_zoom,box_select,lasso_select,help,reset,save\"\n", "overview = figure(tools=TOOLS, width=overview_width, height=overview_height, active_drag=\"lasso_select\")\n", "\n", "overview.scatter(x=pp[:,0], y=pp[:,1], color=\"#333333\", muted_alpha=0,\n", " size=3, level='underlay', name='points',\n", " line_color=None, legend_label='data')\n", "\n", "if show_labels:\n", " labels = df.index.astype(str) if labels_column == 'index' else df[labels_column].astype(str)\n", " overview.text(x=pp[:,0], y=pp[:,1], text=labels, legend_label='labels',\n", " font_size=\"10pt\", x_offset=5, y_offset=5, muted_alpha=0,\n", " text_baseline=\"middle\", text_align=\"left\", color='#666666', level='annotation')\n", "\n", "source_selection = ColumnDataSource({'x': [], 'y': []})\n", "overview.patch(source=source_selection, x='x', y='y', fill_color=None, line_width=2, line_color='#4d4d4d',\n", " level='annotation')\n", "\n", "overview.legend.location = 'bottom_right'\n", "overview.legend.label_height=1\n", "overview.legend.click_policy='mute'\n", "overview.legend.visible = True\n", "\n", "overview.outline_line_color = None\n", "\n", "overview.xgrid.visible = False\n", "overview.ygrid.visible = False\n", "overview.xaxis.visible = False\n", "overview.yaxis.visible = False\n", "overview.toolbar.logo = None" ] }, { "cell_type": "code", "execution_count": null, "id": "613a8300", "metadata": {}, "outputs": [], "source": [ "# Check the embedding with the code below\n", "\n", "# pn.Row(overview)" ] }, { "cell_type": "markdown", "id": "da7bea52", "metadata": {}, "source": [ "**small multiples** charts are created upon request" ] }, { "cell_type": "code", "execution_count": null, "id": "0deea1a8", "metadata": {}, "outputs": [], "source": [ "def _make_chart( var, df_polys, df_scatter, bounds, cnt_in, cnt_out ):\n", " global df\n", "\n", " xvals = df[var].unique()\n", " is_categorical = False\n", " if len(xvals) < 10:\n", " is_categorical = True\n", " xvals = sorted(xvals.astype(str))\n", " \n", " global histogram_width\n", " p = figure(width=histogram_width, height=histogram_height, title=var)\n", " df_scatter['size'] = df_scatter['size'] * histogram_height / overview_height\n", " \n", " p.multi_polygons(source=df_polys, xs='xs', ys='ys', color='color', fill_alpha=0.5, level='image', line_color=None)\n", " p.scatter(source=df_scatter, x='x', y='y', color='color', size='size', level='overlay')\n", " \n", " global source_selection\n", " p.patch(source=source_selection, x='x', y='y', fill_color=None, level='annotation', line_width=1, line_color='#4d4d4d')\n", " \n", " p.xgrid.visible = False\n", " p.ygrid.visible = False\n", " p.xaxis.visible = False\n", " p.yaxis.visible = False\n", " p.toolbar.logo = None\n", " p.toolbar_location = None\n", " p.border_fill_color = '#f0f0f0'\n", " \n", " p_histo = figure(height=100, width=histogram_width, name='histo')\n", " if is_categorical:\n", " p_histo = figure(height=100, width=histogram_width, name='histo', x_range=xvals)\n", " p_histo.vbar(x=xvals, top=cnt_in, bottom=0, width=0.9, line_color='white', color=rangeset.colormap)\n", " p_histo.vbar(x=xvals, top=0, bottom=np.array(cnt_out)*-1, width=0.9, line_color='white', color=rangeset.colormap)\n", " else:\n", " p_histo.quad(bottom=[0]*len(cnt_in), top=cnt_in, left=bounds[:-1], right=bounds[1:], line_color='white', color=rangeset.colormap)\n", " p_histo.quad(bottom=np.array(cnt_out)*(-1), top=[0]*len(cnt_out), left=bounds[:-1], right=bounds[1:], line_color='white', color=rangeset.colormap)\n", "\n", " df_select = df[df[var] < bounds[0]]\n", " p_histo.square(df_select[var], -.5, color=rangeset.colormap[0])\n", " df_select = df[df[var] > bounds[-1]]\n", " p_histo.square(df_select[var], -.5, color=rangeset.colormap[-1])\n", " \n", " p_histo.toolbar.logo = None\n", " p_histo.toolbar_location = None\n", " p_histo.xgrid.visible = False\n", " p_histo.xaxis.minor_tick_line_color = None\n", " p_histo.yaxis.minor_tick_line_color = None\n", " p_histo.outline_line_color = None\n", " p_histo.border_fill_color = '#f0f0f0'\n", " \n", " global show_numpy_histogram\n", " if show_numpy_histogram:\n", " if is_categorical:\n", " frequencies, edges = np.histogram(df[var], bins=len(xvals))\n", " p_histo.vbar(x=xvals, bottom=0, width=.5, top=frequencies*-1,\n", " line_color='white', color='gray', line_alpha=.5, fill_alpha=0.5)\n", " else:\n", " frequencies, edges = np.histogram(df[var])\n", " p_histo.quad(bottom=[0]*len(frequencies), top=frequencies*-1, left=edges[:-1], right=edges[1:], \n", " line_color='white', color='gray', line_alpha=.5, fill_alpha=0.5)\n", " \n", " return (p, p_histo)" ] }, { "cell_type": "markdown", "id": "6d80242c", "metadata": {}, "source": [ "## Create input widget (buttons, sliders, etc) and layout" ] }, { "cell_type": "code", "execution_count": null, "id": "45ff3d45", "metadata": {}, "outputs": [], "source": [ "class MyCheckbox(pnw.Checkbox):\n", " variable = \"\"\n", " \n", " def __init__(self, variable=\"\", slider=None, **kwds):\n", " super().__init__(**kwds)\n", " \n", " self.variable = variable\n", " self.slider = slider\n", " \n", "def init_slider_values(var):\n", " vmin = df[var].min()\n", " vmax = df[var].max()\n", " step = 0\n", " \n", " if var in custom_range:\n", " vmin,vmax,step = custom_range[var]\n", " value = (vmin,vmax)\n", " \n", " if var in default_range:\n", " value = default_range[var]\n", " \n", " return (vmin, vmax, step, value)" ] }, { "cell_type": "markdown", "id": "0dac1b5a", "metadata": {}, "source": [ "Create all toplevel GUI elements" ] }, { "cell_type": "code", "execution_count": null, "id": "015258eb", "metadata": {}, "outputs": [], "source": [ "ranges_embd = pn.Column()\n", "ranges_aux = pn.Column()\n", "\n", "sliders = {}\n", "\n", "def create_slider(var):\n", " vmin, vmax, step, value = init_slider_values(var) \n", " slider = pnw.RangeSlider(name=var, start=vmin, end=vmax, step=step, value=value) \n", "\n", " checkbox = MyCheckbox(name='', variable=var, value=False, width=20, slider=slider)\n", " return pn.Row(checkbox,slider)\n", " \n", "for var in selected_var:\n", " s = create_slider(var)\n", " sliders[var] = s\n", "\n", " if var in selected_var_embd:\n", " ranges_embd.append(s)\n", " else:\n", " ranges_aux.append(s)\n", " \n", "selected_var = []\n", "\n", "for r in ranges_embd:\n", " selected_var.append(r[1].name)\n", "for r in ranges_aux:\n", " selected_var.append(r[1].name)" ] }, { "cell_type": "code", "execution_count": null, "id": "da79a863", "metadata": {}, "outputs": [], "source": [ "gui_colormap = pn.Row(pn.pane.Str(styles={'background': rangeset.colormap[0]}, height=30, width=20), \"very low\",\n", " pn.pane.Str(styles={'background': rangeset.colormap[1]}, height=30, width=20), \"low\",\n", " pn.pane.Str(styles={'background': rangeset.colormap[2]}, height=30, width=20), \"medium\",\n", " pn.pane.Str(styles={'background': rangeset.colormap[3]}, height=30, width=20), \"high\",\n", " pn.pane.Str(styles={'background': rangeset.colormap[4]}, height=30, width=20), \"very high\", sizing_mode='stretch_width')\n", "\n", "selectColoring = pn.widgets.Select(name='', options=['None']+selected_var)\n", "\n", "\n", "# set up the GUI\n", "layout = pn.Row(pn.Column(\n", " pn.Row(pn.pane.Markdown('''# NoLiES: The non-linear embedding surveyor\\n\n", "NoLiES augments the projected data with additional information. The following interactions are supported:\\n\n", "* **Attribute-based coloring** Chose an attribute from the drop-down menu below the embedding to display contours for multiple value ranges.\n", "* **Selective muting**: Click on the legend to mute/hide parts of the chart. Press _labels_ to hide the labels.\n", "* **Contour control** Change the slider range to change the contours.\n", "* **Histograms** Select the check-box next to the slider to view the attribute's histogram.\n", "* **Selection** Use the selection tool to outline a set of points and share this outline across plots.''', sizing_mode='stretch_width'), \n", " margin=(0, 25,0,25)),\n", " pn.Row(\n", " pn.Column(pn.pane.Markdown('''# Attributes\\nEnable histograms with the checkboxes.'''), \n", " '## Embedding',\n", " ranges_embd,\n", " #pn.layout.Divider(),\n", " '## Auxiliary',\n", " ranges_aux, margin=(0, 25, 0, 0)),\n", " pn.Column(pn.pane.Markdown('''# Embedding - '''+type(embedding).__name__+'''   Dataset - '''+dataset_name, sizing_mode='stretch_width'), \n", " overview, \n", " pn.Row(selectColoring, gui_colormap)\n", " ), \n", " margin=(0,25,25,25)\n", " ), \n", " #pn.Row(sizing_mode='stretch_height'), \n", " pn.Row(pn.pane.Markdown('''Data source: http://stats.oecd.org/Index.aspx?DataSetCode=BLI''',\n", " width=800), sizing_mode='stretch_width', margin=(0,25,0,25))),\n", " pn.GridBox(ncols=small_multiples_ncols, sizing_mode='stretch_both', margin=(220,25,0,0)),\n", " styles={'background': '#efefef'}\n", " )" ] }, { "cell_type": "markdown", "id": "e542a53c", "metadata": {}, "source": [ "Adjust the order of the variable so that it reflects the sorting of the range sliders (we distinguish between those used for embedding and auxiliary ones)." ] }, { "cell_type": "code", "execution_count": null, "id": "6a3a995b", "metadata": {}, "outputs": [], "source": [ "# Check the GUI with the following code - this version is not interactive yet\n", "\n", "layout" ] }, { "cell_type": "markdown", "id": "1b30905c", "metadata": {}, "source": [ "## Callbacks\n", "\n", "Callbacks for **slider interactions**" ] }, { "cell_type": "code", "execution_count": null, "id": "da84cc58", "metadata": {}, "outputs": [], "source": [ "visible = [False]*len(selected_var)\n", "mapping = {v: k for k, v in dict(enumerate(selected_var)).items()}\n", "\n", " \n", "def onSliderChanged(event):\n", " '''Actions upon attribute slider change.\n", " \n", " Attributes\n", " ----------\n", " event: bokeh.Events.Event\n", " information about the event that triggered the callback\n", " '''\n", "\n", " var = event.obj.name\n", " v_range = event.obj.value\n", " \n", " # if changed variable is currently displayed\n", " if var == layout[0][1][1][2][0].value:\n", " setColoring(var, v_range)\n", " \n", " # find the matching chart and update it\n", " for col in layout[1]:\n", " if col.name == var:\n", " df_polys, df_scatter, bounds, cnt_in, cnt_out = rangeset.compute_contours(var, v_range, bins=20 if col.name == 'groups' else 5)\n", " p,histo = _make_chart(var, df_polys, df_scatter, bounds, cnt_in, cnt_out)\n", " col[0].object = p\n", " col[1].object = histo\n", "\n", "def onSliderChanged_released(event):\n", " '''Actions upon attribute slider change.\n", " \n", " Attributes\n", " ----------\n", " event: bokeh.Events.Event\n", " information about the event that triggered the callback\n", " '''\n", "\n", " var = event.obj.name\n", " v_range = event.obj.value\n", " \n", " print('\\''+var+'\\': ('+str(v_range[0])+','+str(v_range[1])+')')\n", "\n", "\n", "def onAttributeSelected(event):\n", " '''Actions upon attribute checkbox change.\n", " \n", " Attributes\n", " ----------\n", " event: bokeh.Events.Event\n", " information about the event that triggered the callback\n", " '''\n", " var = event.obj.variable\n", " i = mapping[var]\n", " \n", " if event.obj.value == True:\n", " v_range = event.obj.slider.value\n", " \n", " df_polys, df_scatter, bounds, cnt_in, cnt_out = rangeset.compute_contours(var, v_range)\n", " p,p_histo = _make_chart(var, df_polys, df_scatter, bounds, cnt_in, cnt_out)\n", " pos_insert = sum(visible[:i])\n", " layout[1].insert(pos_insert, pn.Column(p,pn.panel(p_histo), name=var, margin=5))\n", " else:\n", " pos_remove = sum(visible[:i])\n", " layout[1].pop(pos_remove)\n", " \n", " visible[i] = event.obj.value \n", "\n", "# link widgets to their callbacks\n", "for var in sliders.keys():\n", " sliders[var][0].param.watch(onAttributeSelected, 'value')\n", " sliders[var][1].param.watch(onSliderChanged, 'value')\n", " sliders[var][1].param.watch(onSliderChanged_released, 'value_throttled')" ] }, { "cell_type": "markdown", "id": "4d8a1fd2", "metadata": {}, "source": [ "Callbacks **rangeset selection** in overview plot" ] }, { "cell_type": "code", "execution_count": null, "id": "625f690b", "metadata": {}, "outputs": [], "source": [ "def clearColoring():\n", " '''Remove rangeset augmentation from the embedding.'''\n", " \n", " global overview\n", " overview.legend.visible = False\n", " \n", " for r in overview.renderers:\n", " if r.name is not None and ('poly' in r.name or 'scatter' in r.name):\n", " r.visible = False\n", " r.muted = True\n", " \n", "def setColoring(var, v_range=None):\n", " '''Compute and render the rangeset for a selected variable.\n", " \n", " Attributes\n", " ----------\n", " var: str\n", " the selected variable\n", " v_range: tuple (min,max)\n", " the user define value range for the rangeset\n", " '''\n", " \n", " global overview \n", " overview.legend.visible = True\n", " \n", " df_polys, df_scatter, bounds, cnt,cnt = rangeset.compute_contours(var, val_range=v_range, bins=bins)\n", " for r in overview.renderers:\n", " if r.name is not None and ('poly' in r.name or 'scatter' in r.name):\n", " r.visible = False\n", " r.muted = True\n", " \n", " if len(df_polys) > 0:\n", " for k in list(rangeset.labels.keys())[::-1]:\n", " g = df_polys[df_polys.color == k]\n", " \n", " label_id = rangeset.color2label(k)\n", " label = label_id\n", " if var in label_encoders.keys():\n", " label = label_id + ' ' +label_encoders[var].inverse_transform([int(rangeset.color2label(k))-1])[0]\n", " r = overview.select('poly '+label)\n", " \n", " if len(r) > 0:\n", " r[0].visible = True\n", " r[0].muted = False\n", " r[0].data_source.data = dict(ColumnDataSource(g).data)\n", " else:\n", " overview.multi_polygons(source = g, xs='xs', ys='ys', name='poly '+label, level='image',\n", " color='color', alpha=.5, legend_label=label,\n", " line_color=None, muted_color='gray', muted_alpha=.1) \n", " \n", " g = df_scatter[df_scatter.color == k]\n", " r = overview.select('scatter '+label)\n", " if len(r) > 0:\n", " r[0].visible = True\n", " r[0].muted = False\n", " r[0].data_source.data = dict(ColumnDataSource(g).data)\n", " else:\n", " overview.circle(source = g, x='x', y='y', size='size', name='scatter '+label,\n", " color='color', alpha=1, legend_label=label,\n", " muted_color='gray', muted_alpha=0) \n", "\n", "def onChangeColoring(event):\n", " '''Actions upon change of the rangeset attribute.\n", " \n", " Attributes\n", " ----------\n", " event: bokeh.Events.Event\n", " information about the event that triggered the callback\n", " '''\n", " var = event.obj.value\n", " \n", " if var == 'None':\n", " clearColoring()\n", " else:\n", " v_range = sliders[var][1].value\n", " setColoring(var, v_range)\n", " \n", "selectColoring.param.watch( onChangeColoring, 'value' )" ] }, { "cell_type": "markdown", "id": "13342617", "metadata": {}, "source": [ "User **selection of data points** in the overview chart." ] }, { "cell_type": "code", "execution_count": null, "id": "d3cc0a4b", "metadata": {}, "outputs": [], "source": [ "def onSelectionChanged(event):\n", " if event.final:\n", " sel_pp = pp[list(overview.select('points').data_source.selected.indices)]\n", " if len(sel_pp) == 0:\n", " source_selection.data = dict({'x': [], 'y': []})\n", " else:\n", " points = MultiPoint(sel_pp)\n", " poly = unary_union([polygon for polygon in triangulate(points) if rangeset._max_edge(polygon) < 3]).boundary.parallel_offset(-0.05).coords.xy\n", " source_selection.data = dict({'x': poly[0].tolist(), 'y': poly[1].tolist()})\n", "\n", "overview.on_event(SelectionGeometry, onSelectionChanged)" ] }, { "cell_type": "code", "execution_count": null, "id": "3fa37029", "metadata": {}, "outputs": [], "source": [ "layout.servable('NoLies')" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.10" } }, "nbformat": 4, "nbformat_minor": 5 }