highchart源码学习
一、highchart的组成
大致浏览一遍源代码,Highchart作为一个对象,会有大致以下几个构造函数。
Highchart = {
Tooltip: function () {},
Pointer: function () {},
Legend: function () {},
Chart: function () {},
Series: function () {}
...
}
- 每个构造函数通过prototype添加一些操作函数
-这里可以一一写几个重要的函数
-这里可以一一写几个重要的函数 - 每个构造函数都接受chart、option做为参数,这样取参数和操作chart对象就很方便。
二、chart对象的创建
- 通过 new Highchart.Chart()调用了构造函数,创建实例对象Highcharts.Chart
if (win.jQuery) {
win.jQuery.fn.highcharts = function () {
var args = [].slice.call(arguments);
if (this[0]) { // this[0] is the renderTo div
// Create the chart
if (args[0]) {
new Highcharts[ // eslint-disable-line no-new
isString(args[0]) ? args.shift() : 'Chart' // Constructor defaults to Chart
](this[0], args[0], args[1]);
return this;
}
// When called without parameters or with the return argument, return an existing chart
return charts[attr(this[0], 'data-highcharts-chart')];
}
};
}
- 通过getArgs()收集参数,得到:
- 参数数组args=[div#container, 用户配置的参数对象, ...];
- renderTo:渲染图表的地方
- 进行chart.init()初始化。
- 得到chart对象的相关属性
var chart = this;
this.userOptions // 用户配置的参数
this.margin
this.spacing // 图表四周的间距
this.option // merge后的最终完整配置,具体有哪些配置项见highchart官网
this.axes // 存放刻度值?
this.series = []
charts // 图表数组,存放所有图表
chart.xAxis = [] // 存放图表的所有x轴
chart.yAxis = [] // 存放图表的所有y轴
chart.animation // 是否有图表动画
- 开始chart.firstRender()
chart.firstRender()```
4. firstRender做以下工作:
- 获取container,并给container添加一些属性:例如“data-highcharts-chart="0"”
chart.getContainer();
-为container添加了width、height(默认400px)等css样式
-通过SVGrender()画svg,同时对svg添加了一些说明等
- 重置margin
- 设置chart图表的尺寸
```javascript
chart.setChartSize();
-设置了chart.clipBox、chart.plotBox
- 设置chart的series属性(目前没觉得有用?)
chart.propFromSeries();
- 获取刻度值(坐标轴)
chart.getAxes();
遍历配置option中的所有x轴和y轴的配置信息,为每一个轴创建Axis对象
each(optionsArray, function (axisOptions) {
new Axis(chart, axisOptions);
});
现在重点说一下Axis对象的创建,见第三部分
- 初始化series
each(options.series || [], function (serieOptions) {
chart.initSeries(serieOptions);
});
-为series[xData]、series[yData]存放x数据和y数据
-将y数据存放之series.option.data中
-chartSeries存放series对象
- 链接series
chart.linkSeries();
- 创建Pointer对象
if (Highcharts.Pointer) {
chart.pointer = new Pointer(chart, options);
}
-创建了tooltip对象
if (Highcharts.Tooltip && options.tooltip.enabled) {
chart.tooltip = new Tooltip(chart, options.tooltip);
this.followTouchMove = pick(options.tooltip.followTouchMove, true);
}
this.setDOMEvents(); // 为point对象绑定了事件
此时,请注意,已经将图表应该具有的DOM节点就都有了,包括:chart图大小、xy轴、xy轴label、点坐标、柱状图条、tooltip提示、图例legend,那么就要开始往这些节点中添加真正的数字或者样式了。
- 画图表(见第四部分)
chart.render();
三、Axis对象的创建
1.Axis对象的一些属性必须知道:
axis = this; //x轴的信息配置
this.option = {
categories:Array[13] // x轴上的标注
dateTimeLabelFormats:Object
endOnTick:false
gridLineColor:"#D8D8D8"
index:0
isX:true
labels:Object
lineColor:"#C0D0E0"
lineWidth:1
maxPadding:0.01
minPadding:0.01
minorGridLineColor:"#E0E0E0"
minorGridLineWidth:1
minorTickColor:"#A0A0A0"
minorTickLength:2
minorTickPosition:"outside"
startOfWeek:1
startOnTick:false
tickColor:"#C0D0E0"
tickLength:10
tickPixelInterval:100
tickPosition:"outside"
tickmarkPlacement:"between"
title:Object
type:"linear"
__proto__:Object
}
axis.minPixelPadding = 0;
axis.categories // 所有x轴上的分类
axis.minRange = axis.userMinRange = options.minRange || options.maxZoom;
axis.range = options.range;
axis.offset = options.offset || 0;
axis = this; // y轴配置信息
this.option = {
alternateGridColor:null
dateTimeLabelFormats:Object
endOnTick:true
gridLineColor:"rgba(151, 151, 151, .1)"
gridLineWidth:0
index:0
labels:Object
lineColor:"#C0D0E0"
lineWidth:0
maxPadding:0.05
min:14
minPadding:0
minorGridLineColor:"rgba(255,255,255,0.07)"
minorGridLineWidth:1
minorTickColor:"#A0A0A0"
minorTickInterval:null
minorTickLength:2
minorTickPosition:"outside"
opposite:false
showLastLabel:true
stackLabels:Object
startOfWeek:1
startOnTick:true
tickColor:"#C0D0E0"
tickLength:10
tickPixelInterval:72
tickPosition:"outside"
tickWidth:0
tickmarkPlacement:"between"
title:Object
type:"linear"
__proto__:Object
}
向chart.axes数组添加所有的Highchart.Axis坐标轴对象;
向chart[xAxis]存放x轴信息(这里是一个x轴)、chart[yAxis]存放y轴信息(这里是两个y轴)
四、chart.render()
- 画-图标题
chart.setTitle();
- 画-legend
chart.legend = new Legend(chart, options.legend);
- 画-图的大小尺寸
chart.setChartSize();
- 画-范围,根据data中的最大值与最小值
each(axes, function (axis) {
axis.setScale();
});
-计算刻度线数目
可以看到,刻度线数目计算出来之后,与4比较大小,比4小就设成5,比4大就是本身。
getTickAmount: function () {
var options = this.options,
tickAmount = options.tickAmount,
tickPixelInterval = options.tickPixelInterval;
if (!defined(options.tickInterval) && this.len < tickPixelInterval && !this.isRadial &&
!this.isLog && options.startOnTick && options.endOnTick) {
tickAmount = 2;
}
if (!tickAmount && this.alignToOthers()) {
// Add 1 because 4 tick intervals require 5 ticks (including first and last)
tickAmount = mathCeil(this.len / tickPixelInterval) + 1;
}
// For tick amounts of 2 and 3, compute five ticks and remove the intermediate ones. This
// prevents the axis from adding ticks that are too far away from the data extremes.
if (tickAmount < 4) { //感觉这个4是自己定的呀?这里还说明一下这么做是为了防止极端数据里轴太远,没太明白4怎么来的
this.finalTickAmt = tickAmount;
tickAmount = 5;
}
this.tickAmount = tickAmount;
}
-设置y轴的最大值或者最小值
if (!categories && !axis.axisPointRange && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) {
// 原始y数据最大值 - 原始y数据最小数
length = axis.max - axis.min;
if (length) {
if (!defined(hardMin) && minPadding) {
axis.min -= length * minPadding;
}
if (!defined(hardMax) && maxPadding) {
// 获取y轴的最大值,可能带小数点
axis.max += length * maxPadding;
}
}
}
-获取间距值
对于tickPixelInterval,默认x轴为100,y轴为72
// get tickInterval
if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) {
axis.tickInterval = 1;
} else if (isLinked && !tickIntervalOption &&
tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) {
axis.tickInterval = tickIntervalOption = axis.linkedParent.tickInterval;
} else {
//获取最初的tickInterval,因为可能会带小数点,所以需要后面处理
axis.tickInterval = pick(
tickIntervalOption,
this.tickAmount ? ((axis.max - axis.min) / mathMax(this.tickAmount - 1, 1)) : undefined,
categories ? // for categoried axis, 1 is default, for linear axis use tickPix
1 :
// don't let it be more than the data range
(axis.max - axis.min) * tickPixelIntervalOption / mathMax(axis.len, tickPixelIntervalOption)
);
}
if (!isDatetimeAxis && !isLog && !tickIntervalOption) {
// 将原始的tickInterval处理为整数
axis.tickInterval = normalizeTickInterval(
axis.tickInterval,
null,
getMagnitude(axis.tickInterval),
// If the tick interval is between 0.5 and 5 and the axis max is in the order of
// thousands, chances are we are dealing with years. Don't allow decimals. #3363.
pick(options.allowDecimals, !(axis.tickInterval > 0.5 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999)),
!!this.tickAmount
);
}
其中格式化tickInterval的函数normalizeTickInterval会有1、2、2.5、5、10共五个档来得到interval,怎么会有这样的档,目前还不清楚。
function normalizeTickInterval(interval, multiples, magnitude, allowDecimals, preventExceed) {
var normalized,
i,
retInterval = interval;
// round to a tenfold of 1, 2, 2.5 or 5
magnitude = pick(magnitude, 1);
normalized = interval / magnitude;
// multiples for a linear scale
if (!multiples) {
multiples = [1, 2, 2.5, 5, 10];
// the allowDecimals option
if (allowDecimals === false) {
if (magnitude === 1) {
multiples = [1, 2, 5, 10];
} else if (magnitude <= 0.1) {
multiples = [1 / magnitude];
}
}
}
// normalize the interval to the nearest multiple
for (i = 0; i < multiples.length; i++) {
retInterval = multiples[i];
if ((preventExceed && retInterval * magnitude >= interval) || // only allow tick amounts smaller than natural
(!preventExceed && (normalized <= (multiples[i] + (multiples[i + 1] || multiples[i])) / 2))) {
break;
}
}
// multiply back to the correct magnitude
retInterval *= magnitude;
return retInterval;
}
-设置好刻度线位置
setTickPositions: function () {
var options = this.options,
tickPositions,
tickPositionsOption = options.tickPositions,
tickPositioner = options.tickPositioner,
startOnTick = options.startOnTick,
endOnTick = options.endOnTick,
single;
// Set the tickmarkOffset
this.tickmarkOffset = (this.categories && options.tickmarkPlacement === 'between' &&
this.tickInterval === 1) ? 0.5 : 0; // #3202
// get minorTickInterval
this.minorTickInterval = options.minorTickInterval === 'auto' && this.tickInterval ?
this.tickInterval / 5 : options.minorTickInterval;
// Find the tick positions
this.tickPositions = tickPositions = tickPositionsOption && tickPositionsOption.slice(); // Work on a copy (#1565)
if (!tickPositions) {
if (this.isDatetimeAxis) {
tickPositions = this.getTimeTicks(
this.normalizeTimeTickInterval(this.tickInterval, options.units),
this.min,
this.max,
options.startOfWeek,
this.ordinalPositions,
this.closestPointRange,
true
);
} else if (this.isLog) {
tickPositions = this.getLogTickPositions(this.tickInterval, this.min, this.max);
} else {
tickPositions = this.getLinearTickPositions(this.tickInterval, this.min, this.max); //获取刻度值
}
// Too dense ticks, keep only the first and last (#4477)
if (tickPositions.length > this.len) {
tickPositions = [tickPositions[0], tickPositions.pop()];
}
this.tickPositions = tickPositions;
// Run the tick positioner callback, that allows modifying auto tick positions.
if (tickPositioner) {
tickPositioner = tickPositioner.apply(this, [this.min, this.max]);
if (tickPositioner) {
this.tickPositions = tickPositions = tickPositioner;
}
}
}
if (!this.isLinked) {
// reset min/max or remove extremes based on start/end on tick
this.trimTicks(tickPositions, startOnTick, endOnTick);
// When there is only one point, or all points have the same value on this axis, then min
// and max are equal and tickPositions.length is 0 or 1. In this case, add some padding
// in order to center the point, but leave it with one tick. #1337.
if (this.min === this.max && defined(this.min) && !this.tickAmount) {
// Substract half a unit (#2619, #2846, #2515, #3390)
single = true;
this.min -= 0.5;
this.max += 0.5;
}
this.single = single;
if (!tickPositionsOption && !tickPositioner) {
this.adjustTickAmount();
}
}
}
其中getLinearTickPositions()函数可以算出刻度线数组,例如[0,2500,5000,7500,10000,12500],因为要包含所有的series,就需要去比min还小的数roundedMin,比max还大的数roundedMax,(这里的min和max在前面的代码片段“设置y轴的最大值或者最小值”中已经求出来了),所以就会多出两个刻度。例如,series中最大数为9525,按照2500的tickInterval来算,只要到10000即可,但是前面算出来axis.max为10000.55,所以10000不够,需要再加一个2500,成为12500,这样就多出两个刻度线来。
getLinearTickPositions: function (tickInterval, min, max) {
var pos,
lastPos,
roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval),
roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval),
tickPositions = [];
// For single points, add a tick regardless of the relative position (#2662)
if (min === max && isNumber(min)) {
return [min];
}
// Populate the intermediate values
pos = roundedMin;
while (pos <= roundedMax) {
// Place the tick on the rounded value
tickPositions.push(pos);
// Always add the raw tickInterval, not the corrected one.
pos = correctFloat(pos + tickInterval);
// If the interval is not big enough in the current min - max range to actually increase
// the loop variable, we need to break out to prevent endless loop. Issue #619
if (pos === lastPos) {
break;
}
// Record the last value
lastPos = pos;
}
return tickPositions;
}
-调节刻度线数目
当刻度线过多时,将tickInterval加倍,来减少刻度线数目,重新得出刻度线数组[0,5000,10000,15000],但又因为刻度线数目小于5,所以需要子啊push一个元素构成5个元素,即变为[0,5000,10000,15000,20000]。
adjustTickAmount: function () {
var tickInterval = this.tickInterval,
tickPositions = this.tickPositions,
tickAmount = this.tickAmount,
finalTickAmt = this.finalTickAmt,
currentTickAmount = tickPositions && tickPositions.length,
i,
len;
if (currentTickAmount < tickAmount) {
while (tickPositions.length < tickAmount) {
tickPositions.push(correctFloat(
tickPositions[tickPositions.length - 1] + tickInterval
));
}
this.transA *= (currentTickAmount - 1) / (tickAmount - 1);
this.max = tickPositions[tickPositions.length - 1];
// We have too many ticks, run second pass to try to reduce ticks
} else if (currentTickAmount > tickAmount) {
this.tickInterval *= 2; //间距加倍
this.setTickPositions();
}
// The finalTickAmt property is set in getTickAmount
if (defined(finalTickAmt)) {
i = len = tickPositions.length;
while (i--) {
if (
(finalTickAmt === 3 && i % 2 === 1) || // Remove every other tick
(finalTickAmt <= 2 && i > 0 && i < len - 1) // Remove all but first and last
) {
tickPositions.splice(i, 1);
}
}
this.finalTickAmt = UNDEFINED;
}
}
5.画-图表的border和background
chart.drawChartBox();
6.画-xy轴
// Axes
if (chart.hasCartesianSeries) {
each(axes, function (axis) {
if (axis.visible) {
axis.render();
}
});
}
7.画-series