Skip to content

Commit

Permalink
feat: add snapshot feature for all chart engines, refactor doc, add c…
Browse files Browse the repository at this point in the history
…olorpicker form_snippet
  • Loading branch information
mutantsan committed Jan 21, 2025
1 parent 4d9e304 commit ec8ab86
Show file tree
Hide file tree
Showing 91 changed files with 50,540 additions and 7,934 deletions.
2 changes: 1 addition & 1 deletion ckanext/charts/assets/css/charts.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

136 changes: 106 additions & 30 deletions ckanext/charts/assets/js/charts-render-chartjs.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,122 @@
ckan.module("charts-render-chartjs", function ($, _) {
ckan.module("charts-render-chartjs", function($, _) {
"use strict";

return {
const: {
zoomUnsupportedTypes: ['pie', 'doughnut', 'radar']
},
options: {
config: null
config: null,
chartBg: 'white'
},

initialize: function () {
initialize: function() {
$.proxyAll(this, /_/);

if (window.charts_chartjs) {
window.charts_chartjs.destroy();
}
this.chartControl = this.el.next(".chart-control");
this.chartId = this.el[0].id;
this.isZoomSupported = !this.const.zoomUnsupportedTypes.includes(this.options.config.type);

window.charts_chartjs = window.charts_chartjs || {};

if (!this.options.config) {
console.error("No configuration provided");
return;
}

const unsupportedTypes = ['pie', 'doughnut', 'radar'];
const isZoomSupported = !unsupportedTypes.includes(this.options.config.type);
this._registerChartBackground();
this._destroyChartInAContainer(this.chartId);

if (isZoomSupported) {
const zoomOptions = this.options.config.options.plugins.zoom;
this.options.config.id = this.chartId;

this.options.config.options.plugins.title.text = () => {
return 'Zoom: ' + this.zoomStatus(zoomOptions) + ', Pan: ' + this.panStatus(zoomOptions);
};
var chart = new Chart(this.el[0].getContext("2d"), this.options.config)

$('#resetZoom').on('click', this.resetZoom);
$('#toggleZoom').on('click', (e) => this.toggleZoom(e, zoomOptions));
$('#togglePan').on('click', (e) => this.togglePan(e, zoomOptions));
}
window.charts_chartjs[chart.id] = chart;

$(".zoom-control").toggle(isZoomSupported);
this._registerControlEvents(chart.id);
},

window.charts_chartjs = new Chart(this.el[0].getContext("2d"), this.options.config);
/**
* Register a new chart background plugin that will draw a background
* on the canvas before drawing the chart.
*
* We need it to have a white background on the chart when the chart
* is being exported as an image. Otherwise, the background will be
* transparent.
*/
_registerChartBackground: function() {
Chart.register({
id: 'chartjs-chart-background',
beforeDraw: (chart, args, opts) => {
const ctx = chart.canvas.getContext('2d');
ctx.save();
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = this.options.chartBg;
ctx.fillRect(0, 0, chart.width, chart.height);
ctx.restore();
}
})
},

resetZoom: function(event) {
event.preventDefault();
window.charts_chartjs.resetZoom();
/**
* Destroy the chart in a container before rendering a new one.
*
* We're deleting reference only to the chart, that share the same
* container id. It should still allow us to have multiple charts
* on the same page.
*
* @param {String} containerId
*/
_destroyChartInAContainer: function(containerId) {
for (const [_, chart] of Object.entries(window.charts_chartjs)) {
if (chart.canvas.id !== containerId) {
continue;
}

window.charts_chartjs[chart.id].destroy();
delete window.charts_chartjs[chart.id];
break;
}
},

zoomStatus: function(zoomOptions) {
/**
* Register control events for the chart.
*
* @param {String} chartId - The id of the chart
*/
_registerControlEvents: function(chartId) {
this.chartControl.toggle(this.isZoomSupported);

if (this.isZoomSupported) {
const zoomOptions = this.options.config.options.plugins.zoom;

this.options.config.options.plugins.title.text = () => {
return 'Zoom: ' + this.getZoomStatus(zoomOptions) + ', Pan: ' + this.getPanStatus(
zoomOptions);
};

this.chartControl.find('#resetZoom').off().on('click', (e) => this.resetZoom(e, chartId));
this.chartControl.find('#toggleZoom').off().on('click', (e) => this.toggleZoom(e, zoomOptions, chartId));
this.chartControl.find('#togglePan').off().on('click', (e) => this.togglePan(e, zoomOptions, chartId));
}

this.chartControl.find("#makeSnapshot").off().on("click", (e) => this._makeSnapshot(e, chartId));
},

getZoomStatus: function(zoomOptions) {
return zoomOptions.zoom.drag.enabled ? 'enabled' : 'disabled';
},

panStatus: function(zoomOptions) {
getPanStatus: function(zoomOptions) {
return zoomOptions.pan.enabled ? 'enabled' : 'disabled';
},

toggleZoom: function (event, zoomOptions) {
resetZoom: function(event, chartId) {
event.preventDefault();
window.charts_chartjs[chartId].resetZoom();
},

toggleZoom: function(event, zoomOptions, chartId) {
event.preventDefault();

const zoomEnabled = zoomOptions.zoom.wheel.enabled;
Expand All @@ -60,14 +125,25 @@ ckan.module("charts-render-chartjs", function ($, _) {
zoomOptions.zoom.pinch.enabled = !zoomEnabled;
zoomOptions.zoom.drag.enabled = !zoomEnabled;

window.charts_chartjs.update();
window.charts_chartjs[chartId].update();
},

togglePan: function(event, zoomOptions) {
togglePan: function(event, zoomOptions, chartId) {
event.preventDefault();

zoomOptions.pan.enabled = !zoomOptions.pan.enabled;
window.charts_chartjs.update();
}
};
window.charts_chartjs[chartId].update();
},

_makeSnapshot: function(event, chartId) {
event.preventDefault();

var dataUrl = window.charts_chartjs[chartId].toBase64Image();

var link = document.createElement('a')
link.download = 'view-snapshot-' + Date.now() + '.png';
link.href = dataUrl
link.click()
},
}
});
64 changes: 59 additions & 5 deletions ckanext/charts/assets/js/charts-render-observable.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
ckan.module("charts-render-observable", function ($, _) {
ckan.module("charts-render-observable", function($, _) {
"use strict";

return {
options: {
config: null
config: null,
},

initialize: function () {
initialize: function() {
$.proxyAll(this, /_/);

this.chartControl = this.el.next(".chart-control");
this.chartId = this.el[0].id;

window.charts_obvservable = window.charts_obvservable || {};

if (!this.options.config) {
console.error("No configuration provided");
return;
Expand Down Expand Up @@ -37,6 +42,50 @@ ckan.module("charts-render-observable", function ($, _) {
}

this.el[0].replaceChildren(plot);

window.charts_obvservable[this.chartId] = plot;

this.chartControl.find("#makeSnapshot").on(
"click", (e) => this._makeSnapshot(e, this.chartId)
);
},

_makeSnapshot: function(event, chartId) {
event.preventDefault();

var chart = window.charts_obvservable[chartId];

if (!chart) {
console.error("Chart not found");
return;
}

//get svg element.
var svg = chart.querySelector("svg");

//get svg source.
var serializer = new XMLSerializer();
var source = serializer.serializeToString(svg);

//add name spaces.
if(!source.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)){
source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
}

if(!source.match(/^<svg[^>]+"http\:\/\/www\.w3\.org\/1999\/xlink"/)){
source = source.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
}

//add xml declaration
source = '<?xml version="1.0" standalone="no"?>\r\n' + source;

//convert svg source to URI data scheme.
var dataUrl = "data:image/svg+xml;charset=utf-8,"+encodeURIComponent(source);
var link = document.createElement('a')
link.download = 'view-snapshot-' + Date.now() + '.svg';
link.href = dataUrl
link.click()

}
};
});
Expand All @@ -46,7 +95,7 @@ ckan.module("charts-render-observable", function ($, _) {
// https://observablehq.com/@d3/pie-chart

function PieChart(data, {
names, // given d in data, returns the (ordinal) label
names, // given d in data, returns the (ordinal) label
values, // given d in data, returns the (quantitative) value
title, // given d in data, returns the title text
width = 640, // outer width, in pixels
Expand Down Expand Up @@ -133,7 +182,12 @@ function PieChart(data, {
.attr("font-weight", (_, i) => i ? null : "bold")
.text(d => d);

const resultSvg = Object.assign(svg.node(), { scales: { color } });
const resultSvg = Object.assign(svg.node(), {
scales: {
color
}
});

resultSvg.setAttribute("opacity", opacity);

return resultSvg;
Expand Down
39 changes: 36 additions & 3 deletions ckanext/charts/assets/js/charts-render-plotly.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,53 @@
ckan.module("charts-render-plotly", function ($, _) {
ckan.module("charts-render-plotly", function($, _) {
"use strict";

return {
options: {
config: null
},

initialize: function () {
initialize: function() {
$.proxyAll(this, /_/);

this.chartControl = this.el.next(".chart-control");

window.charts_plotly = window.charts_plotly || {};

if (!this.options.config) {
console.error("No configuration provided");
return;
}

Plotly.newPlot(this.el[0], this.options.config);
if (!this.chartControl.length) {
console.error("No chart control found");
return;
}

Plotly.newPlot(this.el[0], this.options.config).then(
(chart) => {
window.charts_plotly[chart.id] = chart;

this.chartControl.find("#makeSnapshot").on("click", (e) => this._makeSnapshot(e, chart.id));
}
);
},

_makeSnapshot: function(event, chartId) {
event.preventDefault();

Plotly.toImage(
window.charts_plotly[chartId], {
height: window.charts_plotly[chartId].clientHeight,
width: window.charts_plotly[chartId].clientWidth
})
.then(
function(dataUrl) {
var link = document.createElement('a')
link.download = 'view-snapshot-' + Date.now() + '.png';
link.href = dataUrl;
link.click()
}
)
}
};
});
5 changes: 4 additions & 1 deletion ckanext/charts/assets/webassets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ chartjs:
filter: rjsmin
output: ckanext-charts/%(version)s-chartjs.js
contents:
# do not change the order of these files as they are dependent on each other
- js/vendor/chartjs.min.js
- js/vendor/chartjs-adapter-moment.js
- js/charts-render-chartjs.js
- js/vendor/hammerjs.min.js
- js/vendor/chartjs-plugin-zoom.min.js
##############################

- js/charts-render-chartjs.js
extra:
preload:
- base/main
Expand Down
15 changes: 15 additions & 0 deletions ckanext/charts/chart_builders/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,3 +855,18 @@ def size_max_field(self) -> dict[str, Any]:
"type": "int",
"help_text": "Maximum size of dots or bubbles",
}

def color_picker_field(self) -> dict[str, Any]:
return {
"field_name": "color_picker",
"label": "Color Picker",
"form_snippet": "chart_color_picker.html",
"group": "Styles",
"type": "str",
"validators": [
self.get_validator("default")("#ffffff"),
self.get_validator("unicode_safe"),
],
"help_text": "Select a color",
"default": "#ffffff",
}
Loading

0 comments on commit ec8ab86

Please sign in to comment.