WLJS LogoWLJS Notebook

JS libraries for data visualization

Fortunately, we have a vast choice of many great JavaScript tools for data visualization. In this guide, we'll try a few of them and integrate them seamlessly with Wolfram Language.

ApexCharts

ApexCharts is a free and open-source interactive charting library with built-in transition animations. It is distributed as an NPM package, which makes it easy to use with MJS cells.

You do not need to reevaluate or recompile any Javascript cells, the generated output is stored in the notebook. Copy the whole group from the cell's properties and paste it to a new notebook if you want to reuse it.

First, let's install it locally using shell cells:

.sh npm i apexcharts --prefix .

Let's look at the documentation and create a simple example:

.mjs import ApexCharts from 'apexcharts' const dom = document.createElement('div'); this.return(dom); const chart = new ApexCharts(dom, { "series" : [44, 55, 67, 83], "labels" : ["Apples", "Oranges", "Bananas", "Berries"], "chart" : { "height" : 350, "type" : "radialBar" } }); this.after = () => { chart.render(); };
(*VB[*)(ApexCharts[<|"series" -> {44, 55, 67, 83}, "labels" -> {"Apples", "Oranges", "Bananas", "Berries"}, "chart" -> <|"height" -> 250, "type" -> "radialBar"|>|>])(*,*)(*"1:eJyFjs0KwjAQhKtWRX0K79492148CELzBNt20y6EJiQR9NU9udufgz8ggeHbWWYz+9IWepYkSUhZztbUw7RlOTm85y34GPRCvJ14IdiKIJLt9HyKFTeDaiUTesKg02lxoRDpwEBHkVxEsfyIGijRfET7xck5g0GtGa8eumbkDDp+I6Pv/32/umSopP7g/2/fIjVtpOd3QYH4cKg2DB5qApOBfwG1E0WT"*)(*]VB*)

ApexCharts requires the DOM element dom to be mounted to the document before calling the render method. This is why we used the "hook" after, which is called after the element is already in the document.

Next, we can turn it into a frontend symbol with its own state and lifecycle. Let's adjust our script accordingly:

.mjs import ApexCharts from 'apexcharts' const whenVisible = (ele, cbk) => { let observer = new IntersectionObserver(function(entries) { if(entries[0].isIntersecting === true) { observer.unobserve(ele); cbk(); return; } }, { threshold: [0] }); observer.observe(ele); }; core.ApexCharts = async (args, env) => { const options = await interpretate(args[0], env); const chart = new ApexCharts(env.element, options); whenVisible(env.element, () => chart.render()) }

Here we added a whenVisible helper function that renders the chart when it becomes visible in the viewport. Therefore, if you have a long notebook, the animation will play at the right time. We provide chart data via the chart object, which will be represented as an Association on the Wolfram Language side.

For the next step we need to define ApexCharts on the evaluation kernel:

ApexCharts;

That's it 😀 Actually, we need to define the output form of ApexCharts. For now, we'll avoid data transformation and pass it as is:

ApexCharts /: MakeBoxes[a: ApexCharts[_Association], StandardForm] := ViewBox[a,a]

Now let's create an example chart:

ApexCharts[<| "series" -> { <|"name" -> "Net Profit", "data" -> RandomInteger[{2,4}, 5]|>, <|"name" -> "Revenue", "data" -> RandomInteger[{2,4}, 5]|>, <|"name" -> "Free Cash Flow", "data" -> RandomInteger[{2,4}, 5]|> }, "chart" -> <|"type" -> "bar", "width"->300|> |>]

(*VB[*)(ApexCharts[<|"series" -> {<|"name" -> "Net Profit", "data" -> {3, 3, 4, 2, 4}|>, <|"name" -> "Revenue", "data" -> {2, 3, 3, 4, 4}|>, <|"name" -> "Free Cash Flow", "data" -> {4, 2, 4, 4, 2}|>}, "chart" -> <|"type" -> "bar", "width" -> 300|>|>])(*,*)(*"1:eJyVUVsKwjAQrG9Q8Az136/qBaRQFESkPUHabkigNiXZWr29WYtQpUL6M+xOsjOz7CZVMR95nmemFo6qyNtuaeFQwSMUTKPhY+JWxBmjMslQqrIlaSyuC0jm1IGWYPjk83CWBl1mqSjZDRKyvQD6V624xJ5fOUPGZ139E+XddXD/xqCtB7kviIE7lDW4Wwd/AgyzXtsi0gB+yIzwo0I17gm+Fu4yPxI0mdE9XZPhs4KEjpky3aPVyByF3FqrF4yWcaM="*)(*]VB*)

To use it in slides or Markdown cells, define WLXForm:

WLXForm requires any passed expression to be frontend object

ApexCharts /: MakeBoxes[a: ApexCharts[_Association], WLXForm] := With[{ f = CreateFrontEndObject[a] }, MakeBoxes[f, WLXForm] ]

Final improvements

Even if we set the StandardForm output form for ApexCharts, the input expression itself is still in the output cell. You can verify this by evaluating the output again. It may become an issue if the data reaches a few kilobytes, as it can slow down the editor. To solve this, we can compress it to a single reference represented as a frontend object. The data won't be lost and will remain valid for evaluation:

ApexCharts /: MakeBoxes[a: ApexCharts[_Association], form: StandardForm ] := With[{ o = CreateFrontEndObject[a] }, MakeBoxes[o, form] ] /; ByteCount[a] > 1024*4

On this page