ipyflex: A WYSIWYG layout editor for Jupyter widgets

Version: 0.1.2

ipyflex aims to help users transform existing Jupyter widgets into an interactive dashboard with a sophisticated layout without coding.

By being a Jupyter widget itself, ipyflex can be easily integrated with Voila to deploy the dashboard.

_images/ipyflex-intro.png

A ipyflex dashboard deployed with Voila.

Quickstart

To get started with ipyflex, install with pip:

pip install ipyflex

or with conda:

conda install -c conda-forge  ipyflex

Contents

Installation

The simplest way to install ipyflex is via pip:

pip install ipyflex

or via conda:

conda install -c conda-forge  ipyflex
  • If you installed via pip, and notebook version < 5.3, you will also have to install / configure the front-end extension as well. If you are using classic notebook (as opposed to Jupyterlab), run:

    jupyter nbextension install [--sys-prefix / --user / --system] --py ipyflex
    jupyter nbextension enable [--sys-prefix / --user / --system] --py ipyflex
    

with the appropriate flag. If you are installing using conda, these commands should be unnecessary, but If you need to run them the commands should be the same (just make sure you choose the –sys-prefix flag).

  • If you are using Jupyterlab <= 2, install the extension with:

    conda install -c conda-forge yarn
    jupyter labextension install @jupyter-widgets/jupyterlab-manager ipyflex
    

Usage

ipyflex is meant to be used with widgets based on ipywidgets. The entry point of ipyflex is the FlexLayout class, it allows users to dynamically customize the layout and fill their dashboard from the existing widgets.

Create a dashboard from existing widgets

The simplest way to create an ipyflex dashboard is to create a dictionary of existing widgets with the keys are the names of the widget and values are the instances of widgets and then use FlexLayout to compose the layout.

from ipyflex import FlexLayout
import ipywidgets as ipw
widgets = { 'Widget 1': ipw.HTML('<h1> Widget 1</h1>'),
            'Widget 2': ipw.HTML('<h1> Widget 2</h1>'),
            'Widget 3': ipw.HTML('<h1> Widget 3</h1>'),
            'Widget 4': ipw.HTML('<h1> Widget 4</h1>')
        }
dashboard = FlexLayout(widgets)
dashboard
_images/ipyflex.gif

Users can pass some configurations to the constructor of FlexLayout to set the template of the style of the dashboard:

dashboard = FlexLayout(widgets,
    template = 'saved.json',
    style = {'height': '50vh', 'borderTop': '5px'},
    editable = False)
  • template: the path to save template file, this file can be generated from the dashboard interface.

  • style: CSS styles to be passed to the root element of the dashboard, it accepts any CSS rules but the keys need to be in camelCase format.

  • editable: flag to enable or disable the editable mode. In non-editable mode, the toolbar with the Save template button is removed, tabs can not be removed, dragged, or renamed.

FlexLayout interface

FlexLayout interface is composed of three components:

  • Toolbar: located at bottom of the interface, it contains the button to save the current layout template to disk.

  • Section tab bar: a bar to hold the section tabs, it is located on top of the toolbar. A FlexLayout dashboard can contain multiple sections.

  • Section display window: the activated section is shown in this window. Each section is can be composed of multiple widgets.

A typical interface is displayed in the figure below:

_images/ipyflex-main.png
Toolbar
  • Save template: save dashboard configuration into a json file in the current working folder. If FlexLayout is started with a template, the current template will be overwritten.

Section tab bar
  • Uses can use + button to add a new section into the dashboard, a section is displayed as a tab in the section tab bar. Each section can be dragged to modify its position, double-clicked to rename, and removed with the x button.

_images/ipyflex-section.gif
Section display window
  • A section is composed of multiple widgets, users can use the add widget button to add the predefined widgets into the section. The added widget will be displayed in the widget tab bar with the name taken from its key in the widget dictionary.

  • A typical layout of a section with annotation for buttons is shown in the image below:

_images/ipyflex-widget-window.png
  • The widget menu can be opened by the add widget button, it contains the keys of the widget dictionary defined in the constructor of FlexLayout. The Create new item in the widget menu is always available, it will be detailed in the next section.

  • Right-click on the section name will give users options to show or hide the widget tab bar of this section.

  • Users can customize the layout of a section by using drag and drop on each widget. The widgets can also be resized by dragging their borders.

  • Users can change the name of the widget tab by double-clicking on the tab name.

_images/ipyflex-widget-main.gif

Create a dashboard layout without widgets

Even without widgets, users can still define a dashboard layout with FlexLayout and then fill the dashboard progressively. To do so, just use the Create new button in the widget menu to add widgets to the dashboard, the placeholder tabs will be created for the new widgets. Once the real widgets are ready, users can update the dashboard with add method:

dashboard = FlexLayout() # Create an empty dashboard
#Add a widget named `foo` to the dashboard by using `Create new` button
#Now add the real widget `foo_widget` to dashboard
dashboard.add('foo', foo_widget)
#The dashboard will be updated with the real widget.
_images/ipyflex-create-new.gif

Examples

This section contains several examples generated from Jupyter notebooks. The widgets have been embedded into the page for demonstrative purposes.

Ipygany example

[1]:
# This notebook is taken from https://github.com/QuantStack/ipygany/blob/master/examples/isocolor.ipynb
# with adaptation to use with ipyflex

import numpy as np
from ipywidgets import FloatSlider, FloatRangeSlider, Dropdown, Select, VBox, AppLayout, jslink
from ipygany import Scene, IsoColor, PolyMesh, Component, ColorBar, colormaps
from ipyflex import FlexLayout

# Create triangle indices
nx = 100
ny = 100

triangle_indices = np.empty((ny - 1, nx - 1, 2, 3), dtype=int)

r = np.arange(nx * ny).reshape(ny, nx)

triangle_indices[:, :, 0, 0] = r[:-1, :-1]
triangle_indices[:, :, 1, 0] = r[:-1, 1:]
triangle_indices[:, :, 0, 1] = r[:-1, 1:]

triangle_indices[:, :, 1, 1] = r[1:, 1:]
triangle_indices[:, :, :, 2] = r[1:, :-1, None]

triangle_indices.shape = (-1, 3)

# Create vertices
x = np.arange(-5, 5, 10/nx)
y = np.arange(-5, 5, 10/ny)

xx, yy = np.meshgrid(x, y, sparse=True)

z = np.sin(xx**2 + yy**2) / (xx**2 + yy**2)

vertices = np.empty((ny, nx, 3))
vertices[:, :, 0] = xx
vertices[:, :, 1] = yy
vertices[:, :, 2] = z
vertices = vertices.reshape(nx * ny, 3)

height_component = Component(name='value', array=z)

mesh = PolyMesh(
    vertices=vertices,
    triangle_indices=triangle_indices,
    data={'height': [height_component]}
)

height_min = np.min(z)
height_max = np.max(z)

# Colorize by height
colored_mesh = IsoColor(mesh, input='height', min=height_min, max=height_max)

# Create a slider that will dynamically change the boundaries of the colormap
colormap_slider_range = FloatRangeSlider(value=[height_min, height_max], min=height_min, max=height_max, step=(height_max - height_min) / 100.)

jslink((colored_mesh, 'range'), (colormap_slider_range, 'value'))

# Create a colorbar widget
colorbar = ColorBar(colored_mesh)

# Colormap choice widget
colormap = Dropdown(
    options=colormaps,
    description='colormap:'
)

jslink((colored_mesh, 'colormap'), (colormap, 'index'))
scene = Scene([colored_mesh])
color = VBox([colormap, colorbar])
[2]:
# Create the dictionary of widgets
widgets = {'Viewer': scene,'Slider range': colormap_slider_range, 'Color map': color}
[3]:
w = FlexLayout(widgets, style={'height':'620px'}, template = '3d.json', editable=False)
[4]:
w
[ ]:

Simple dashboard with chart

[1]:
from ipyflex import FlexLayout
import ipywidgets as widgets
import plotly.graph_objects as go
import numpy
import math
[2]:
slider = widgets.FloatRangeSlider(description = 'Range')
[3]:
omega = widgets.FloatSlider(description = 'Omega',value = 1)
[4]:
fig = go.FigureWidget()
fig.add_trace(go.Scatter(x=[],y=[]))
fig.update_layout(title = 'Hello Ipyflex')
fig.update_traces

f = lambda t: math.sin(t)
def compute(*ignore):
    min = slider.value[0]
    max =  slider.value[1]
    x = numpy.arange(min, max, (max-min)/100)
    y =  [f(omega.value* _) for _ in x]
    fig.data[0].x= x
    fig.data[0].y= y

slider.observe(compute, 'value')
omega.observe(compute, 'value')

[5]:
all_widgets =  {'A slider widget':slider, 'Output result': fig, 'Another slider': omega}
[6]:
w = FlexLayout(all_widgets, style={'height': '600px'}, template = './simple.json', editable=False)
w
[ ]:

[ ]:

Stock indexes example

[1]:
import numpy as np
import pandas as pd
from ipydatagrid import DataGrid, TextRenderer, BarRenderer, Expr
import ipydatagrid
import time
import plotly.graph_objects as go
from ipyflex import FlexLayout
[2]:
def create_graph(x, y, title, **kwargs):
    layout = go.Layout(
        autosize=True,
        height=300,
    )
    fig = go.FigureWidget(data=go.Scatter(x=x,y=y,fill='tozeroy'), layout=layout)
    return fig

def create_OHLC(data, title, **kwargs):
    layout = go.Layout(
        autosize=True,
        height=300,
    )
    fig = go.FigureWidget(data=go.Candlestick(x=data.index,
                        open=data['Open'],
                        high=data['High'],
                        low=data['Low'],
                        close=data['Close']), layout=layout)
    fig.update(layout_xaxis_rangeslider_visible=False)
    return fig

cotton_candy = {
    "header_background_color": "rgb(207, 212, 252, 1)",
    "header_grid_line_color": "rgb(0, 247, 181, 0.9)",
    "vertical_grid_line_color": "rgb(0, 247, 181, 0.3)",
    "horizontal_grid_line_color": "rgb(0, 247, 181, 0.3)",
    "selection_fill_color": "rgb(212, 245, 255, 0.3)",
    "selection_border_color": "rgb(78, 174, 212)",
    "header_selection_fill_color": "rgb(212, 255, 239, 0.3)",
    "header_selection_border_color": "rgb(252, 3, 115)",
    "cursor_fill_color": "rgb(186, 32, 186, 0.2)",
    "cursor_border_color": "rgb(191, 191, 78)",
}


def create_widget(data):
    widgets = {}
    for ticker, value in data.items():
        index = value.index
        _price = create_graph(index, value.Close,f'{ticker}',
                 labels=["Open","High", "Low", "Close"])
        _ohlc = create_OHLC(value, f'{ticker} OHLC')
        datagrid = DataGrid(value, base_row_size=32, base_column_size=150, layout={"height": "630px"})
        datagrid.grid_style = cotton_candy
        widgets[f'{ticker} price'] = _price
        widgets[f'{ticker} OHLC'] = _ohlc
        widgets[f'{ticker} Data'] = datagrid
    return widgets
[3]:
import yfinance as yahooFinance
tickers  = ['^IXIC', '^DJI', '^GSPC']
tickers_name = {'^IXIC':'NASDAQ Composite', '^DJI':'Dow Jones Industrial Average', '^GSPC':'S&P 500'}
data = {}
for ticker in tickers:
    info = yahooFinance.Ticker(ticker)
    name = tickers_name[ticker]
    data[name] = info.history(period="3mo")
[4]:
widgets = create_widget(data)
[5]:
a = FlexLayout(widgets, style={'height':'700px'}, template='stock.json', editable=False)
[6]:
a
[ ]:

Developer install

To install a developer version of ipyflex, you will first need to clone the repository:

git clone https://github.com/trungleduc/ipyflex
cd ipyflex

Create a dev environment:

conda create -n ipyflex-dev -c conda-forge nodejs yarn python jupyterlab
conda activate ipyflex-dev

Install the python. This will also build the TS package:

pip install -e ".[test, examples]"

When developing your extensions, you need to manually enable your extensions with the notebook / lab frontend. For lab, this is done by the command:

jupyter labextension develop --overwrite .
yarn run build

For classic notebook, you need to run:

jupyter nbextension install --sys-prefix --symlink --overwrite --py ipyflex
jupyter nbextension enable --sys-prefix --py ipyflex

Note that the –symlink flag doesn’t work on Windows, so you will here have to run the install command every time that you rebuild your extension. For certain installations you might also need another flag instead of –sys-prefix, but we won’t cover the meaning of those flags here.

How to see your changes

Typescript:

If you use JupyterLab to develop then you can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension’s source and automatically rebuild the widget:

# Watch the source directory in one terminal, automatically rebuilding when needed
yarn run watch
# Run JupyterLab in another terminal
jupyter lab

After a change wait for the build to finish and then refresh your browser and the changes should take effect.

Python:

If you make a change to the python code then you will need to restart the notebook kernel to have it take effect.