dashblocks
Version:
Enable Analytics in your Apps: Declarative Interactive Dashboards
452 lines (418 loc) • 14.7 kB
JavaScript
import dbColors from '../dbcolors';
import dbUtils from '../dbutils';
import log from '../log';
import merge from 'deepmerge';
import { dbStdProps } from '../mixins/dbstdprops';
import pathOr from 'ramda/es/pathOr';
// Chart.js to be imported asynchronously
let Chart = null;
export function generateChart(chartId, chartType) {
return {
render: function(createElement) {
return createElement(
'div',
{
style: this.styles,
class: this.cssClasses
},
[
createElement('canvas', {
attrs: {
id: this.chartId,
width: this.width,
height: this.height
},
ref: 'canvas'
})
]
);
},
mixins: [dbStdProps],
props: {
chartId: {
default: chartId,
type: String
},
width: {
default: 100,
type: Number
},
height: {
default: 100,
type: Number
},
cssClasses: {
type: String,
default: ''
},
styles: {
type: Object
},
plugins: {
type: Array,
default() {
return [];
}
},
data: Object,
options: {
type: Object
}
},
data() {
return {
_chart: null,
_plugins: this.plugins,
chartOptions: {},
chartData: {}
};
},
computed: {
defaultOptions: function() {
let _component = this;
return {
responsive: true,
maintainAspectRatio: false,
onClick: function(evt, item) {
_component.handleClick(evt, item);
},
legend: {
labels: {
fontColor: this.dark ? '#AAA' : '#666' // TODO Get from dbcolors
}
},
plugins: {
labels: {
fontColor: this.dark ? '#AAA' : '#666' // TODO Get from dbcolors
}
}
};
},
defaultColors: function() {
return dbColors.getColors(this.dark, this.colorScheme);
}
},
watch: {
_updated: function() {
log.debug('DbChartjs: _updated prop changed');
this.scheduleUpdate(true, false);
},
data: {
handler() {
log.debug('DbChartjs: data prop changed');
this.scheduleUpdate(true, false);
},
deep: true
},
options: {
handler() {
log.debug('DbChartjs: options prop changed');
this.scheduleUpdate(false, true);
},
deep: true
},
dark: function() {
log.debug('DbChartjs: dark prop changed');
this.scheduleUpdate(true, true);
},
colorScheme: function() {
this.optionsChanged = true;
this.scheduleUpdate(true, true);
}
},
mounted() {
// Make a full copy of data:
// Chartjs augments datasets with _meta data, which may lead to watch loop
// as well as, updating properties passed to component is not a good idea
// check if this.data is even defined
this.chartData = JSON.parse(JSON.stringify(this.data || {}));
this.preProcess(true);
import('chart.js').then(module => {
log.info('chart.js: imported');
Chart = module.default;
import('chartjs-plugin-labels').then(mp => {
import('chartjs-plugin-funnel').then(mp => {
this.$nextTick(() => {
this.renderChart(this.chartData, this.chartOptions);
});
});
});
});
//this.renderChart(this.chartData, this.chartOptions);
},
methods: {
scheduleUpdate(updateData, updateOptions) {
log.debug('DbChartjs: schedule update');
this.$nextTick(() => {
if (updateData) {
this.updateData();
}
this.preProcess(updateOptions);
if (this.$data._chart) {
if (updateOptions) {
this.$data._chart.options = this.chartOptions;
}
this.$data._chart.update();
}
});
},
preProcess(updateOptions) {
if (updateOptions) {
this.setupOptions();
}
// Process datasets: set default colors if not specified
// For chart.js, need to set colors in datasets - see
// https://github.com/chartjs/Chart.js/issues/815
if (!this.chartData || !('datasets' in this.chartData) || !Array.isArray(this.chartData.datasets)) {
return;
}
for (let idx = 0; idx < this.chartData.datasets.length; idx++) {
this.setupDataset(this.chartData.datasets[idx], idx);
}
},
updateData() {
if (!this.data) {
return;
}
if ('labels' in this.data) {
this.chartData.labels = this.data.labels;
}
if (!('datasets' in this.chartData) || !('datasets' in this.data) || !Array.isArray(this.data.datasets)) {
this.chartData.datasets = [];
return;
}
// If dataset was removed or added, need to re-initialize all datasets
if (this.data.datasets.length !== this.chartData.datasets.length) {
this.chartData.datasets = [];
}
for (let idx = 0; idx < this.data.datasets.length; idx++) {
let ds = this.data.datasets[idx];
// TODO Fix Note: if data was not initialized properly, datasets could be undefined
if (idx < this.chartData.datasets.length) {
// Preserve _meta which is generated and maintained by chartjs
/// Merge in new data / props passed via data prop
let merged = Object.assign({ _meta: this.chartData.datasets[idx]._meta }, merge({}, ds));
this.chartData.datasets[idx] = merged;
//this.chartData.datasets[idx].data = merged; //merge([],ds.data); // ???
//this.chartData.datasets[idx].label = ds.label;
} else {
// Adding new dataset dynamically - setup it once
this.chartData.datasets.push(merge({}, ds));
this.setupDataset(this.chartData.datasets[idx], idx);
}
}
},
getColor(i) {
return this.defaultColors[i % this.defaultColors.length];
},
// Set params and colors for dataset, if not explicitly specified
setupDataset(ds, idx) {
if ('borderColor' in ds || 'backgroundColor' in ds) {
return; // Colors set in dataset
}
// TODO Dark/Light switch - make sure colors are updated when we set them, and not updated when passed in options
// TODO Use meta key: _db
// Depending on chart type
if (['pie-chart', 'doughnut-chart', 'polar-chart'].includes(this.chartId)) {
ds.borderWidth = 1; //ds.borderWidth || 0;
//ds.borderColor = "rgba(0, 0, 0, 0.2)";
ds.borderColor = 'rgba(255, 255, 255, 0.2)';
//ds.segmentStrokeWidth = 20;
//ds.segmentStrokeColor = "rgba(255, 255, 255, 0)";
ds.backgroundColor = this.defaultColors.map(x => dbColors.hex2RGBA(x, 0.5));
} else if (['funnel-chart'].includes(this.chartId)) {
ds.borderWidth = ds.borderWidth || 1;
ds.borderColor = this.defaultColors[idx];
ds.backgroundColor = ds.data.map((x, i) => dbColors.hex2RGBA(this.getColor(i), 0.5));
//ds.backgroundColor.push(this.defaultColors[0]);
} else {
ds.borderWidth = ds.borderWidth || 1;
ds.borderColor = this.defaultColors[idx];
ds.backgroundColor = dbColors.hex2RGBA(this.defaultColors[idx], 0.5);
}
},
setupOptions() {
log.debug('DbChartjs: setting up options');
let opts = null;
if (['radar-chart', 'polar-chart'].includes(this.chartId)) {
opts = merge.all([
this.defaultOptions,
{
scale: {
angleLines: {
lineWidth: 1,
color: this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'
},
gridLines: {
color: this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)',
zeroLineColor: '#FFF'
},
ticks: {
showLabelBackdrop: !this.dark,
fontColor: this.getAxesColor()
}
}
},
this.options || {} // What's passed in options will override defaults
]);
} else if (['pie-chart', 'doughnut-chart'].includes(this.chartId)) {
opts = merge(this.defaultOptions, this.options || {});
// Enables custom label generation
//utils.ensureProperty(opts, 'legend', {});
//utils.ensureProperty(opts.legend, 'labels', {});
//utils.ensureProperty(opts.legend.labels, 'generateLabels', this.generatePieLabels);
} else {
opts = merge(this.defaultOptions, this.options || {});
dbUtils.ensureProperty(opts, 'scales', {});
dbUtils.ensureProperty(opts.scales, 'xAxes', [{}]);
opts.scales.xAxes = opts.scales.xAxes.map(x => this.setupScale(x));
dbUtils.ensureProperty(opts.scales, 'yAxes', [{}]);
opts.scales.yAxes = opts.scales.yAxes.map(x => this.setupScale(x));
}
this.chartOptions = opts;
},
setupScale(axe) {
return merge(
{
gridLines: {
color: this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)',
zeroLineColor: '#EEE'
},
ticks: {
fontColor: this.getAxesColor()
}
},
axe
);
},
getAxesColor() {
return this.dark ? '#BBB' : '#666'; // TODO Get from dbcolors
},
addPlugin(plugin) {
this.$data._plugins.push(plugin);
},
generateLegend() {
if (this.$data._chart) {
return this.$data._chart.generateLegend();
}
},
// TODO Custom labels generation - consider if needs to be used
generateLabels: function(chart) {
var data = chart.data;
if (data.labels.length && data.datasets.length) {
return data.labels.map(function(label, i) {
var meta = chart.getDatasetMeta(0);
var ds = data.datasets[0];
var arc = meta.data[i];
var custom = (arc && arc.custom) || {};
var getValueAtIndexOrDefault = Chart.helpers.getValueAtIndexOrDefault;
var arcOpts = chart.options.elements.arc;
var fill = custom.backgroundColor ? custom.backgroundColor : getValueAtIndexOrDefault(ds.backgroundColor, i, arcOpts.backgroundColor);
var stroke = custom.borderColor ? custom.borderColor : getValueAtIndexOrDefault(ds.borderColor, i, arcOpts.borderColor);
var bw = custom.borderWidth ? custom.borderWidth : getValueAtIndexOrDefault(ds.borderWidth, i, arcOpts.borderWidth);
return {
// And finally :
text: ds.data[i] + ' ' + label,
fillStyle: fill,
strokeStyle: stroke,
lineWidth: bw,
hidden: isNaN(ds.data[i]) || meta.data[i].hidden,
index: i
};
});
}
return [];
},
renderChart(data, options) {
this.$nextTick(() => {
if (this.$data._chart) {
this.$data._chart.destroy();
this.$data._chart = null;
}
let chart = new Chart(this.$refs.canvas.getContext('2d'), {
type: chartType,
data: data,
options: options,
plugins: this.$data._plugins
});
this.$data._chart = chart;
});
},
handleClick(event, item) {
log.debug('handleClick');
let charElement = this.$data._chart.getElementAtEvent(event);
if (Array.isArray(charElement) && charElement.length === 1) {
let chartEl = charElement[0];
let index = chartEl._index;
let datasetIndex = chartEl._datasetIndex;
let label = pathOr('', ['_chart', 'data', 'labels', index], chartEl);
let value = pathOr(0, ['_chart', 'data', 'datasets', datasetIndex, 'data', index], chartEl);
let datasetLabel = pathOr(0, ['_chart', 'data', 'datasets', datasetIndex, 'label'], chartEl);
this.$emit('db-event', {
type: 'item-click',
label: label,
value: value,
index: index,
datasetLabel: datasetLabel,
datasetIndex: datasetIndex,
element: chartEl,
item: item
});
}
}
},
beforeDestroy() {
if (this.$data._chart) {
this.$data._chart._component = null;
this.$data._chart.destroy();
this.$data._chart = null;
}
}
};
}
export const DbChartjsBar = generateChart('bar-chart', 'bar');
export const DbChartjsHorizontalBar = generateChart('horizontalbar-chart', 'horizontalBar');
export const DbChartjsDoughnut = generateChart('doughnut-chart', 'doughnut');
export const DbChartjsLine = generateChart('line-chart', 'line');
export const DbChartjsPie = generateChart('pie-chart', 'pie');
export const DbChartjsPolarArea = generateChart('polar-chart', 'polarArea');
export const DbChartjsRadar = generateChart('radar-chart', 'radar');
export const DbChartjsBubble = generateChart('bubble-chart', 'bubble');
export const DbChartjsScatter = generateChart('scatter-chart', 'scatter');
export const DbChartjsFunnel = generateChart('funnel-chart', 'funnel');
export default {
DbChartjsBar,
DbChartjsHorizontalBar,
DbChartjsDoughnut,
DbChartjsLine,
DbChartjsPie,
DbChartjsPolarArea,
DbChartjsRadar,
DbChartjsBubble,
DbChartjsScatter,
DbChartjsFunnel
};
// Process Axes
/*
if (isRadial) {
dbUtils.ensureProperty(opts, 'scale', {});
dbUtils.ensureProperty(opts.scale, 'angleLines', {});
dbUtils.ensureProperty(opts.scale.angleLines, 'lineWidth', 1);
dbUtils.ensureProperty(
opts.scale.angleLines,
'color',
this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'
);
dbUtils.ensureProperty(opts.scale, 'gridLines', {});
dbUtils.ensureProperty(
opts.scale.gridLines,
'color',
this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'
);
dbUtils.ensureProperty(opts.scale, 'ticks', {});
dbUtils.ensureProperty(opts.scale.ticks, 'showLabelBackdrop', !this.dark);
dbUtils.ensureProperty(opts.scale.ticks, 'fontColor', this.getAxesColor());
} else {
// TODO setup
}
*/