Widget API

Live provides an API to register new charts implementations.

Standard API

Depends on Live 2.27.0

Widgets service

The widgetsService is available in live/services/widgets. It is javascript module that allows registering custom widget implementation to Live run-time. Every widget must have a unique string to represent its type. In this tutorial example we choose my-simple-chart as shown below:

// Widgets service usage:
import i18n from 'live/services/i18n'
import widgetsService from 'live/services/widgets'

// The widget implementation
import SimpleChart from './my-simple-chart'
const type = 'my-simple-chart'

// Examples on: "Registering the widget" sections
widgetService.register(...)

Creating the Widget types

First we need to create types. If you do not have all definitions in mind at first, don't worry, create some placeholder types and enrich them at a later point.

types.ts
// 1. Typing the widget State shape:
// State will be the consolidated value derived
// from the events flow by the event reducer
// on this example we'll keep this simple shape:
type ChartState = {
    value: number
}

// 2. Typing data events
// The shape of these events in runtime
// are defined by the query of the widget

// a. Event shape
type ChartEvent = LiveEvent<{
    fieldA: number
    fieldB: number
}>

// b. Describe your message shape
type ChartMessage = FlowMessage<ChartEvent>
 
 
// 3. Typing the widget configuration:
type Configs = {
    widget: LiveWidget<{ color: string }>
}

Creating the Data Reducer

To get from events a consolidated current state we offer the abstraction of a data reducer. It is as simple as a switch-case statement that will always return an updated state value from each message received.

reducer.ts
const SimpleReducer: WidgetReducerType<ChartState, ChartMessage> = (state = getInitialState(), message) => {
    // test for initialState calls
    if (!message) {
        return state
    }

    if (message.baseline && message.type !== 'event') {
        return state
    }

    switch (message.type) {
        case 'start':
            return getInitialState()

        case 'event':
            return {
                value: message.events[0].fieldA + message.events[0].fieldB
            }
        default:
            return state
    }
}

On the reducer above we're getting fields fieldA and fieldB defined in ChartEvent to consolidate our state value on data events. All event types definitions.

Creating a pure Javascript Widget

Widget implementation

Create a simple class that implements ChartInterface

SimpleChart.ts
class SimpleChart implements ChartInterface<ChartState, ChartMessage, Configs> {}

After this, let the IDE or Code editor help you:

You should get something like this:

SimpleChart.ts
class SimpleChart implements ChartInterface<ChartState, MyMessage, ChartConfigs> {
    // Additionally: create custom instance properties
    cfg: ChartConfigs
    el: HTMLElement

    // Additionally: use the constructor to get needed values
    constructor({ options, el }: ChartConstructorOptions<ChartState, FlowMessage, ChartConfigs>) {
        this.cfg = options.cfg
        this.el = el
    }

    init(): void {
        this.el.innerHTML = 'Chart has been initialized'
    }

    render(state: ChartState, configs: Configs, size: Dimensions): void {
        this.el.innerHTML = `value is ${state.value}, size is ${size.width}x${size.height}`
        this.el.style.color = configs.widget.jsonConfig.color
    }

    destroy(): void {
        this.el.remove()
    }
}

That is all on the chart implementation.

Registering the widget

Wrap your widget with the widgetWrapper decorator

init.ts
import widgetService from 'live/services/widgets'
import { widgetWrapper } from 'live/widgets/decorators/pure'
import SimpleChart from './SimpleChart'


widgetService.register({
    type: 'SimpleChart',
    get name() {
        return i18n('Simple chart')
    },
    get description() {
        return i18n('This chart shows the number 42')
    },
    config: {
        color: 'green'
    },
    // the wrapper is needed to enable the reducer mechanism and comply the API
    impl: widgetWrapper<ChartState, ChartMessage, ChartConfigs>(SimpleChart, simpleReducer)
})

You are all set. A new chart implementation has been added to Live and can now be chosen on the Pipes widget editor.

Complete example

class SimpleChart implements ChartInterface<ChartState, MyMessage, ChartConfigs> {
    // Additionally: create custom instance properties
    cfg: ChartConfigs
    el: HTMLElement

    // Additionally: use the constructor to get needed values
    constructor({ options, el }: ChartConstructorOptions<ChartState, FlowMessage, ChartConfigs>) {
        this.cfg = options.cfg
        this.el = el
    }

    init(): void {
        this.el.innerHTML = 'Chart has been initialized'
    }

    render(state: ChartState, configs: Configs, size: Dimensions): void {
        this.el.innerHTML = `value is ${state.value}, size is ${size.width}x${size.height}`
        this.el.style.color = configs.widget.jsonConfig.color
    }

    destroy(): void {
        this.el.remove()
    }
}

Creating a React widget

Widget implementation

To create a React widget simply create a React component, like so:

// SimpleChart.tsx
const SimpleChart = (props: LiveReactChartProps<ChartState, ChartMessage, Configs>): JSX.Element => {
    const { state, width, height, widget } = props
    const { value } = state

    return (
        <div style={{ color: widget.jsonConfig.color }}>
            value is {value}, size is {width}x{height}
        </div>
    )
}

React widget heads up

If your component is a Memo remember to return a new state at the reducer when you want it to be updated, a shallow copy is enough.

React widget tip: split data and config components

By doing this you can restrict React updates to state and configs in different sub-trees.

eg: <ChartData data={state} /> and <ChartConfig config={widget.jsonConfig> width={width} height={height} />

Registering the widget

Wrap your widget with the reactWidgetWrapper decorator

// init.ts
import widgetService from 'live/services/widgets'
import { reactWidgetWrapper } from 'live/widgets/decorators/react'

widgetService.register({
    type: 'SimpleChart',
    get name() {
        return i18n('Simple chart')
    },
    get description() {
        return i18n('This chart shows the number 42')
    },
    config: {
        color: 'green'
    },
    // the wrapper is needed to enable the reducer mechanism and comply the API
    impl: reactWidgetWrapper<ChartState, ChartMessage, ChartConfigs>(SimpleChart, simpleReducer)
})

You are all set. A new chart implementation has been added to Live and can now be chosen on the Pipes widget editor.

Complete example

// SimpleChart.tsx
const SimpleChart = (props: LiveReactChartProps<ChartState, ChartMessage, Configs>): JSX.Element => {
    const { state, width, height, widget } = props
    const { value } = state

    return (
        <div style={{ color: widget.jsonConfig.color }}>
            value is {value}, size is {width}x{height}
        </div>
    )
}

Legacy API - deprecated

Deprecated since Live 2.27.0, this became an underneath API that shouldn't be used directly anymore. Prefer the Standard API to create new charts.

Live prior to 2.27 continues to support Legacy API as default API for chart creation. Some other helpers will be dropped soon also. For example, live/lib/react-widget is deprecated in favor of live/widgets/decorators/react.

app.js
import i18n from 'live/services/i18n'
import WidgetsService from 'live/services/widgets'

// The widget implementation
import TutorialWidget from './widget'

// required:
// registering widget name and description
WidgetsService.name('tutorial', i18n('tutorial widget title'))
WidgetsService.description('tutorial', i18n('tutorial widget description'))

// required:
// registering optional its default config, it also acts a config mask
// you should put every possible configuration here.
WidgetsService.config('tutorial', { textColor: 'green' })

// required:
// registering the implementation
WidgetsService.impl('tutorial', TutorialWidget)

// optional:
// configurations that changes the behavior
// of the default editor and also the widget rendering
WidgetsService.policies('tutorial', {})
WidgetsService.properties('tutorial', {})

Live provides a factory function in live/widgets/helpersto proper setup the prototype to use a function constructor for the Widget instance creation.

Live is implemented using ReactJS, however the Live Widget API doesn't require widgets developers to write a React Component. The Widget implementation must provide the following methods:

widget.js
import moment from 'moment'
import { extend } from 'lodash'
import { widgetFactory } from 'live/widgets/helpers'

const TutorialWidget = widgetFactory()

extend(TutorialWidget.prototype, {

    initialize() { }, // optional method
    destroy() { }, // optional method
    
    setEl(el) { return this },
    render() { return this },

    flow(type, events, layer, baseline, preventRedraw) {  },
})

module.exports = TutorialWidget

Widget.setEl

setEl(element) is called right after the DOM element creation for the widget container. The elementparameter contains the divthat acts as container and the developer could save the elementreference at its instance for future usage. This method should return the current widget instance.

setEl(el: HTMLElement) {
    this.el = el

    return this
}

Widget.render

render() is called once at very first rendering of the Widget (be careful to avoid misunderstanding with render behavior of the React Components). This method is executed as a initialization phase at Widget Life-Cycle, it is commonly used to get the DOM ready for the events arrival. Besides the elementcontainer also receives a CSS class named using the -widget suffix over the widget type string, in our example: tutorial-widget. So the developer will be able to customize the container as necessary and append child elements as well, for example:

render() {
    this.el.insertAdjacentHTML('beforeend', '<span class="value"> - </span>')
    this.el.insertAdjacentHTML('beforeend', '<span class="date"> - </span>')
    this.el.insertAdjacentHTML('beforeend', '<span class="control">waiting</span>')

    // Widget implementation MUST call the `onReady` function.
    if (typeof this.options.onReady == "function")
        this.options.onReady.apply(this)

    return this
}

The `render` return types

Object with defined `options` property

{ options: LiveWidgetImplOptions }

If the render function returns a object with options it tells Live that the widget handles its own rendering process. Then you must handle by your own any dynamic change to your own properties. Of course, in event flow method (as we describe next) you can handle how to change anything in your widget since new events arrive.

The widgetFactory constructor assigns a default empty object to options. Keep that in mind if your render method returns this.

TheLiveWidgetImplOptions type is described in Live Widget Configuration section.

Function: considered to be React Component

If the render function returns a Function, the return value is considered to be a React Component. Keep in mind this function definition must not define options property. The component will be created like so:

<WidgetComponent
    mode={this.props.mode} // the dashboard mode or mode 'editor'
    event={this.state.event} // the last event
    batch={this.state.batch} // the last event batch
    widget={widget} // the widget configuration
    height={this.state.height} // current height
    width={this.state.width} // current width
/>

Object with `portal` property: property considered to be a React Component

The portal property is also considered to be a React Component. It differs from returning a function because this PortalComponent will be rendered as a sibling of the widget's HTMLElement reference element.

The `onReady` function

The onReady function received on the Widget's constructor and assigned to this.options.onReady is a function that as soon as it is called it flags the widget as ready to receive data. That said, calling this.options.onReady is mandatory.

Widget.flow

flow(type, events, layer, baseline, preventRedraw) is called as soon as onReady Promise is resolved and will be executed every time the new data event or event control arrives. In strict sense of a Pipes Widgets, this is the most important method in widget implementation. It should be faster, be careful to avoid memory consumption and any delays on it will result in lower event processing throughput.

The event type string describes what kind of events is coming. Live organizes them in two categories data eventsandcontrol events. For further understanding what event exists and how they like, see Live Events Types section.

The batch of events is an array of objects and its format depends on Pipes Expression (see start event at Live Event Types). If only single events are emitted each time, events.length will be always 1. But Pipes has support to emit at every N items collected, so the event could arrive in batch mode.

Live Widget could have separated layersto execute different Pipes queries at the same time (the widget configuration could choose if multiple layers are supported). The layersparameter is a number with the layer index. The baseline and preventRedraw parameters are boolean.

An illustrative example how implement the flow function is given:

flow(type, events, layer, baseline, preventRedraw) {
    switch (type) {
        // data events
        case 'event':
            const liveevent = events[0] // we take only one but it could be a batch
            const timestamp = liveevent.timestamp / 1000
            const event2str = JSON.stringify(liveevent, null, 2)
            const eventdate = 'event emitted in ' + moment.unix(timestamp).toString()
            this.el.querySelector('.value').innerHTML = event2str
            this.el.querySelector('.date').innerHTML = eventdate
            break
        // control events
        case 'start':
            this.el.querySelector('.control').innerHTML = 'starting...'
            break
        case 'warning':
            this.el.querySelector('.control').innerHTML = events.message
            break
        case 'endHistory':
            this.el.querySelector('.control').innerHTML = 'query duration was ' + events.duration + 'ms !'
            break
        default:
            break
    }
    // debugging only event controls
    if (type !== 'event')
        console.debug('event control "' + type + '" has arrived ', events)
}

The initialize and destroy methods are optional. Initialize is called at instantiation time and destroy is the last call before the Live removes the container from DOM.

The screenshot below shown an example of a chart instance of Tutorial Pipes Widget using the Pipes Expression => random() every 5 sec that produces only real-time events that spawn every 5 seconds.

Widget Life Cycle

Live Widget instance is created at very first time when the user click in widget name at side bar of widget editor (as shown above). But at this point the widget isn't part of a dashboard yet, the user must save it. After saved, now the widget gains an unique ID in dashboard context and will be instantiate every time the dashboard be open (either in edit mode or view mode). Internally a Live Widget instance is named Chart.

Some basic examples of properties of a widget instance are shown below:

  • this.options.id : number that identifies the widget in dashboard

  • this.options.name : string that identifies the widget name in dashboard

  • this.options.mode : string that indicates one of:

    • editorwhen widget itself is being edited

    • edit when dashboard is being edited

    • viewwhen dashboard is opened in view mode

    • fullscreenwhen dashboard is opened in fullscreen mode

The Live Widget Configuration section presents the complete reference of the widget configuration type.

Appendix: complete example files

See the complete example files discussed in the previous headings. You could create a webappdirectory, put all the following files on it and read the Live Widget Packaging section to learn how to build a single bundle.js using Webpack.

import i18n from 'live/services/i18n'
import WidgetsService from 'live/services/widgets'
import TutorialWidget from './widget'

try {
    document.addEventListener('DOMContentLoaded', function () {
        require('./init')
    })
    
    // required:
    // registering widget name and description
    WidgetsService.name('tutorial', i18n('tutorial widget title'))
    WidgetsService.description('tutorial', i18n('tutorial widget description'))
    
    // required:
    // registering optional its default config, it also acts a config mask
    // you should put every possible configuration here.
    WidgetsService.config('tutorial', { textColor: 'green' })
    
    // required:
    // registering the implementation
    WidgetsService.impl('tutorial', TutorialWidget)
} catch (e) {
    console.error(e)
}

require('../scss/index.scss')

Last updated