前端图表chart

highchart源码学习

2016-04-27  本文已影响672人  cuiy245

一、highchart的组成

大致浏览一遍源代码,Highchart作为一个对象,会有大致以下几个构造函数。


Highchart = {
 Tooltip: function () {},
 Pointer: function () {},
 Legend: function () {},
 Chart: function () {},
 Series: function () {}
 ...
}
  1. 每个构造函数通过prototype添加一些操作函数
    -这里可以一一写几个重要的函数
    -这里可以一一写几个重要的函数
  2. 每个构造函数都接受chart、option做为参数,这样取参数和操作chart对象就很方便。

二、chart对象的创建

  1. 通过 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')];
            }
        };
    }
  1. 通过getArgs()收集参数,得到:
  1. 进行chart.init()初始化。
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()```
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.propFromSeries();
 chart.getAxes();

遍历配置option中的所有x轴和y轴的配置信息,为每一个轴创建Axis对象

each(optionsArray, function (axisOptions) {
        new Axis(chart, axisOptions); 
});

现在重点说一下Axis对象的创建,见第三部分

 each(options.series || [], function (serieOptions) {
          chart.initSeries(serieOptions);
});

-为series[xData]、series[yData]存放x数据和y数据
-将y数据存放之series.option.data中
-chartSeries存放series对象

chart.linkSeries();
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()

  1. 画-图标题
chart.setTitle();
  1. 画-legend
chart.legend = new Legend(chart, options.legend);
  1. 画-图的大小尺寸
chart.setChartSize();
  1. 画-范围,根据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

上一篇下一篇

猜你喜欢

热点阅读