Support

Support

  • Olympe Website

›Tutorials

Olympe support overview

  • Olympe Support
  • Olympe Training Plan

DRAW

    Getting Started

    • Introduction
    • Content Organization
    • User Interface - Main concepts
    • User Interface - Functions
    • User Interface - Custom Visual Components
    • User Interface - Interactions
    • User Interface - Navigation
    • Data - Data Models
    • Data - Data Sets
    • Logic - Functions vs Actions
    • Create and modify data
    • Query Data
    • Transition with data
    • Generate Rest API connectors from OpenAPI files
    • Themes
    • Project dependencies
    • Export & Import

    Tutorials

    • Simple App Example
    • Expense App Tutorial

CODE

  • CODE API
  • Tutorials

    • Bootstrap a CODE development environment
    • Initialize the versioning system
    • Extend DRAW with custom logic bricks made with CODE
    • Extend DRAW with custom visual bricks made with CODE
    • Retrieve data from Olympe datacloud, transform it and display it in a custom Visual Component
    • Modifying the data on the data cloud
    • Visualizing participants interaction with a graph

    Further reading

    • A Beginner's Guide to Dataflows

Platform

  • VM Supported Hosts

Release notes

  • DRAW
  • CODE
  • Orchestrator

Demos

  • Charts Example
  • Workflows Management

Extend DRAW with custom visual bricks made with CODE

Introduction

The Olympe DRAW visual development platform can be extended through the creation of custom visual components. This tutorial focuses on the creation of an hardcoded visual component that can later be used and replicated in DRAW for UI construction.

Prerequisites

It is assumed that the following tutorials have been completed already:

  • How to bootstrap an Olympe project with hardcoded parts.
  • How to implement a Celsius to Fahrenheit converter function

In particular, we use the tutorials project created earlier.

Outcome

At the end of this tutorial, you will have a bar chart implementation:

outcome

How to implement an hardcoded bar chart

Overview of the steps required

The creation of an hardcoded visual component, similarly to an hardcoded functional component, involves the followings steps:

  • Designate the parameters the visual component will be sensible to
  • Define the visual component in the softcoded world
  • Implement the rendering and UX of the component in the hardcoded world.

Figuring out the Bar Chart properties

Let's assume the following specifications for our "Bar Chart" component:

  • Numerical values in the CSV (comma separated values) format are taken as input
  • The chart should have a configurable title
  • The bar color should be configurable

We establish thus the following list of properties (and their types) of our component:

  • CSV (String)
  • Title (String)
  • BarColor (Color)

Similarly to the Celsius-to-Fahrenheit function, we thus need to generate at least one tag (you'll see why not necessarily 4 below) using the helper:

olympe.utils.generateUniqueTag()

Creating the barchart module

We implement the bar chart in a new module that we name barchart.

The involved steps to create this module is similar to the one we created for the Celsius-to-Fahrenheit function:

  • We create a barchart directory inside modules
  • We create a src and an initscripts folders within barchart
  • We provide a _ns.js file (contained in src) that declares the barchart namespace:
// barchart/src/_ns.js
/**
 *
 * @namespace
 */
const barchart = {};
  • We create a BarChartImpl.js file in the src directory as well (which is for now empty)
  • We create an init script BarChart.js in the initscripts folder, containing the following four lines:
// barchart/initscript/BarChart.js
db = /** @type {composer.script.OSaveFileBuilder} */(db);
db.setDefaultGroupOfObjects('10000000000000000GOO');
db.newRepository('0170ef75108629bf0000', 'Custom visual component repository', db.ROOT_REPO_DEFAULT).done();
db.setWorkingRepository('0170ef75108629bf0000');
  • We create a module.json inside the barchart directory with the following content:
{
  "name": "barchart",
  "version": "0.0.1",
  "sources": [
    "src/_ns.js",
    "src/BarChartImpl.js"
  ],
  "datacloudinit": {
    "defaultDomain": "bricks.olympe.ch",
    "initScriptDirectories": [
      "initscripts/"
    ]
  }
}

We should obtain the following project structure (omitting the helloworld and c2f modules):

tutorial
├── modules
│   └── barchart
│       ├── module.json
│       ├── initscripts
│       │   └── BarChart.js
│       └── src
│           ├── BarChartImpl.js
│           └── _ns.js
├── package-lock.json
├── package.json
├── gulpfile.js
├── module.json
├── snapshot.sh
└── node_modules/

We should also modify the root module.json to indicate that we include a new module. It should now look as follows:

{
  "name": "tutorials",
  "version": "0.0.1",
  "dependencies": {
    "olympe-composer.targets": "*"
  },
  "targets": {
    "html": {
      "dependencies": {
        "olympe-composer": "*",
        "olympe.native.html.target.default": "*",
        "helloworld": "*",
        "c2f": "*",
        "barchart": "*"
      }
    }
  }
}

Writing the "SC" definition of the visual component

Now we can write the softcoded definition of the visual component by considering initscripts/BarChart.js. We start the definition of our component, this time by calling db.newModel(). Unfortunately, a dedicated helper similar to db.newFunction() for functions is not available yet for component. This means we will have to perform a couple of operation manually... fortunately, that will be also the opportunity to unveil a bit how data are structured in Olympe (the full blown explanation is kept for another tutorial).

Creating a model

By calling db.newModel(), we instruct the OSaveFileBuilder to initiate the definition of a model. Why a model ? Because this model will be instantiated each time a bar chart will be dragged onto a screen in draw. As usual, if your linting configuration works correctly, you should receive a hint from e.g. WebStorm about parameters. newModel() takes a tag as first argument (this is a convention in init scripts), and a name as second.

Making the model an extension of another

After the call to newModel, we need to indicate that we want this model to extend another existing model called Component. For that, we need to locate the tag of this Component. We find it in the file modules/olympe/sc/modules/ui/datamodel/01_abstract_models.js (in the near future, this tag will be available as a constant - db.COMPONENT). We copy the definition of its tag value and paste it in our init script:

// barchart/initscript/BarChart.js
const Component = '01612df263119ee77622';

db.newModel('0170f4619348dfd0c935', 'Bar Chart')
    .extends(Component)

through this extension, the instance of our model will (among other things) inherit all the standard properties define in Component, as the height, width, border, etc.

Designating our model as an instance of RenderableDefinition

Now, since our bar chart model itself needs to inherit properties, we need to designate it as instance of a specific model called RenderableDefinition. This operation is done through the helper setModelTag():

// barchart/initscript/BarChart.js
const Component = '01612df263119ee77622';
const RenderableDefinition = '01612df1f810c91e8997';

db.newModel('0170f4619348dfd0c935', 'Bar Chart')
    .extends(Component)
    .setModelTag(RenderableDefinition)

Now, I see you telling "hey, dude, isn't it kinda weird to have to set the model of a model?" Don't worry, this is absolutely normal (to find this weird). As a matter of fact, in the Olympe world, all models have a model. This (generally confusing) oddity will be explained in another tutorial. For now, just consider the gist of it: our bar chart model can be considered as a template. Now, isn't it totally acceptable to consider this template as an instance of a class Template? This is what RenderableDefinition is: the mother class of all templates of renderable components. In the Olympe jargon, such class Template are called Definitions.

Defining properties (themable or not)

Now that our model is correctly tied to existing elements, we can start defining its properties. This is done through either

  • defineThemableProperty: for properties that can typically be determined by a theme. In our example, this will typically be the BarColor property. This will let us define classes of BarCharts in DRAW's theme editor, with different bar colors.
  • defineProperty: for properties not really themable, typically the values the bar chart should display.

These both methods share the same signature:

  • the tag of the property
  • the type of the property
  • the name of the property
  • an optional default value.

Our model definition thus becomes:

// barchart/initscript/BarChart.js
db.newModel('0170f4619348dfd0c935', 'Bar Chart')
    .extends(Component).setModelTag(RenderableDefinition)
    .defineThemableProperty('0170f477486cc0d10315', db.COLOR, 'Bar color', db.createColor(100, 100, 100, 1))
    .defineProperty('0170f477486cc0d10316', db.STRING, 'Chart title')
    .defineProperty('0170f477486cc0d10317', db.STRING, 'CSV')
    .done();

Observe the following in the above code:

  • The tags are very similar ! Yes, it is permitted to cheat sometimes and just increment the first one (but in this usage only)
  • To specify a default color, we use the db.createColor() helper.

Registering the properties in DRAW

Unfortunately, we are not done yet with SC definitions. We still need to register the properties we want to expose in DRAW in the properties editor. As a reminder, the property editor is this part of the interface:

propertyEditor

To perform the later action, we need to find the reference to the section under which each property should be listed (for instance, under the "Value" section of the "Custom properties tab" as shown on the figure above). As a convention, properties of custom components should be placed under this "Custom properties tab", which has itself two sections of interests for us:

  • the "Value" section, which has tag 0161bd31111c6b32e43d
  • the "Custom properties" section, which has tag 0164188f5cc0c205bc06

We typically want to register the CSV property under "Value", and the two other properties under "Custom properties". This registration goes as follow:

// barchart/initscript/BarChart.js
const barColorProperty = '0170f477486cc0d10315';
const chartTitleProperty = '0170f477486cc0d10316';
const CSVProperty = '0170f477486cc0d10317';

db.newModel('0170f4619348dfd0c935', 'Bar Chart')
    .extends(Component).setModelTag(RenderableDefinition)
    .defineThemableProperty(barColorProperty, db.COLOR, 'Bar color', db.createColor(100, 100, 100, 1))
    .defineProperty(chartTitleProperty, db.STRING, 'Chart title')
    .defineProperty(CSVProperty, db.STRING, 'CSV')
    .done();

const multiLineTextInlineEditorInstance = '016cfca73e63f9ab5ba6';
const valueSection = '0161bd31111c6b32e43d';
const customSection = '0164188f5cc0c205bc06';
db.newComposerProperty('0170f477486cc0d10319', 'Chart values', valueSection, CSVProperty, multiLineTextInlineEditorInstance)
    .setRank(1)
    .done();

Observe that:

  • registering a property actually means creating something called a ComposerProperty. Any new such element must be created with a tag.
  • we cheated again and incremented the tag of the first property to obtain the tag of the composer property.
  • since the tag of the CSV property is used twice (once at the creation, once as a reference), it is a good practice to declare it into a const.
  • A rank can be designated for ordering within the section.
  • with the last argument we prescribe what kind of editor will be associated to the property. For your information, here's a list of the most common inline editors (this is how we call them):
const stringInlineEditorInstance = '01611e3015371b459cf7';
const numberInlineEditorInstance = '01611e3015371b459cf5';
const booleanInlineEditorInstance = '01616a7f404d2d428a35';
const colorInlineEditorInstance = '01616aa63574678f7c45';
const fontFamilyInlineEditorInstance = '016d258c26b7a9fcffc7';
const multiLineTextInlineEditorInstance = '016cfca73e63f9ab5ba6';
const stringEnumInlineEditorInstance = '016c286b15a32cd8ce4b';

Note finally that we aim at simplifying a great deal this property registration process, which nowadays is quite cumbersome and error prone.

Exercice : registering the two other properties

To practice a bit, the registration of the two other properties is let up to you.

Solution We just need to add the following lines:

// barchart/initscript/BarChart.js
const stringInlineEditorInstance = '01611e3015371b459cf7';
const colorInlineEditorInstance = '01616aa63574678f7c45';

db.newComposerProperty('0170f477486cc0d10320', 'Chart title', customSection, chartTitleProperty, stringInlineEditorInstance)
    .setRank(2)
    .done();
db.newComposerProperty('0170f477486cc0d10321', 'Bar color', customSection, barColorProperty, colorInlineEditorInstance)
    .setRank(3)
    .done();

The initscript/BarChart.js script should now look as follows:

// barchart/initscript/BarChart.js
db = /** @type {composer.script.OSaveFileBuilder} */(db);
db.setDefaultGroupOfObjects('10000000000000000GOO');
db.newRepository('0170ef75108629bf0000', 'Custom visual component repository', db.ROOT_REPO_DEFAULT).done();
db.setWorkingRepository('0170ef75108629bf0000');

const Component = '01612df263119ee77622';
const RenderableDefinition = '01612df1f810c91e8997';
const multiLineTextInlineEditorInstance = '016cfca73e63f9ab5ba6';
const stringInlineEditorInstance = '01611e3015371b459cf7';
const colorInlineEditorInstance = '01616aa63574678f7c45';
const valueSection = '0161bd31111c6b32e43d';
const customSection = '0164188f5cc0c205bc06';

const barColorProperty = '0170f477486cc0d10315';
const chartTitleProperty = '0170f477486cc0d10316';
const CSVProperty = '0170f477486cc0d10317';

db.newModel('0170f4619348dfd0c935', 'Bar Chart')
    .extends(Component)
    .setModelTag(RenderableDefinition)
    .defineThemableProperty(barColorProperty, db.COLOR, 'Bar color', db.createColor(100, 100, 100, 1))
    .defineProperty(chartTitleProperty, db.STRING, 'Chart title')
    .defineProperty(CSVProperty, db.STRING, 'CSV')
    .done();

db.newComposerProperty('0170f477486cc0d10320', 'Chart title', customSection, chartTitleProperty, stringInlineEditorInstance)
    .setRank(1)
    .done();
db.newComposerProperty('0170f477486cc0d10321', 'Bar color', customSection, barColorProperty, colorInlineEditorInstance)
    .setRank(2)
    .done();
db.newComposerProperty('0170f477486cc0d10319', 'Chart values', valueSection, CSVProperty, multiLineTextInlineEditorInstance)
    .setRank(3)
    .done();

Registering the component within the Visual editor

Unlike functions that are automatically made available in the function editor, custom visual component must be manually associated with the visual editor (the DRAW editor used for screens and visual components). For that, we need to add the following lines:

// barchart/initscript/BarChart.js
const marketPlaceItemRel = '01611d3c5cfb4e80d64d';
const visualSubEditorInstance = '0161d6e0551951d18bc9';
db.newRelation(marketPlaceItemRel, visualSubEditorInstance, '0170f4619348dfd0c935');

Notice that in the above code, we perform the most rudimental operation: we add a link (of type marketPlaceItemRel) between two nodes in the database.

Re-initializing the datacloud to incorporate our new definition

It is now time to call gulp rst once again. Once the reset process is over, you can open DRAW and check if you see the bar chart component in the marketplace of the visual editor . If it is there as expected, try to drag one onto the screen. This should provoke an error as the implementation is missing.

barchart_noimpl

Implementing the bar chart

Writing the boilerplate

Let's now implement the rendering of the bar chart in src/BarChartImpl.js

In this file, we start by writing (or copy-pasting from this tutorial - it is allowed) the following boilerplate:

// barchart/src/BarChartImpl.js
/**
 * @extends {olympe.sc.ui.Component}
 */
barchart.BarChart = class BarChart extends olympe.sc.ui.Component {
    /**
     * @override
     */
    render(context) {
        const dimension = this.getDimension(context);
        const layout = new olympe.ui.std.AbsoluteLayout(dimension);
        this.initCommonProperties(layout, context);
        layout.setBackgroundColor(olympe.df.Color.darkGray());
        return layout;
    }
};

Note that our class BarChart extends from olympe.sc.ui.Component. In that we follow what we wrote in our SC definition.

For now we simply create an empty AbsoluteLayout painted in gray, sized to the dimension of the component, and we return it.

Linking this implementation with the SC definition

As it was the case for the CelsiusToFahrenheit function, we need to establish a link between this implementation and our SC definition. This time we use the olympe.dc.Registry:

// barchart/src/BarChartImpl.js
barchart.BarChart.entry = olympe.dc.Registry.registerSync('0170f4619348dfd0c935', barchart.BarChart);

Testing if the link is established

Refresh DRAW and check if the error you had before is still there. You should see now a dark gray rectangle on your screen.

barchart0

Linking the properties

Before going any further in the implementation, we also need to link the properties between the SC and HC world. Here, contrarily to functions, we do not use a method chaining mechanism. Instead, we use the "entry" that was returned by the registerSync() method (that we kept under barchart.BarChart.entry and add properties on it:

// barchart/src/BarChartImpl.js
barchart.BarChart.csvProp = barchart.BarChart.entry.addProperty('0170f477486cc0d10317');
barchart.BarChart.titleProp = barchart.BarChart.entry.addProperty('0170f477486cc0d10316');
barchart.BarChart.colorProp = barchart.BarChart.entry.addProperty('0170f477486cc0d10315');

Implementing the bar chart

This time, we are ready to write the implementation itself. As a first step, we should collect the value of the csvProp. This can be done by calling this.getProperty() inside the render method:

// barchart/src/BarChartImpl.js
        const csvValue = this.getProperty(barchart.BarChart.csvProp, context, olympe.df.OString);

The getProperty method takes a parameter of type olympe.dc.InstanceTag as first argument that should designate which property one want to retrieve. The olympe.dc.InstanceTag type is the union of about everything that can be or carry a tag, be it a string as in the present case, or an instance. The second argument is the context, that we received as parameter in the render method (we can ignore it for now). Finally, the third argument is the type of the property. Since our values will be given as a string, we put here olympe.df.OString.

It is important to remark that this.getProperty will return a Proxy, since the property of the bar chart that we render can be modified over time (also remotely).

Therefore, our next operation will consist in describing a processing node. Hence, we want typically the bar chart to be repainted each time the csv values are updated.

// barchart/src/BarChartImpl.js
        olympe.df.processFlows([csvValue], (val) => {

        });

Then, inside the processing function of the process function, we can

  • transform the updated value of the csv property, val, into a string by calling val.valueOf()
  • split this (JavaScript) string using commas as separator
  • iterate over the so obtained values array and, for each value:
    • create a rectangle of width 20 and height proportional to the i-th value (scaled by a factor 10)
    • set the background color of this rectangle to blue (for now)
    • add this rectangle to the layout, at a position depending on its index.
// barchart/src/BarChartImpl.js
            const valuesArray = val.valueOf().split(',');
            for (let i=0, l=valuesArray.length ; i < l ; i++) {
                const rectangle = new olympe.ui.std.Rectangle(new olympe.df.Vector2(20, valuesArray[i]*10));
                rectangle.setBackgroundColor(olympe.df.Color.blue());
                layout.appendChild(rectangle, 'rect' + i, new olympe.df.Vector2(i*30, 0));
            }

Finally, we can remove the gray background color as its purpose was to check if something was getting rendered:

// barchart/src/BarChartImpl.js

// remove the following line
layout.setBackgroundColor(olympe.df.Color.darkGray());

Full code for BarChartImpl.js

// barchart/src/BarChartImpl.js
/**
 * @extends {olympe.sc.ui.Component}
 */
barchart.BarChart = class BarChart extends olympe.sc.ui.Component {
    /**
     * @override
     */
    render(context) {
        const dimension = this.getDimension(context);
        const layout = new olympe.ui.std.AbsoluteLayout(dimension);
        this.initCommonProperties(layout, context);
        const csvValue = this.getProperty(barchart.BarChart.csvProp, context, olympe.df.OString);
        olympe.df.processFlows([csvValue], (val) => {
            const valuesArray = val.valueOf().split(',');
            for (let i=0, l=valuesArray.length ; i < l ; i++) {
                const rectangle = new olympe.ui.std.Rectangle(new olympe.df.Vector2(20, valuesArray[i]*10));
                rectangle.setBackgroundColor(olympe.df.Color.blue());
                layout.appendChild(rectangle, 'rect' + i, new olympe.df.Vector2(i*30, 0));
            }
        });
        return layout;
    }
};

barchart.BarChart.entry =
    olympe.dc.Registry.registerSync('0170f4619348dfd0c935', barchart.BarChart);

barchart.BarChart.csvProp = barchart.BarChart.entry.addProperty('0170f477486cc0d10317');
barchart.BarChart.titleProp = barchart.BarChart.entry.addProperty('0170f477486cc0d10316');
barchart.BarChart.colorProp = barchart.BarChart.entry.addProperty('0170f477486cc0d10315');

Putting all these instructions together, and by specifying some values in the "Custom properties" tab, the following result is expected:

barchart1

Exercise: improving this rudimental bar chart

We can improve the bar chart by:

  • Making the bars "grow upwards".
  • Applying the color property to the bars.
  • Displaying the chart title.

Solution

Inverting the bars

To fix the direction of the bars, we need to set the Y position of each bar to the layout height minus the height of the bar.

A first attempt may look as follow:

// barchart/src/BarChartImpl.js
layout.appendChild(
    rectangle,
    'rect' + i,
    new olympe.df.Vector2(
        i*30,
        layout.getHeight() - valuesArray[i]*10 // <-- line of interest
    ));

However, layout.getHeight() returns a Proxy because this property may change over time. Since this Proxy object encapsulates a numerical value, it provides the usual mathematical operations through methods. For the subtraction, the associated method is minus().

The line of interest then becomes:

// barchart/src/BarChartImpl.js
layout.getHeight().minus(valuesArray[i]*10)

When the layout changes (which can be due to the user resizing the bar chart), the Y position of the bars are automatically updated.

Bars color

We just need to retrieve the color property through this.getProperty(), as we have done earlier to fetch the CSV values:

// barchart/src/BarChartImpl.js
rectangle.setBackgroundColor(
    this.getProperty(barchart.BarChart.colorProp, context, olympe.df.Color)
);

Chart title

For this part, we need to:

  • Create a label
  • Set its text to the value contained in the barchart.BarChart.titleProp property
  • Append it to layout and specify its position.

For instance, if we would like to have the title to be centered, we can add the following lines to render, after having created the layout object:

// barchart/src/BarChartImpl.js
const chartTitle = new olympe.ui.std.Label();
chartTitle.setText(
    this.getProperty(barchart.BarChart.titleProp, context, olympe.df.OString)
);
chartTitle.setTextHorizontalAlign(olympe.ui.common.HorizontalAlign.CENTER);
layout.appendChild(
    chartTitle,
    'charttitle',
    new olympe.df.Vector2(
        // We center the title by computing the appropriate X coordinate.
        // We note that layout.getWidth() and chartTitle.getWidth() return
        // a Proxy to denote the fact that the values may change over time.
        // The position will be automatically recalculated if at least one
        // of the layout or chart title changes.
        layout.getWidth()
            .minus(chartTitle.getWidth())
            .div(2),
        0
    )
);

Result

barchart2

Full code for BartChartImpl.js:

// barchart/src/BarChartImpl.js
/**
 * @extends {olympe.sc.ui.Component}
 */
barchart.BarChart = class BarChart extends olympe.sc.ui.Component {
    /**
     * @override
     */
    render(context) {
        const dimension = this.getDimension(context);
        const layout = new olympe.ui.std.AbsoluteLayout(dimension);
        this.initCommonProperties(layout, context);

        const csvValue = this.getProperty(barchart.BarChart.csvProp, context, olympe.df.OString);
        const chartTitle = new olympe.ui.std.Label();
        chartTitle.setText(this.getProperty(barchart.BarChart.titleProp, context, olympe.df.OString));
        chartTitle.setTextHorizontalAlign(olympe.ui.common.HorizontalAlign.CENTER);
        layout.appendChild(
            chartTitle,
            'charttitle',
            new olympe.df.Vector2(
                layout.getWidth()
                    .minus(chartTitle.getWidth())
                    .div(2),
                0
            )
        );
        olympe.df.processFlows([csvValue], (val) => {
            const valuesArray = val.valueOf().split(',');
            for (let i=0, l=valuesArray.length ; i < l ; i++) {
                const rectangle = new olympe.ui.std.Rectangle(new olympe.df.Vector2(20, valuesArray[i]*10));
                rectangle.setBackgroundColor(
                    this.getProperty(barchart.BarChart.colorProp, context, olympe.df.Color, olympe.df.Color.blue())
                );
                layout.appendChild(rectangle, 'rect' + i, new olympe.df.Vector2(i*30, layout.getHeight().minus(valuesArray[i]*10)));
            }
        });
        return layout;
    }
};

barchart.BarChart.entry =
    olympe.dc.Registry.registerSync('0170f4619348dfd0c935', barchart.BarChart);

barchart.BarChart.csvProp = barchart.BarChart.entry.addProperty('0170f477486cc0d10317');
barchart.BarChart.titleProp = barchart.BarChart.entry.addProperty('0170f477486cc0d10316');
barchart.BarChart.colorProp = barchart.BarChart.entry.addProperty('0170f477486cc0d10315');

← Extend DRAW with custom logic bricks made with CODERetrieve data from Olympe datacloud, transform it and display it in a custom Visual Component →
  • Introduction
    • Prerequisites
    • Outcome
  • How to implement an hardcoded bar chart
    • Overview of the steps required
    • Figuring out the Bar Chart properties
    • Creating the barchart module
    • Writing the "SC" definition of the visual component
    • Implementing the bar chart
    • Exercise: improving this rudimental bar chart
Olympe Website
Copyright © 2021 Olympe