javascript - 如何绘制堆/堆叠条形图?

标签 javascript highcharts chart.js data-visualization canvasjs

如何像 Highcharts ( https://www.highcharts.com/demo/column-stacked ) 或 CanvasJS ( https://canvasjs.com/javascript-charts/stacked-column-100-chart/ ) 中那样绘制堆/堆叠条形图?我正在制作一个需要这样图表的微信微程序,但我很难通过查看他们的源代码(它们很大)来弄清楚如何进行数据可视化。而且我无法直接使用为桌面和移动浏览器构建的 JavaScript 组件,因为微信微程序的语法不同。

我喜欢这个教程:http://www.williammalone.com/articles/html5-canvas-javascript-bar-graph/

但我还没有弄清楚如何从常规条形图创建堆积条形图。

我简化了代码,使其更易于阅读:

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;
  }
}

最佳答案

如果您可以计算出每个部分应占总条形的百分比,则可以使用这些值来绘制适当缩放的矩形。这是此概念的一个非常简单的示例,展示了如何对一个柱执行此操作:

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>

关于javascript - 如何绘制堆/堆叠条形图?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47950568/

相关文章:

javascript - 如何在向下钻取后创建具有堆叠柱的图表?

javascript - 未从 JSON 文件获取数据

javascript - Angular react 形式 - 绑定(bind) [value] 之外的另一个属性

javascript - AngularJS - ng-app 如果不为空则无法工作

javascript - HighCharts 在栏上放置标签

javascript - Highcharts - 堆叠完整系列(条形图)

angular - 如何在 Angular Chart.js 中以异步方式更新数据图表?

chart.js - 将标签放置在单杠下方

jquery 使用 Chart.js 加载

javascript - 如何集成jade和reactjs组件