# Building New Visualizations (Widgets) for Sisense

In this tutorial, you will build an Add-on that adds a new type of visualization that can be used in dashboards.

TIP

This guide assumes you're already familiar with the general structure of Sisense Add-ons, as described here.

It is also highly recommended to visit the JavaScript API Reference where you can find detailed descriptions of the various methods and events referenced in this page.

TIP

This tutorial covers the basics of Sisense widgets. Building new widgets for Sisense Mobile sometimes requires a slightly different process, which is described here.

# Step 1: Creating an empty Add-on

Follow the same steps described here. Name your Add-on simpleTable (both the folder and within plugin.json). You can skip the "additional code files" step for now.

You should now have the following files:

  • plugin.json
  • main.6.js

Now, download/copy this file to your Add-on's folder. You will use this module to render your data as a simple HTML table. It is a stand-in for the more complex 3rd-party visualization modules you would probably use for a real widget, such as various d3.js or Highcharts visualizations.

Replace the contents of main.6.js with this code:

import simpleTable from `./simpleTable.6`;

prism.registerWidget('simpleTable', {
    // This will be the widget manifest
});

The empty object passed as the 2nd argument to registerWidget is the widget manifest - a JSON object describing the widget type, containing the information Sisense needs to display and store your visualization. Most of the work in developing new visualizations for Sisense happens within this object.

# Step 2: Creating the widget manifest

In this step, you will start creating the widget manifest object, by defining the simpler, static properties it requires. In the following steps, you will add the more complex parts needed for a complete widget.

# Defining basic properties

There are several simple properties that every widget type has, which determine how it's represented in the new widget wizard or widget editor UI. Add these properties to the widget manifest:

  • name: A unique name for your widget type. May be identical to your Add-on name, but doesn't have to be.
  • family: What type of widget this is - use the value table
  • title: A title to display in the UI
  • iconSmall: A root-relative path to a .png file containing an icon to represent the widget type

Your widget manifest should now look like this:

prism.registerWidget('simpleTable', {
    name: 'simpleTable',
    family: 'table',
    title: 'Simple Table',
    iconSmall: '/plugins/simpleTable/widget-24.png',
});

# Defining default sizing

Another property of the widget manifest is sizing which defines some restrictions on how the widget is positioned and scaled in the dashboard layout.

These settings will prevent the user from stretching or squeezing the widget beyond the limits of its usability.

{
    sizing: {
        minHeight: 100,
        maxHeight: 1000,
        minWidth: 100,
        maxWidth: 1000
    }
}

# Step 3: Widget data

For the widget to be able to retrieve and display data, you need to define how the widget interacts with metadata - which panels can be populated by the user, how the query is built and how results are processed.

# Defining the data panels

Each widget type requires different types and amounts of fields in the data set it can render. When building a widget, you need to define which metadata panels appear in the widget editor UI. You will use the metadata items chosen by the user in each panel to construct your widget's query in a later step.

First, add a data property to your widget manifest, and within it add an array called panels:

{
    data: {
        panels: []
    }
}

Each item in the panels array will generate a metadata panel in the widget editor UI.

There are many optional features of the metadata panel object, which cannot all be covered in this tutorial. In this example, you will add 3 simple panels to your Add-on:

  1. A "Categories" panel allowing for 1 dimension item
  2. A "Values" panel allowing for 1 measure item
  3. A "Filters" panel allowing for unlimited widget filters

Add the following items to the panels array:

[
    {
        name: 'Category',
        type: 'visible',
        metadata: {
            types: ['dimensions'],
            maxitems: 1
        }
    },
    {
        name: 'Value',
        type: 'visible',
        metadata: {
            types: ['measures'],
            maxitems: 1
        }
    },
    {
        name: 'filters',
        type: 'filters',
        metadata: {
            types: ['dimensions'],
            maxitems: -1
        }
    }
]

Note that all metadata panels are configurable except for the filters panel. You will copy & paste this exact panel object to every widget you build.

# Building the query

When a fresh query needs to run (for example, when dashboard filters are changed) Sisense will invoke your widget's buildQuery method, passing in a blank JAQL query object and expecting the method to return a fully formed JAQL query.

You only need to take care of your widget's metadata - Sisense will append dashboard filters automatically.

Add a method called buildQuery to your data object, like so:

{
    data: {
        buildQuery: (widget, query) => {

            widget.metadata.panel('Category').items.forEach((item) => {
                query.metadata.push($$.object.clone(item, true));
            });

            widget.metadata.panel('Value').items.forEach((item) => {
                query.metadata.push($$.object.clone(item, true));
            });

            widget.metadata.panel('filters').items.forEach((item) => {
                const itemClone = $$.object.clone(item, true);
                itemClone.panel = 'scope';
                query.metadata.push(itemClone);
            });

            return query;
        }
    }
}

# Handling transitions between widgets

When the user changes a widget's type, ideally as much of the existing metadata selected by the user should be preserved and used for the new widget. However, since every widget type has different requirements, this can't be achieved automatically - every widget type needs to define how it handles incoming metadata items from another widget type.

When the user switches to your widget type in the widget editor, Sisense will invoke the populateMetadata method of your widget, which is defined as part of the widget manifest's data object.

Sisense provides a helper service, prism.$jaql to break down the metadata items into different categories. You can read more about this service here.

For this example, you will simply add dimension items to your "Categories" panel, measures to the "Values" panel and filters to the "Filters" panel:

{
    data: {
        populateMetadata: (widget, items) => {
            const breakdown = prism.$jaql.analyze(items);
            widget.metadata.panel('Category').push(breakdown.dimensions);
            widget.metadata.panel('Value').push(breakdown.measures);
            widget.metadata.panel('filters').push(breakdown.filters);
        }
    }
}

# Step 4: Adding the style panel

Many widget types need to have some configuration, usually for the visual aspects - for example, to let the dashboard designer choose a line chart's line width, or where the widget's legend is displayed.

These settings are usually accessible via the style panel on the right side of the widget editor. This panel's content is defined by each widget type, and in this example we'll add the ability to define a custom label that will be rendered in the widget in the next step.

First, create a blank HTML file in your Add-on's directory. This will be the template for the style panel's UI. In this example, we'll call the file style-panel-template.html.

Now, specify this file in the widget manifest via the styleEditorTemplate property:

{
    styleEditorTemplate: '/plugins/simpleTable/style-panel-template.html'
}

Next, you need to define where this label's text will be stored within the widget instance object. The widget manifest's style property is meant exactly for that, and allows you to set default values for these settings.

Add this property to your widget manifest:

{
    style: {
        label: ''
    }
}

You also need to build a controller for this UI, which will handle changes to it and ensure they are reflected in the widget.

Create a file called style-panel-controller.6.js in your Add-on's folder and paste the following code in it:

// AngularJS controller for the style panel
const controller = ['$scope', ($scope) => {
    // Watch for style property changes to redraw the widget
    $scope.$watch('widget.style.label', () => {
        $scope.$root.widget.redraw();
    });
}];

module.exports = controller;

Then import it in your main.6.js file:

import controllerDefinition from './style-panel-controller.6';

mod.controller('stylerController', controllerDefinition);

The last step is to build the template and use all of the above. Paste this HTML code in the template file you previously created:

<div data-ng-controller="plugin-simpleTable.controllers.stylerController">
	<div class="style-control">
		<label>Custom Label:</label>
		<input type="text" data-ng-model="widget.style.label" />
	</div>
</div>

As you can see, the input DOM element is bound to widget.style.label which means this field will be updated whenever the user changes the text. This will trigger an event in the controller (which is attached to the template in the first line) which in turn will cause the widget to re-draw.

# Step 5: Rendering the widget

Finally, having defined how your widget interacts with the Sisense application, it is time to write the code which will render the widget, using the sample simpleTable component imported in step #1.

Add a method called render to your widget manifest:

{
    render: (widget, args) => {

        const widgetElement = $(args.element)[0];
        const widgetObjectID = widget.oid;
        const data = widget.queryResult;
        const responseMetadata = widget.rawQueryResult;

        // Clear widget element
        widgetElement.innerHTML = '';

        // Render the table
        let tableElement = simpleTable(responseMetadata.headers, data);
        widgetElement.appendChild(tableElement);

        // Render the custom label
        const settingsContainer = document.createElement('div');
        settingsContainer.innerText = widget.style.label;
        widgetElement.appendChild(settingsContainer);
    }
}