Javascript Library
A ready to-go example is in this repository
Clone it to <Documents>/WLJS Notebooks/Extensions/ using:
git clone https://github.com/WLJSTeam/wljs-plugin-example-1Then restart WLJS Notebook.
Folder name of the extension must match with its unique name
Here is the simples example on what you can do with extensions. Why not to add ApexCharts? They looks beautiful and already animated. What we need
- Kernel package, which implements
ApexCharts[]symbols - Javascript bundle, which should include ApexCharts and bridge it with Wolfram Kernel using frontend symbols
Summary what will be done
- package for evaluation kernel, which adds a new symbol
ApexCharts - Javascript module, which renders the content of
ApexChartsexpression
Preparations
Use wljs-plugin-template template and create a new repository or use a ready-to-go example given in the heading of this tutorial.
Then edit the content of package.json
{
"name": "wljs-plugin-example-1",
"version": "0.0.1",
"scripts": {
"build": "node --max-old-space-size=8192 ./node_modules/.bin/rollup --config rollup.config.mjs"
},
"description": "An example plugin for WLJS Notebook. Library functions",
"wljs-meta": {
"kernel": [
"src/Kernel.wl"
],
"js": "dist/kernel.js",
"minjs": "dist/kernel.min.js",
"priority": 5000,
"category": "Notebook Extensions"
},
"repository": {
"type": "git",
"url": "https://github.com/WLJSTeam/wljs-plugin-example-1"
},
"dependencies": {
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-terser": "^0.4.4",
"rollup": "^3.21.6"
}
}By the default a template implies, that we will use rollup.js for bundling. Run in the root directory of this package
npm iAnd then install apexcharts
npm i apexchartsKernel package
Create a new file in src/Kernel.wl, which is going to be our package for evaluation kernel. Looking at ApexCharts API, it is easy to imagine the way how it can be used
ApexCharts[<|
"series" -> {44, 55, 67, 83},
"labels" -> {"Apples", "Oranges", "Bananas", "Berries"},
"chart" -> <|
"height" -> 350,
"type" -> "radialBar"
|>
|>]or any other way, one can pre-transform the data and use intermediate symbols. Following the simplest path, we write to our Kernel file
BeginPackage["CoffeeLiqueur`Extensions`ApexCharts`"]
(* Public context *)
ApexCharts::usage = "ApexCharts[a_Association] constructor"
Begin["`Private`"]
End[]
EndPackage[]We can put a few checks to ensure that input is an association
nonAssocHeadQ[_] = True
nonAssocHeadQ[_Association] = False
ApexCharts::notassoc = "Input is not an association"
ApexCharts[_?nonAssocHeadQ ] := (Message[ApexCharts::notassoc]; $Failed)Now we can think about ApexCharts as if it was a new entity, and the interpretation of this entity will be our graphs. To draw actual graphs instead of a symbolic representation we need to define an output form. For that ViewBox comes in hand, which is provided in CoffeeLiqueur`Extensions`Boxes` context
BeginPackage["CoffeeLiqueur`Extensions`ApexCharts`", {
"CoffeeLiqueur`Extensions`Boxes`"
}]
(* Public context *)
...
(* Output forms *)
ApexCharts /: MakeBoxes[a: ApexCharts[_Association], StandardForm ] := With[{},
ViewBox[a, a]
]First a will be an underlying expression (behind the graph), while second a is going to be rendered instead of it. However, if you try to evaluate this with defined output form, you get a similar error
BROKEN BOXViewBox tries to evaluate a non-existing frontend symbol ApexCharts.
Javascript Library
Now we need to define ApexCharts symbol on the frontend. The idea is simple: take the provided data and using Apex API render a graph on the given DOM element
let ApexCharts;
core.ApexCharts = async (args, env) => {
if (!ApexCharts) ApexCharts = (await import('apexcharts')).default; //lazy loading
const options = await interpretate(args[0], env);
const chart = new ApexCharts(env.element, options);
chart.render();
}Our association will be in options object after the interpretation.
Implement dynamic imports (lazy) if possible
Now we need to bundle this
npm run buildAfter the restart, it should work with our extension like a charm

However, after several evaluation dead instances of ApexCharts will pile up as a garbage in Javascript memory. It is recommended to properly remove them. For that we need to identify each instance of ApexCharts and assign destructor function. We use instanced frontend symbools, which behave like classes
let ApexCharts;
core.ApexCharts = async (args, env) => {
if (!ApexCharts) ApexCharts = (await import('apexcharts')).default; //lazy loading
const options = await interpretate(args[0], env);
const chart = new ApexCharts(env.element, options);
chart.render();
env.local.chart = chart;
}
core.ApexCharts.destroy = (args, env) => {
env.local.chart.destroy();
}
core.ApexCharts.virtual = trueNow we need to bundle this
npm run buildPolishing
We might do a few more tweaks. If our ApexCharts expression becomes too big, it might slow down the editor. For that reason we use Frontend Objects
BeginPackage["CoffeeLiqueur`Extensions`ApexCharts`", {
"CoffeeLiqueur`Extensions`Boxes`",
"CoffeeLiqueur`Extensions`FrontendObject`"
}]
(* Public context *)ApexCharts /: MakeBoxes[a: ApexCharts[_Association], form: StandardForm ] := With[{o = CreateFrontEndObject[a]},
MakeBoxes[o, form]
] /; ByteCount[a] > 1024*4 Frontend objects has a special predefined StandardForm (and WLXForm), where it uses a reference to a Wolfram Expression stored separately.
Another improvement can be WLXForm, if we decide to use those charts on slides or markdown cells:
ApexCharts /: MakeBoxes[a: ApexCharts[_Association], form: WLXForm ] := With[{o = CreateFrontEndObject[a]},
MakeBoxes[o, form]
]