Custom chart engine
Implementing new chart engines support
Implementing support for a new chart engine includes multiple steps and changes in Python, HTML, and JavaScript. Starting from the Python code:
-
Create a new builder class at
ckanext.charts.chart_builder
that inherits fromBaseChartBuilder
and implements theget_supported_forms
method. This method must return a list of classes that represent supported chart types forms. -
Each form type builder must be connected with a respective chart type builder.
-
The chart type builder must implement a
to_json
method that will return a dumped JSON data, which will be passed to a JS script. -
The form type builder must implement a
get_form_fields
method that will return a list of all form fields that will be rendered for the user, allowing them to provide all the necessary information for a chart. -
Register your chart engine by adding the builder class to
get_chart_engines
inckanext.charts.chart_builder.__init__.py
.
A full example of an implementation of bar
chart for obvervable plot
library.
from __future__ import annotations
import json
from typing import Any
import ckanext.charts.exception as exception
from ckanext.charts.chart_builders.base import BaseChartBuilder, BaseChartForm
class ObservableBuilder(BaseChartBuilder):
@classmethod
def get_supported_forms(cls) -> list[type[Any]]:
return [ObservableBarForm]
class ObservableBarBuilder(ObservableBuilder):
def to_json(self) -> str:
return json.dumps(
{
"type": "bar",
"data": self.df.to_dict(orient="records"),
"settings": self.settings,
}
)
class ObservableBarForm(BaseChartForm):
name = "Bar"
builder = ObservableBarBuilder
def fill_field(self, choices: list[dict[str, str]]) -> dict[str, str]:
field = self.color_field(choices)
field.update({"field_name": "fill", "label": "Fill"})
return field
def get_form_fields(self):
columns = [{"value": col, "label": col} for col in self.df.columns]
chart_types = [
{"value": form.name, "label": form.name}
for form in self.builder.get_supported_forms()
]
return [
self.title_field(),
self.description_field(),
self.engine_field(),
self.type_field(chart_types),
self.x_axis_field(columns),
self.y_axis_field(columns),
self.fill_field(columns),
self.opacity_field(),
self.limit_field(),
]
Vendor and custom JS
Another step is to register JS/CSS vendor libraries of the chart you want to use. Refer to CKAN documentation to read about adding CSS and JavaScript files using Webassets.
You also will need a CKAN JS module, that will be responsible for rendering the Chart. This module will work with the vendor library and will be responsible for rendering the chart in the container.
This module must be registered inside a webassets.yml
as well.
ckan.module("charts-render-observable", function ($, _) {
"use strict";
return {
options: {
config: null
},
initialize: function () {
$.proxyAll(this, /_/);
if (!this.options.config) {
console.error("No configuration provided");
return;
}
var plot;
switch (this.options.config.type) {
case "bar":
plot = Plot.barY(this.options.config.data, this.options.config.settings).plot();
break;
default:
return;
}
this.el[0].replaceChildren(plot);
}
};
});
HTML container
And an HTML file, that will provide a proper container and include your JS module with data-module
.
{% asset "charts/observable" %}
{% if chart %}
<div id="chart-container" data-module="charts-render-observable" data-module-config="{{ chart }}"></div>
{% else %}
<p class="text-muted">
{{ _("Cannot build chart with current settings") }}
</p>
{% endif %}
Note, that we should add {% asset "charts/observable" %}
not only here, but in charts_form.html
too.
The reason for having a separate HTML
file and JS
module is that different libraries may require different types of container elements (such as div, canvas, etc.) to initialize or may need additional boilerplate code to build a chart. There's no easy way to abstract this, so you have to implement these things yourself.