How to draw pile/stack barchart like the one in Highcharts (https://www.highcharts.com/demo/column-stacked) or CanvasJS (https://canvasjs.com/javascript-charts/stacked-column-100-chart/)? I am making a wechat micro program that requires such chart yet I have hard time figuring out how to do the data visualization by looking through their source code (they are huge). And I can't directly use javascript components built for desktop and mobile browsers since the syntax for wechat micro program is different.
I like this tutorial: http://www.williammalone.com/articles/html5-canvas-javascript-bar-graph/
But I yet to figure out how to create stacked bar chart from regular bar chart.
I've simplified my code so it is easier to read:
var Charts = function Charts(opts) {
opts.title = opts.title || {};
opts.subtitle = opts.subtitle || {};
opts.yAxis = opts.yAxis || {};
opts.xAxis = opts.xAxis || {};
opts.extra = opts.extra || {};
opts.legend = opts.legend === false ? false : true;
opts.animation = opts.animation === false ? false : true;
var config$$1 = assign({}, config);
config$$1.yAxisTitleWidth = opts.yAxis.disabled !== true && opts.yAxis.title ? config$$1.yAxisTitleWidth : 0;
config$$1.pieChartLinePadding = opts.dataLabel === false ? 0 : config$$1.pieChartLinePadding;
config$$1.pieChartTextPadding = opts.dataLabel === false ? 0 : config$$1.pieChartTextPadding;
this.opts = opts;
this.config = config$$1;
this.context = wx.createCanvasContext(opts.canvasId);
this.chartData = {};
this.event = new Event();
this.scrollOption = {
currentOffset: 0,
startTouchX: 0,
distance: 0
};
drawCharts.call(this, opts.type, opts, config$$1, this.context);
};
Charts.prototype.updateData = function() {
var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
this.opts.series = data.series || this.opts.series;
this.opts.categories = data.categories || this.opts.categories;
this.opts.title = assign({}, this.opts.title, data.title || {});
this.opts.subtitle = assign({}, this.opts.subtitle, data.subtitle || {});
drawCharts.call(this, this.opts.type, this.opts, this.config, this.context);
};
function Animation(opts) {
this.isStop = false;
opts.duration = typeof opts.duration === 'undefined' ? 1000 : opts.duration;
opts.timing = opts.timing || 'linear';
var delay = 17;
var createAnimationFrame = function createAnimationFrame() {
if (typeof requestAnimationFrame !== 'undefined') {
return requestAnimationFrame;
} else if (typeof setTimeout !== 'undefined') {
return function(step, delay) {
setTimeout(function() {
var timeStamp = +new Date();
step(timeStamp);
}, delay);
};
} else {
return function(step) {
step(null);
};
}
};
var animationFrame = createAnimationFrame();
var startTimeStamp = null;
var _step = function step(timestamp) {
if (timestamp === null || this.isStop === true) {
opts.onProcess && opts.onProcess(1);
opts.onAnimationFinish && opts.onAnimationFinish();
return;
}
if (startTimeStamp === null) {
startTimeStamp = timestamp;
}
if (timestamp - startTimeStamp < opts.duration) {
var process = (timestamp - startTimeStamp) / opts.duration;
var timingFunction = Timing[opts.timing];
process = timingFunction(process);
opts.onProcess && opts.onProcess(process);
animationFrame(_step, delay);
} else {
opts.onProcess && opts.onProcess(1);
opts.onAnimationFinish && opts.onAnimationFinish();
}
};
_step = _step.bind(this);
animationFrame(_step, delay);
}
function fillSeriesColor(series, config) {
var index = 0;
return series.map(function(item) {
if (!item.color) {
item.color = config.colors[index];
index = (index + 1) % config.colors.length;
}
return item;
});
}
function calLegendData(series, opts, config) {
if (opts.legend === false) {
return {
legendList: [],
legendHeight: 0
};
}
var padding = 5;
var marginTop = 8;
var shapeWidth = 15;
var legendList = [];
var widthCount = 0;
var currentRow = [];
series.forEach(function(item) {
var itemWidth = 3 * padding + shapeWidth + measureText(item.name || 'undefined');
if (widthCount + itemWidth > opts.width) {
legendList.push(currentRow);
widthCount = itemWidth;
currentRow = [item];
} else {
widthCount += itemWidth;
currentRow.push(item);
}
});
if (currentRow.length) {
legendList.push(currentRow);
}
return {
legendList: legendList,
legendHeight: legendList.length * (config.fontSize + marginTop) + padding
};
}
function calYAxisData(series, opts, config) {
var ranges = getYAxisTextList(series, opts, config);
var yAxisWidth = config.yAxisWidth;
var rangesFormat = ranges.map(function(item) {
item = util.toFixed(item, 2);
item = opts.yAxis.format ? opts.yAxis.format(Number(item)) : item;
yAxisWidth = Math.max(yAxisWidth, measureText(item) + 5);
return item;
});
if (opts.yAxis.disabled === true) {
yAxisWidth = 0;
}
return { rangesFormat: rangesFormat, ranges: ranges, yAxisWidth: yAxisWidth };
}
function getYAxisTextList(series, opts, config) {
var data = dataCombine(series);
data = data.filter(function(item) {
return item !== null;
});
var minData = Math.min.apply(this, data);
var maxData = Math.max.apply(this, data);
if (typeof opts.yAxis.min === 'number') {
minData = Math.min(opts.yAxis.min, minData);
}
if (typeof opts.yAxis.max === 'number') {
maxData = Math.max(opts.yAxis.max, maxData);
}
if (minData === maxData) {
var rangeSpan = maxData || 1;
minData -= rangeSpan;
maxData += rangeSpan;
}
var dataRange = getDataRange(minData, maxData);
var minRange = dataRange.minRange;
var maxRange = dataRange.maxRange;
var range = [];
var eachRange = (maxRange - minRange) / config.yAxisSplit;
for (var i = 0; i <= config.yAxisSplit; i++) {
range.push(minRange + eachRange * i);
}
return range.reverse();
}
function calCategoriesData(categories, opts, config) {
var result = {
angle: 0,
xAxisHeight: config.xAxisHeight
};
var _getXAxisPoints = getXAxisPoints(categories, opts, config),
eachSpacing = _getXAxisPoints.eachSpacing;
var categoriesTextLenth = categories.map(function(item) {
return measureText(item);
});
var maxTextLength = Math.max.apply(this, categoriesTextLenth);
if (maxTextLength + 2 * config.xAxisTextPadding > eachSpacing) {
result.angle = 45 * Math.PI / 180;
result.xAxisHeight = 2 * config.xAxisTextPadding + maxTextLength * Math.sin(result.angle);
}
return result;
}
function getXAxisPoints(categories, opts, config) {
var yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth;
var spacingValid = opts.width - 2 * config.padding - yAxisTotalWidth;
var dataCount = opts.enableScroll ? Math.min(5, categories.length) : categories.length;
var eachSpacing = spacingValid / dataCount;
var xAxisPoints = [];
var startX = config.padding + yAxisTotalWidth;
var endX = opts.width - config.padding;
categories.forEach(function(item, index) {
xAxisPoints.push(startX + index * eachSpacing);
});
if (opts.enableScroll === true) {
xAxisPoints.push(startX + categories.length * eachSpacing);
} else {
xAxisPoints.push(endX);
}
return { xAxisPoints: xAxisPoints, startX: startX, endX: endX, eachSpacing: eachSpacing };
}
function measureText(text) {
var fontSize = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10;
text = String(text);
var text = text.split('');
var width = 0;
text.forEach(function(item) {
if (/[a-zA-Z]/.test(item)) {
width += 7;
} else if (/[0-9]/.test(item)) {
width += 5.5;
} else if (/\./.test(item)) {
width += 2.7;
} else if (/-/.test(item)) {
width += 3.25;
} else if (/[\u4e00-\u9fa5]/.test(item)) {
width += 10;
} else if (/\(|\)/.test(item)) {
width += 3.73;
} else if (/\s/.test(item)) {
width += 2.5;
} else if (/%/.test(item)) {
width += 8;
} else {
width += 10;
}
});
return width * fontSize / 10;
}
function drawYAxisGrid(opts, config, context) {
var spacingValid = opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight;
var eachSpacing = Math.floor(spacingValid / config.yAxisSplit);
var yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth;
var startX = config.padding + yAxisTotalWidth;
var endX = opts.width - config.padding;
var points = [];
for (var i = 0; i < config.yAxisSplit; i++) {
points.push(config.padding + eachSpacing * i);
}
points.push(config.padding + eachSpacing * config.yAxisSplit + 2);
context.beginPath();
context.setStrokeStyle(opts.yAxis.gridColor || "#cccccc");
context.setLineWidth(1);
points.forEach(function(item, index) {
context.moveTo(startX, item);
context.lineTo(endX, item);
});
context.closePath();
context.stroke();
}
function drawColumnDataPoints(series, opts, config, context) {
var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1;
var _calYAxisData = calYAxisData(series, opts, config),
ranges = _calYAxisData.ranges;
var _getXAxisPoints = getXAxisPoints(opts.categories, opts, config),
xAxisPoints = _getXAxisPoints.xAxisPoints,
eachSpacing = _getXAxisPoints.eachSpacing;
var minRange = ranges.pop();
var maxRange = ranges.shift();
context.save();
if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) {
context.translate(opts._scrollDistance_, 0);
}
series.forEach(function(eachSeries, seriesIndex) {
var data = eachSeries.data;
var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process);
points = fixColumeData(points, eachSpacing, series.length, seriesIndex, config, opts);
context.beginPath();
context.setFillStyle(eachSeries.color);
points.forEach(function(item, index) {
if (item !== null) {
var startX = item.x - item.width / 2 + 1;
var height = opts.height - item.y - config.padding - config.xAxisHeight - config.legendHeight;
context.moveTo(startX, item.y);
context.rect(startX, item.y, item.width - 2, height);
}
});
context.closePath();
context.fill();
});
series.forEach(function(eachSeries, seriesIndex) {
var data = eachSeries.data;
var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process);
points = fixColumeData(points, eachSpacing, series.length, seriesIndex, config, opts);
if (opts.dataLabel !== false && process === 1) {
drawPointText(points, eachSeries, config, context);
}
});
context.restore();
return {
xAxisPoints: xAxisPoints,
eachSpacing: eachSpacing
};
}
function drawStackedDataPoints(series, opts, config, context) {
var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1;
var _calYAxisData = calYAxisData(series, opts, config),
ranges = _calYAxisData.ranges;
var _getXAxisPoints = getXAxisPoints(opts.categories, opts, config),
xAxisPoints = _getXAxisPoints.xAxisPoints,
eachSpacing = _getXAxisPoints.eachSpacing;
var minRange = ranges.pop();
var maxRange = ranges.shift();
context.save();
if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) {
context.translate(opts._scrollDistance_, 0);
}
series.forEach(function(eachSeries, seriesIndex) {
var data = eachSeries.data;
var piles = getDataPiles(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process);
piles = fixPileData(piles, eachSpacing, series.length, seriesIndex, config, opts);
context.beginPath();
context.setFillStyle(eachSeries.color);
piles.forEach(function(items, column_index) {
if (!!items && items instanceof Array) {
items.forEach(function(item, pile_index) {
var startX = item.x - item.width / 2 + 1;
var height = opts.height - item.y - config.padding - config.xAxisHeight - config.legendHeight;
var startY = getStartY(items, item.y, pile_index);
context.moveTo(startX, startY);
context.rect(startX, startY, item.width - 2, height);
});
}
});
function getStartY(items, y, pile_index) {
for (var i = 0; i < pile_index; i++) {
y += opts.height - items[i].y - config.padding - config.xAxisHeight - config.legendHeight;
}
return y;
};
context.closePath();
context.fill();
});
series.forEach(function(eachSeries, seriesIndex) {
var data = eachSeries.data;
var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process);
points = fixColumeData(points, eachSpacing, series.length, seriesIndex, config, opts);
if (opts.dataLabel !== false && process === 1) {
drawPointText(points, eachSeries, config, context);
}
});
context.restore();
return {
xAxisPoints: xAxisPoints,
eachSpacing: eachSpacing
};
}
function drawCharts(type, opts, config, context) {
var _this = this;
var series = opts.series;
var categories = opts.categories;
series = fillSeriesColor(series, config);
var _calLegendData = calLegendData(series, opts, config),
legendHeight = _calLegendData.legendHeight;
config.legendHeight = legendHeight;
var _calYAxisData = calYAxisData(series, opts, config),
yAxisWidth = _calYAxisData.yAxisWidth;
config.yAxisWidth = yAxisWidth;
if (categories && categories.length) {
var _calCategoriesData = calCategoriesData(categories, opts, config),
xAxisHeight = _calCategoriesData.xAxisHeight,
angle = _calCategoriesData.angle;
config.xAxisHeight = xAxisHeight;
config._xAxisTextAngle_ = angle;
}
var duration = opts.animation ? 1000 : 0;
this.animationInstance && this.animationInstance.stop();
switch (type) {
case 'column':
this.animationInstance = new Animation({
timing: 'easeIn',
duration: duration,
onProcess: function onProcess(process) {
drawYAxisGrid(opts, config, context);
var _drawColumnDataPoints = drawColumnDataPoints(series, opts, config, context, process),
xAxisPoints = _drawColumnDataPoints.xAxisPoints,
eachSpacing = _drawColumnDataPoints.eachSpacing;
_this.chartData.xAxisPoints = xAxisPoints;
_this.chartData.eachSpacing = eachSpacing;
drawXAxis(categories, opts, config, context);
drawLegend(opts.series, opts, config, context);
drawYAxis(series, opts, config, context);
drawCanvas(opts, context);
},
onAnimationFinish: function onAnimationFinish() {
_this.event.trigger('renderComplete');
}
});
break;
case 'stacked':
this.animationInstance = new Animation({
timing: 'easeIn',
duration: duration,
onProcess: function onProcess(process) {
drawYAxisGrid(opts, config, context);
var _drawColumnDataPoints = drawStackedDataPoints(series, opts, config, context, process),
xAxisPoints = _drawColumnDataPoints.xAxisPoints,
eachSpacing = _drawColumnDataPoints.eachSpacing;
_this.chartData.xAxisPoints = xAxisPoints;
_this.chartData.eachSpacing = eachSpacing;
drawXAxis(categories, opts, config, context);
drawLegend(opts.series, opts, config, context);
drawYAxis(series, opts, config, context);
drawCanvas(opts, context);
},
onAnimationFinish: function onAnimationFinish() {
_this.event.trigger('renderComplete');
}
});
break;
}
}