1
votes

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;
  }
}
1
Can you clarify the question? What do you mean by "can't directly use browser javascript plugin"? Is the issue that you're having trouble adapting your data to the example charts you linked, or that you want to recreate a similar style chart from scratch?roctothorpe
I need to recreate a similar style chart from scratch.Aero Wang

1 Answers

0
votes

If you can figure out the percentage of the total bar that each section should take up, you can use those values to draw appropriately scaled rectangles. Here's a very simple example of this concept showing how you would do this for one bar:

var myCanvas = document.getElementById("myCanvas");
var ctx = myCanvas.getContext("2d");
var percents = [0.3, 0.5, 0.2];
var colors = ['red', 'purple', 'green']; 
var barHeight = 300; 
var barWidth = 100; 

var currY = 0;
for (i = 0; i < percents.length; i++) {
  ctx.beginPath();
  ctx.rect(0, currY, barWidth, percents[i]*barHeight);
  ctx.fillStyle = colors[i];
  ctx.fill();
  currY += percents[i]*barHeight;
}
<canvas id="myCanvas" width="300" height="300" style="border:1px solid #d3d3d3;"></canvas>