Extending Canvas with Plugins, Part 2

This is a continuation of Extending Canvas with Plugins, so you should start there.

Thanks to part one, we have a custom function that will hit the Github API, fetch the tag information from a repo, and provide that information as a datatable. Now it's time to do something useful with that datatable; render the dates from the tags on a simple timeline. To do this, we're going to create another function, along with a new render function. We'll also add an element so that we can add the timeline to the workpad from the "Elements" section in the Canvas toolbar.

Let's start with the timeline function. This function is going to take a datatable context, and some arguments that will map columns to timeline parameters, and will return a timeline render type. Much like the github-tags function, this function can run on both the client and the server, so we'll create it as a common function. Create a new file at common/functions/timeline.js and place the following code in that file:

export const timeline = () => ({
  name: 'timeline',
  type: 'render',
  context: {
    types: ['datatable'],
  },
  help: 'A simple timeline visualization.',
  args: {
    name: {
      types: ['string'],
      help: 'The field to map to the name',
    },
    start: {
      types: ['string', null],
      help: 'The field to use for the start time',
    },
    end: {
      types: ['string', null],
      help: 'The field to use for the end time',
    },
    font: {
      types: ['style'],
      help: 'Legend and tick mark fonts',
      default: '{font}',
    },
  },
  fn(context, args) {
    return {
      type: 'render',
      as: 'timeline',
      value: {
        options: {},
        data: context.rows.map((row, i) => ({
          id: i,
          content: row[args.name],
          start: row[args.start],
          end: row[args.end],
          style: args.font.css,
        })),
      },
    };
  },
});

This function works similarly to the github-tags function from part one, with one important difference; it uses the context property to control its input. This way, when you pipe something to the timeline function, it'll check the type on the context before executing the function, and fail early if it's not a datatable.

The function that gets called here, fn, is pretty simple, it just maps column values to properties, and returns an object of type render, which is set to render as a timeline. The "render as" bit is what tells Canvas how to turn this output into something on the screen. The 'timeline' render function is what we'll write next.

The timeline render function is going to use a library called "vis.js" to render the timeline, so the arguments here mirror the arguments for that library pretty closely. It'll require at least a name and start value, both of which will be the name of the column to use, and can optionally take an end value (another column, used to show spans of time) and a font argument. Here, font simply changes the appearance of the text on the timeline, and while it's not super useful, it'll give us an argument that we can control from a sidebar UI. More on that later.

Now that we have a timeline function, we need to register it. Just like before, we'll import it and add it to the exported commonFunctions array in common/functions/index.js. This will cause the function to be registered on startup, just like before. The file should look like this now:

import { canary } from './canary';
import { githubTags } from './github_tags';
import { timeline } from './timeline';

export const commonFunctions = [canary, githubTags, timeline];

If you try to use this now, it'll fail saying that it doesn't know how to render a timeline. For this to work, create a a new file, in a new path: public/render_functions/timeline.js. When canvas evaluates an expression with the timeline function, and gets the render type out of it, this is the renderer, or render function, it will try to call. As mentioned before, this will use "vis.js" to render the timeline, so run npm install --save vis, and then place the following in public/render_functions/timeline.js:

import vis from 'vis';
import 'vis/dist/vis.css';

export default () => ({
  name: 'timeline',
  displayName: 'Timeline',
  help: 'A timeline vis, for showing events over a period of time',
  render(domNode, config, handlers) {
    const { options, data } = config;
    const items = data.filter(row => Boolean(row.content) && Boolean(row.start)).map(row => ({
      ...row,
      start: row.start && new Date(row.start),
      end: row.end && new Date(row.end),
    }));

    // create the vis timeline
    const timeline = new vis.Timeline(domNode, items, options);

    // clean up the timeline when the element is destroyed
    handlers.onDestroy(() => timeline.destroy());

    // tell the system the timeline is ready
    handlers.done();
  },
});

Note that the render function takes three arguments:

Now if you try to use the github-tags data and render a timeline, it'll work. You can check this by adding an element and changing its expression to github-tags limit=30 repo="elastic/kibana" | timeline name=name start=date, and you should see an element that looks like this:

github timeline element

By adding a timeline function, and a timeline render function, now we have a custom view for our custom github data. That's it for now, in the next part, we'll look at how to add controls to the UI to add our timeline element and control the font used without touching the expression.