echarts源码解读《三》:echarts源码之Component分析

分析完echarts简单的结构之后,我们就进入到了Component源码解读,在这篇博文中,我将主要介绍各Component的渲染过程以及其使用的zrender graphic。希望和小伙伴们一起进步呀!!加油!!

前言

echarts对Component的定义呢,可以认为是除Series外的其他配置项,在这篇博文中我们将探讨title(图标标题)、legend(图例组件)、AxisPointer(坐标轴指示器)、坐标系以及坐标轴这些Component。

用户通过传递option对象来设置相应的Component。

在这部分中echarts采用了Model以及View的架构来管理Component:

  • Model:model/Component.js 管理Component数据
  • View:view/Component.js 负责渲染Component视图

Model层

model/Component.js。Component扩展自Model(Model是Echarts中最基本的元素,其定义了mergeOption等方法,混合了LineStyle、AraeStyle、ItemStyle以及TextStyle),重写了init及mergeOption等方法,定义了mergeDefaultAndTheme、getDefaultOption等方法以及defaultOption(Component默认配置)、componentIndex等属性。

View层

view/Component.js。Component中定义了group等属性以及init、render、dispose等方法。

快速扩展

在echarts中定义了extendComponentModel以及extendComponentView方法,可以让Component对Model以及View进行快速扩展

1
2
3
4
5
6
7
export function extendComponentModel(opts) {
return ComponentModel.extend(opts);
}

export function extendComponentView(opts) {
return ComponentView.extend(opts);
}

Title

title是我们在使用echarts图表时设置的标题组件,简单的🌰:

1
2
3
4
title: {
left: 'center',
text: '例子Title',
}

title文件中Model部分通过extendComponentModel方法扩展自Component Model,重写了defaultOption属性,用于设置title的默认option。

View部分通过extendComponentView方法扩展Component View,重写了render方法对title进行渲染,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
render: function (titleModel, ecModel, api) {
...
// 通过zrender的Text图形新建文本元素
var textEl = new graphic.Text({
style: graphic.setTextStyle({}, textStyleModel, {
text: titleModel.get('text'),
textFill: textStyleModel.getTextColor()
}, {disableBox: true}),
z2: 10
});

var textRect = textEl.getBoundingRect();

var subText = titleModel.get('subtext');
// 创建subText元素
var subTextEl = new graphic.Text({
style: graphic.setTextStyle({}, subtextStyleModel, {
text: subText,
textFill: subtextStyleModel.getTextColor(),
y: textRect.height + titleModel.get('itemGap'),
textVerticalAlign: 'top'
}, {disableBox: true}),
z2: 10
});
...

group.add(textEl);
subText && group.add(subTextEl);
// If no subText, but add subTextEl, there will be an empty line.
...
// 设定title的位置
if (!textAlign) {
// Align left if title is on the left. center and right is same
textAlign = titleModel.get('left') || titleModel.get('right');
if (textAlign === 'middle') {
textAlign = 'center';
}
// Adjust layout by text align
if (textAlign === 'right') {
layoutRect.x += layoutRect.width;
}
else if (textAlign === 'center') {
layoutRect.x += layoutRect.width / 2;
}
}
if (!textBaseline) {
textBaseline = titleModel.get('top') || titleModel.get('bottom');
if (textBaseline === 'center') {
textBaseline = 'middle';
}
if (textBaseline === 'bottom') {
layoutRect.y += layoutRect.height;
}
else if (textBaseline === 'middle') {
layoutRect.y += layoutRect.height / 2;
}

textBaseline = textBaseline || 'top';
}

group.attr('position', [layoutRect.x, layoutRect.y]);
var alignStyle = {
textAlign: textAlign,
textVerticalAlign: textBaseline
};
// 设置文本样式
textEl.setStyle(alignStyle);
subTextEl.setStyle(alignStyle);

// 渲染背景
// Get groupRect again because textAlign has been changed
groupRect = group.getBoundingRect();
var padding = layoutRect.margin;
var style = titleModel.getItemStyle(['color', 'opacity']);
style.fill = titleModel.get('backgroundColor');
var rect = new graphic.Rect({
shape: {
x: groupRect.x - padding[3],
y: groupRect.y - padding[0],
width: groupRect.width + padding[1] + padding[3],
height: groupRect.height + padding[0] + padding[2],
r: titleModel.get('borderRadius')
},
style: style,
silent: true
});
graphic.subPixelOptimizeRect(rect);

group.add(rect);
}

title文本渲染主要是通过zrender graphic中的Text进行渲染的,通过zrender Style中定义的setStyle方法对元素进行样式设定。

Legend

legend为echarts的图例组件,简单的🌰:

1
2
3
legend: {
data:['支出','收入']
},

展示效果图如:

legend

legend分为两种:plain(平面)和scroll(可滚动),接下来我将分别讲解两种类型的legend是如何作用的。

Plain

legend.plain为平面的legend图例组件,主要包括LegendAction、LegendModel、LegendView文件。

LegendAction

legendAction文件中注册了legend对外API: legendToggleSelect、legendSelect以及legendUnSelect,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
echarts.registerAction(
'legendToggleSelect', 'legendselectchanged',
zrUtil.curry(legendSelectActionHandler, 'toggleSelected')
);

echarts.registerAction(
'legendSelect', 'legendselected',
zrUtil.curry(legendSelectActionHandler, 'select')
);

echarts.registerAction(
'legendUnSelect', 'legendunselected',
zrUtil.curry(legendSelectActionHandler, 'unSelect')
);

LegendFilter

legendFilter文件中实现了数据过滤,也就是当legend的isSelected方法返回为false时,则与legend name相同的series的数据则不显示,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default function (ecModel) {
var legendModels = ecModel.findComponents({
mainType: 'legend'
});
if (legendModels && legendModels.length) {
ecModel.filterSeries(function (series) {
// If in any legend component the status is not selected.
// Because in legend series is assumed selected when it is not in the legend data.
for (var i = 0; i < legendModels.length; i++) {
if (!legendModels[i].isSelected(series.name)) {
return false;
}
}
return true;
});
}
}

LegendModel

legendModel通过extendComponentModel方法扩展自Component Model,重写了defaultOption属性,重写了init方法,定义了select、unSelect、toggleSelected以及isSelected等方法。

LegendView

legendView通过extendComponentView方法扩展自Component View,重写了init以及render方法对legend进行渲染,主要渲染的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
render: function (legendModel, ecModel, api) {
...
// renderInner中调用了createItem方法创建Legend item
// 并绑定了click、mouseover、mouseout等事件
this.renderInner(itemAlign, legendModel, ecModel, api);
...
};
_createItem: function (
name, dataIndex, itemModel, legendModel,
legendSymbolType, symbolType,
itemAlign, color, selectMode
) {
...
// 使用util/symbol导出的createSymbol方法,创建相应Type的Symbol图形
itemGroup.add(createSymbol(
legendSymbolType,
0,
0,
itemWidth,
itemHeight,
isSelected ? color : inactiveColor,
// symbolKeepAspect default true for legend
symbolKeepAspect == null ? true : symbolKeepAspect
));
...
// 渲染legend文本
itemGroup.add(new graphic.Text({
style: graphic.setTextStyle({}, textStyleModel, {
text: content,
x: textX,
y: itemHeight / 2,
textFill: isSelected ? textStyleModel.getTextColor() : inactiveColor,
textAlign: textAlign,
textVerticalAlign: 'middle'
})
}));

// Add a invisible rect to increase the area of mouse hover
// 添加legend的tooltip效果
var hitRect = new graphic.Rect({
shape: itemGroup.getBoundingRect(),
invisible: true,
tooltip: tooltipModel.get('show') ? zrUtil.extend({
content: name,
// Defaul formatter
formatter: legendGlobalTooltipModel.get('formatter', true) || function () {
return name;
},
formatterParams: {
componentType: 'legend',
legendIndex: legendModel.componentIndex,
name: name,
$vars: ['name']
}
}, tooltipModel.option) : null
});
itemGroup.add(hitRect);
...
this.getContentGroup().add(itemGroup);
...
return itemGroup;
}

Scroll

legend.scroll为可滚动的legend图例组件,主要包括ScrollableLegendAction、ScrollableLegendModel、ScrollableLegendView文件。

ScrollableLegendAction

ScrollableLegendAction文件中注册了legend对外API:legendScroll,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
echarts.registerAction(
'legendScroll', 'legendscroll',
function (payload, ecModel) {
var scrollDataIndex = payload.scrollDataIndex;

scrollDataIndex != null && ecModel.eachComponent(
{mainType: 'legend', subType: 'scroll', query: payload},
function (legendModel) {
legendModel.setScrollDataIndex(scrollDataIndex);
}
);
}
);

ScrollableLegendModel

ScrollableLegendModel通过extend方法扩展自LegendMode,重写了defaultOption属性,重写了init方法,定义了setScrollDataIndex以及getOrient等方法。

ScrollableLegendView

ScrollableLegendView通过extend方法扩展自LegendView,重写了init以及renderInner方法对scrollable legend进行渲染,主要渲染的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
renderInner: function (itemAlign, legendModel, ecModel, api) {
var me = this;

// Render content items.
// 调用LegenView中renderInner渲染基本的legend视图
ScrollableLegendView.superCall(this, 'renderInner', itemAlign, legendModel, ecModel, api);

// 创建分页器
var controllerGroup = this._controllerGroup;

// FIXME: support be 'auto' adapt to size number text length,
// e.g., '3/12345' should not overlap with the control arrow button.
var pageIconSize = legendModel.get('pageIconSize', true);
if (!zrUtil.isArray(pageIconSize)) {
pageIconSize = [pageIconSize, pageIconSize];
}

createPageButton('pagePrev', 0);

var pageTextStyleModel = legendModel.getModel('pageTextStyle');
controllerGroup.add(new graphic.Text({
name: 'pageText',
style: {
textFill: pageTextStyleModel.getTextColor(),
font: pageTextStyleModel.getFont(),
textVerticalAlign: 'middle',
textAlign: 'center'
},
silent: true
}));

createPageButton('pageNext', 1);
// 创建prev&next button
function createPageButton(name, iconIdx) {
var pageDataIndexName = name + 'DataIndex';
var icon = graphic.createIcon(
legendModel.get('pageIcons', true)[legendModel.getOrient().name][iconIdx],
{
// Buttons will be created in each render, so we do not need
// to worry about avoiding using legendModel kept in scope.
onclick: zrUtil.bind(
me._pageGo, me, pageDataIndexName, legendModel, api
)
},
{
x: -pageIconSize[0] / 2,
y: -pageIconSize[1] / 2,
width: pageIconSize[0],
height: pageIconSize[1]
}
);
icon.name = name;
controllerGroup.add(icon);
}
}

AxisPointer

AxisPointer为echarts中的坐标指示器,包括直角坐标以及极坐标等。

AxisPointer.js

axisPointer.js文件中注册了axisPointer对外api,主要代码如下:

1
2
3
4
5
echarts.registerAction({
type: 'updateAxisPointer',
event: 'updateAxisPointer',
update: ':updateAxisPointer'
}, axisTrigger);

存储coordSysAxesInfo信息,主要代码如下:

1
2
3
4
5
6
echarts.registerProcessor(echarts.PRIORITY.PROCESSOR.STATISTIC, function (ecModel, api) {
// Build axisPointerModel, mergin tooltip.axisPointer model for each axis.
// allAxesInfo should be updated when setOption performed.
ecModel.getComponent('axisPointer').coordSysAxesInfo
= axisPointerModelHelper.collect(ecModel, api);
});

AxisPointerModel

AxisPointerModel通过extendComponentModel方法扩展自Component Model,重写了defaultOption属性。

AxisPointerView

AxisPointerView过extendComponentModel方法扩展自Component View,重写了render、remove以及dispose方法。

BaseAxisPointer

BaseAxisPointer为CartesianAxisPointer、PolarAxisPointer等的基类,重写了render以及renderHandler等方法。renderHandler主要是定义move、drag等情况时视图更新方法。

render主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
render: function (axisModel, axisPointerModel, api, forceRender) {
...
var elOption = {};
// 调用子类的makeElOption方法
this.makeElOption(elOption, value, axisModel, axisPointerModel, api);
...

// 创建pointer
this.createPointerEl(group, elOption, axisModel, axisPointerModel);
// 渲染label元素
this.createLabelEl(group, elOption, axisModel, axisPointerModel);
...
updateMandatoryProps(group, axisPointerModel, true);

// 渲染handler
this._renderHandle(value);
}
createPointerEl: function (group, elOption, axisModel, axisPointerModel) {
var pointerOption = elOption.pointer;
if (pointerOption) {
// 通过graphic创建
var pointerEl = inner(group).pointerEl = new graphic[pointerOption.type](
clone(elOption.pointer)
);
group.add(pointerEl);
}
}
createLabelEl: function (group, elOption, axisModel, axisPointerModel) {
if (elOption.label) {
var labelEl = inner(group).labelEl = new graphic.Rect(
clone(elOption.label)
);

group.add(labelEl);
updateLabelShowHide(labelEl, axisPointerModel);
}
}

CartesianAxisPointer

CartesianAxisPointer使用extend方法扩展自BaseAxisPointer,重写了makeElOption、getHandleTransform以及updateHandleTransform方法。makeElOption主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
makeElOption: function (elOption, value, axisModel, axisPointerModel, api) {
var axis = axisModel.axis;
var grid = axis.grid;
var axisPointerType = axisPointerModel.get('type');
var otherExtent = getCartesian(grid, axis).getOtherAxis(axis).getGlobalExtent();
var pixelValue = axis.toGlobalCoord(axis.dataToCoord(value, true));

if (axisPointerType && axisPointerType !== 'none') {
var elStyle = viewHelper.buildElStyle(axisPointerModel);
var pointerOption = pointerShapeBuilder[axisPointerType](
axis, pixelValue, otherExtent, elStyle
);
pointerOption.style = elStyle;
elOption.graphicKey = pointerOption.type;
elOption.pointer = pointerOption;
}

var layoutInfo = cartesianAxisHelper.layout(grid.model, axisModel);
viewHelper.buildCartesianSingleLabelElOption(
value, elOption, layoutInfo, axisModel, axisPointerModel, api
);
}
var pointerShapeBuilder = {
line: function (axis, pixelValue, otherExtent, elStyle) {
var targetShape = viewHelper.makeLineShape(
[pixelValue, otherExtent[0]],
[pixelValue, otherExtent[1]],
getAxisDimIndex(axis)
);
graphic.subPixelOptimizeLine({
shape: targetShape,
style: elStyle
});
return {
type: 'Line',
shape: targetShape
};
},

shadow: function (axis, pixelValue, otherExtent, elStyle) {
var bandWidth = Math.max(1, axis.getBandWidth());
var span = otherExtent[1] - otherExtent[0];
return {
type: 'Rect',
shape: viewHelper.makeRectShape(
[pixelValue - bandWidth / 2, otherExtent[0]],
[bandWidth, span],
getAxisDimIndex(axis)
)
};
}
};

SingleAxisPointer

SingleAxisPointer同CartesianAxisPointer。

PolarAxisPointer

PolarAxisPointer使用extend扩展自BaseAxisPointer,重写了makeElOption方法,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
makeElOption: function (elOption, value, axisModel, axisPointerModel, api) {
var axis = axisModel.axis;

if (axis.dim === 'angle') {
this.animationThreshold = Math.PI / 18;
}

var polar = axis.polar;
var otherAxis = polar.getOtherAxis(axis);
var otherExtent = otherAxis.getExtent();

var coordValue;
coordValue = axis['dataTo' + formatUtil.capitalFirst(axis.dim)](value);

var axisPointerType = axisPointerModel.get('type');
if (axisPointerType && axisPointerType !== 'none') {
var elStyle = viewHelper.buildElStyle(axisPointerModel);
var pointerOption = pointerShapeBuilder[axisPointerType](
axis, polar, coordValue, otherExtent, elStyle
);
pointerOption.style = elStyle;
elOption.graphicKey = pointerOption.type;
elOption.pointer = pointerOption;
}

var labelMargin = axisPointerModel.get('label.margin');
var labelPos = getLabelPosition(value, axisModel, axisPointerModel, polar, labelMargin);
viewHelper.buildLabelElOption(elOption, axisModel, axisPointerModel, api, labelPos);
}
var pointerShapeBuilder = {

line: function (axis, polar, coordValue, otherExtent, elStyle) {
return axis.dim === 'angle'
? {
type: 'Line',
shape: viewHelper.makeLineShape(
polar.coordToPoint([otherExtent[0], coordValue]),
polar.coordToPoint([otherExtent[1], coordValue])
)
}
: {
type: 'Circle',
shape: {
cx: polar.cx,
cy: polar.cy,
r: coordValue
}
};
},

shadow: function (axis, polar, coordValue, otherExtent, elStyle) {
var bandWidth = Math.max(1, axis.getBandWidth());
var radian = Math.PI / 180;

return axis.dim === 'angle'
? {
type: 'Sector',
shape: viewHelper.makeSectorShape(
polar.cx, polar.cy,
otherExtent[0], otherExtent[1],
// In ECharts y is negative if angle is positive
(-coordValue - bandWidth / 2) * radian,
(-coordValue + bandWidth / 2) * radian
)
}
: {
type: 'Sector',
shape: viewHelper.makeSectorShape(
polar.cx, polar.cy,
coordValue - bandWidth / 2,
coordValue + bandWidth / 2,
0, Math.PI * 2
)
};
}
};

坐标轴

公共文件

在直角坐标系以及极坐标系等坐标轴组件中,存在着多个公共文件。

AxisView

AxisView通过extendComponentView方法扩展自Component Model,重写了render、remove以及dispose方法,定义了updateAxisPointer方法。

AxisBuilder

AxisBuilder中定义了axisLine、axisTickLabel以及axisName的渲染方法,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
var builders = {
axisLine: function () {
...

this.group.add(new graphic.Line(graphic.subPixelOptimizeLine({
// Id for animation
anid: 'line',

shape: {
x1: pt1[0],
y1: pt1[1],
x2: pt2[0],
y2: pt2[1]
},
style: lineStyle,
strokeContainThreshold: opt.strokeContainThreshold || 5,
silent: true,
z2: 1
})));

var arrows = axisModel.get('axisLine.symbol');
var arrowSize = axisModel.get('axisLine.symbolSize');

var arrowOffset = axisModel.get('axisLine.symbolOffset') || 0;
if (typeof arrowOffset === 'number') {
arrowOffset = [arrowOffset, arrowOffset];
}

if (arrows != null) {
...
each([{
rotate: opt.rotation + Math.PI / 2,
offset: arrowOffset[0],
r: 0
}, {
rotate: opt.rotation - Math.PI / 2,
offset: arrowOffset[1],
r: Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0])
+ (pt1[1] - pt2[1]) * (pt1[1] - pt2[1]))
}], function (point, index) {
// 创建arrow symbole
if (arrows[index] !== 'none' && arrows[index] != null) {
var symbol = createSymbol(
arrows[index],
-symbolWidth / 2,
-symbolHeight / 2,
symbolWidth,
symbolHeight,
lineStyle.stroke,
true
);

// Calculate arrow position with offset
var r = point.r + point.offset;
var pos = [
pt1[0] + r * Math.cos(opt.rotation),
pt1[1] - r * Math.sin(opt.rotation)
];

symbol.attr({
rotation: point.rotate,
position: pos,
silent: true,
z2: 11
});
this.group.add(symbol);
}
}, this);
}
},

axisTickLabel: function () {
var axisModel = this.axisModel;
var opt = this.opt;
// 通过graphic.Line以及graphic.Text分别对tick及label进行渲染
var tickEls = buildAxisTick(this, axisModel, opt);
var labelEls = buildAxisLabel(this, axisModel, opt);

fixMinMaxLabelShow(axisModel, labelEls, tickEls);
},

axisName: function () {
...
var textEl = new graphic.Text({
// Id for animation
anid: 'name',

__fullText: name,
__truncatedText: truncatedText,

position: pos,
rotation: labelLayout.rotation,
silent: isSilent(axisModel),
z2: 1,
tooltip: (tooltipOpt && tooltipOpt.show)
? extend({
content: name,
formatter: function () {
return name;
},
formatterParams: formatterParams
}, tooltipOpt)
: null
});

graphic.setTextStyle(textEl.style, textStyleModel, {
text: truncatedText,
textFont: textFont,
textFill: textStyleModel.getTextColor()
|| axisModel.get('axisLine.lineStyle.color'),
textAlign: labelLayout.textAlign,
textVerticalAlign: labelLayout.textVerticalAlign
});
...

// FIXME
this._dumbGroup.add(textEl);
textEl.updateTransform();

this.group.add(textEl);

textEl.decomposeTransform();
}

};

AxisModelCreator

AxisModelCreator为生成AxisModel的方法,其在AxisModel的基础上扩展了getCategories、getOrdinalMeta、mergeDefaultAndTheme等方法,重写了defaultOption属性,并通过:

1
2
3
4
ComponentModel.registerSubTypeDefaulter(
axisName + 'Axis',
zrUtil.curry(axisTypeDefaulter, axisName)
);

来注册相应的Axis子类,如xAxis、yAxis、radiusAxis以及angleAxis等。

直角坐标轴 Axis

Axis为直角坐标系Grid中的坐标轴,包括xAxis(x轴)以及yAxis(y轴)。

AxisModel

AxisModel通过extend方法扩展自Component Model,重写了init、mergeOption、,并使用AxisModelCreator分别创建额xAxisModel以及yAxisModel,

CartesianAxisView

CartesianAxisView通过extend方法扩展自AxisView,重写了render方法,定义了splitLine以及splitArea方法,并扩展了xAxis以及yAxis View。主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
var CartesianAxisView = AxisView.extend({
type: 'cartesianAxis',
axisPointerClass: 'CartesianAxisPointer',
render: function (axisModel, ecModel, api, payload) {
...
// 结合AxisBuilder
var axisBuilder = new AxisBuilder(axisModel, layout);
zrUtil.each(axisBuilderAttrs, axisBuilder.add, axisBuilder);
...
CartesianAxisView.superCall(this, 'render', axisModel, ecModel, api, payload);
},
...
_splitLine: function (axisModel, gridModel) {
...
// Simple optimization
// Batching the lines if color are the same
for (var i = 0; i < ticksCoords.length; i++) {
...
this._axisGroup.add(new graphic.Line(graphic.subPixelOptimizeLine({
anid: tickValue != null ? 'line_' + ticksCoords[i].tickValue : null,
shape: {
x1: p1[0],
y1: p1[1],
x2: p2[0],
y2: p2[1]
},
style: zrUtil.defaults({
stroke: lineColors[colorIndex]
}, lineStyle),
silent: true
})));
}
},

_splitArea: function (axisModel, gridModel) {
...
for (var i = 1; i < ticksCoords.length; i++) {
...
this._axisGroup.add(new graphic.Rect({
anid: tickValue != null ? 'area_' + tickValue : null,
shape: {
x: x,
y: y,
width: width,
height: height
},
style: zrUtil.defaults({
fill: areaColors[colorIndex]
}, areaStyle),
silent: true
}));

colorIndex = (colorIndex + 1) % areaColorsLen;
}

this._splitAreaColors = newSplitAreaColors;
}
});
CartesianAxisView.extend({
type: 'xAxis'
});
CartesianAxisView.extend({
type: 'yAxis'
});

极坐标轴 AngleAxis&RadiusAxis

每一个极坐标都拥有一个角度轴AngleAxis和一个半径轴RadiusAxis。

公共文件

AxisModel

AxisModel通过extend方法扩展自Component Model,定义了getCoordSysModel方法,并根据不同option创建了angle和radius Model,主要代码如下:

1
2
axisModelCreator('angle', PolarAxisModel, getAxisType, polarAxisDefaultExtendedOption.angle);
axisModelCreator('radius', PolarAxisModel, getAxisType, polarAxisDefaultExtendedOption.radius);

角度轴 AngleAxis

AngleAxis为极坐标Polar中的角度轴,

AngleAxisView

AngleAxisView通过extend方法扩展自AxisView,重写了render方法,并定义了axisLine、axisTick、axisLabel、splitLine以及splitArea等方法,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
render: function (angleAxisModel, ecModel) {
...
var angleAxis = angleAxisModel.axis;
var polar = angleAxis.polar;
var radiusExtent = polar.getRadiusAxis().getExtent();
var ticksAngles = angleAxis.getTicksCoords();
var labels = zrUtil.map(angleAxis.getViewLabels(), function (labelItem) {
var labelItem = zrUtil.clone(labelItem);
labelItem.coord = angleAxis.dataToCoord(labelItem.tickValue);
return labelItem;
});
...
},

_axisLine: function (angleAxisModel, polar, ticksAngles, radiusExtent) {
var lineStyleModel = angleAxisModel.getModel('axisLine.lineStyle');

var circle = new graphic.Circle({
shape: {
cx: polar.cx,
cy: polar.cy,
r: radiusExtent[getRadiusIdx(polar)]
},
style: lineStyleModel.getLineStyle(),
z2: 1,
silent: true
});
circle.style.fill = null;

this.group.add(circle);
},
_axisTick: function (angleAxisModel, polar, ticksAngles, radiusExtent) {
var tickModel = angleAxisModel.getModel('axisTick');

var tickLen = (tickModel.get('inside') ? -1 : 1) * tickModel.get('length');
var radius = radiusExtent[getRadiusIdx(polar)];

var lines = zrUtil.map(ticksAngles, function (tickAngleItem) {
return new graphic.Line({
shape: getAxisLineShape(polar, [radius, radius + tickLen], tickAngleItem.coord)
});
});
this.group.add(graphic.mergePath(
lines, {
style: zrUtil.defaults(
tickModel.getModel('lineStyle').getLineStyle(),
{
stroke: angleAxisModel.get('axisLine.lineStyle.color')
}
)
}
));
},
_axisLabel: function (angleAxisModel, polar, ticksAngles, radiusExtent, labels) {
...
// Use length of ticksAngles because it may remove the last tick to avoid overlapping
zrUtil.each(labels, function (labelItem, idx) {
...
var textEl = new graphic.Text({silent: true});
this.group.add(textEl);
graphic.setTextStyle(textEl.style, labelModel, {
x: p[0],
y: p[1],
textFill: labelModel.getTextColor() || angleAxisModel.get('axisLine.lineStyle.color'),
text: labelItem.formattedLabel,
textAlign: labelTextAlign,
textVerticalAlign: labelTextVerticalAlign
});
}, this);
},
_splitLine: function (angleAxisModel, polar, ticksAngles, radiusExtent) {
...
for (var i = 0; i < ticksAngles.length; i++) {
var colorIndex = (lineCount++) % lineColors.length;
splitLines[colorIndex] = splitLines[colorIndex] || [];
splitLines[colorIndex].push(new graphic.Line({
shape: getAxisLineShape(polar, radiusExtent, ticksAngles[i].coord)
}));
}

// Simple optimization
// Batching the lines if color are the same
for (var i = 0; i < splitLines.length; i++) {
this.group.add(graphic.mergePath(splitLines[i], {
style: zrUtil.defaults({
stroke: lineColors[i % lineColors.length]
}, lineStyleModel.getLineStyle()),
silent: true,
z: angleAxisModel.get('z')
}));
}
},
_splitArea: function (angleAxisModel, polar, ticksAngles, radiusExtent) {
...
for (var i = 1; i < ticksAngles.length; i++) {
var colorIndex = (lineCount++) % areaColors.length;
splitAreas[colorIndex] = splitAreas[colorIndex] || [];
splitAreas[colorIndex].push(new graphic.Sector({
shape: {
cx: polar.cx,
cy: polar.cy,
r0: r0,
r: r1,
startAngle: prevAngle,
endAngle: -ticksAngles[i].coord * RADIAN,
clockwise: clockwise
},
silent: true
}));
prevAngle = -ticksAngles[i].coord * RADIAN;
}

// Simple optimization
// Batching the lines if color are the same
for (var i = 0; i < splitAreas.length; i++) {
this.group.add(graphic.mergePath(splitAreas[i], {
style: zrUtil.defaults({
fill: areaColors[i % areaColors.length]
}, areaStyleModel.getAreaStyle()),
silent: true
}));
}
}

半径轴 RadiusAxis

RadiusAxis为极坐标Polar中的半径轴。

RadiusAxisView

RadiusAxisView通过extend方法扩展自AxisView,重写了render方法,并定义了splitLine、splitArea方法,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
_splitLine: function (radiusAxisModel, polar, axisAngle, radiusExtent, ticksCoords) {
...
for (var i = 0; i < ticksCoords.length; i++) {
var colorIndex = (lineCount++) % lineColors.length;
splitLines[colorIndex] = splitLines[colorIndex] || [];
splitLines[colorIndex].push(new graphic.Circle({
shape: {
cx: polar.cx,
cy: polar.cy,
r: ticksCoords[i].coord
},
silent: true
}));
}

// Simple optimization
// Batching the lines if color are the same
for (var i = 0; i < splitLines.length; i++) {
this.group.add(graphic.mergePath(splitLines[i], {
style: zrUtil.defaults({
stroke: lineColors[i % lineColors.length],
fill: null
}, lineStyleModel.getLineStyle()),
silent: true
}));
}
},

_splitArea: function (radiusAxisModel, polar, axisAngle, radiusExtent, ticksCoords) {
...
for (var i = 1; i < ticksCoords.length; i++) {
var colorIndex = (lineCount++) % areaColors.length;
splitAreas[colorIndex] = splitAreas[colorIndex] || [];
splitAreas[colorIndex].push(new graphic.Sector({
shape: {
cx: polar.cx,
cy: polar.cy,
r0: prevRadius,
r: ticksCoords[i].coord,
startAngle: 0,
endAngle: Math.PI * 2
},
silent: true
}));
prevRadius = ticksCoords[i].coord;
}

// Simple optimization
// Batching the lines if color are the same
for (var i = 0; i < splitAreas.length; i++) {
this.group.add(graphic.mergePath(splitAreas[i], {
style: zrUtil.defaults({
fill: areaColors[i % areaColors.length]
}, areaStyleModel.getAreaStyle()),
silent: true
}));
}
}

雷达坐标轴

RadarView

RadarView通过extendComponentView方法扩展自Component View,重写了render方法,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
render: function (radarModel, ecModel, api) {
var group = this.group;
group.removeAll();

this._buildAxes(radarModel);
this._buildSplitLineAndArea(radarModel);
},

_buildAxes: function (radarModel) {
var radar = radarModel.coordinateSystem;
var indicatorAxes = radar.getIndicatorAxes();
var axisBuilders = zrUtil.map(indicatorAxes, function (indicatorAxis) {
var axisBuilder = new AxisBuilder(indicatorAxis.model, {
position: [radar.cx, radar.cy],
rotation: indicatorAxis.angle,
labelDirection: -1,
tickDirection: -1,
nameDirection: 1
});
return axisBuilder;
});

zrUtil.each(axisBuilders, function (axisBuilder) {
zrUtil.each(axisBuilderAttrs, axisBuilder.add, axisBuilder);
this.group.add(axisBuilder.getGroup());
}, this);
},

_buildSplitLineAndArea: function (radarModel) {
...
if (shape === 'circle') {
var ticksRadius = indicatorAxes[0].getTicksCoords();
var cx = radar.cx;
var cy = radar.cy;
for (var i = 0; i < ticksRadius.length; i++) {
if (showSplitLine) {
var colorIndex = getColorIndex(splitLines, splitLineColors, i);
splitLines[colorIndex].push(new graphic.Circle({
shape: {
cx: cx,
cy: cy,
r: ticksRadius[i].coord
}
}));
}
if (showSplitArea && i < ticksRadius.length - 1) {
var colorIndex = getColorIndex(splitAreas, splitAreaColors, i);
splitAreas[colorIndex].push(new graphic.Ring({
shape: {
cx: cx,
cy: cy,
r0: ticksRadius[i].coord,
r: ticksRadius[i + 1].coord
}
}));
}
}
}
// Polygon
else {
...
for (var i = 0; i <= realSplitNumber; i++) {
...
if (showSplitLine) {
var colorIndex = getColorIndex(splitLines, splitLineColors, i);
splitLines[colorIndex].push(new graphic.Polyline({
shape: {
points: points
}
}));
}
if (showSplitArea && prevPoints) {
var colorIndex = getColorIndex(splitAreas, splitAreaColors, i - 1);
splitAreas[colorIndex].push(new graphic.Polygon({
shape: {
points: points.concat(prevPoints)
}
}));
}
prevPoints = points.slice().reverse();
}
}

var lineStyle = lineStyleModel.getLineStyle();
var areaStyle = areaStyleModel.getAreaStyle();
// Add splitArea before splitLine
zrUtil.each(splitAreas, function (splitAreas, idx) {
this.group.add(graphic.mergePath(
splitAreas, {
style: zrUtil.defaults({
stroke: 'none',
fill: splitAreaColors[idx % splitAreaColors.length]
}, areaStyle),
silent: true
}
));
}, this);

zrUtil.each(splitLines, function (splitLines, idx) {
this.group.add(graphic.mergePath(
splitLines, {
style: zrUtil.defaults({
fill: 'none',
stroke: splitLineColors[idx % splitLineColors.length]
}, lineStyle),
silent: true
}
));
}, this);
}

RadarModel

RadarModel通过extendComponentModel扩展自Component Model,重写了defaultOption属性以及optionUpdated方法。

平行坐标轴 ParallelAxis

ParallelModel

ParallelModel使用extend方法扩展自Component Model,重写了defaultOption属性,并重写了init、mergeOption等方法。

ParallelAxisAction

parallelAxisAction中注册了axisAreaSelected以及parallelAxisExpand API,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var actionInfo = {
type: 'axisAreaSelect',
event: 'axisAreaSelected'
};

echarts.registerAction(actionInfo, function (payload, ecModel) {
ecModel.eachComponent(
{mainType: 'parallelAxis', query: payload},
function (parallelAxisModel) {
parallelAxisModel.axis.model.setActiveIntervals(payload.intervals);
}
);
});

echarts.registerAction('parallelAxisExpand', function (payload, ecModel) {
ecModel.eachComponent(
{mainType: 'parallel', query: payload},
function (parallelModel) {
parallelModel.setAxisExpand(payload);
}
);
});

ParallelAxisView

ParallelAxisView使用extendComponentView方法扩展自Component View,重写了render等方法。渲染代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
render: function (axisModel, ecModel, api, payload) {
...
var axisBuilder = new AxisBuilder(axisModel, builderOpt);

zrUtil.each(elementList, axisBuilder.add, axisBuilder);

this._axisGroup.add(axisBuilder.getGroup());

this._refreshBrushController(
builderOpt, areaSelectStyle, axisModel, coordSysModel, areaWidth, api
);
...
},
_refreshBrushController: function (
builderOpt, areaSelectStyle, axisModel, coordSysModel, areaWidth, api
) {
...
var rect = graphic.BoundingRect.create({
x: extent[0],
y: -areaWidth / 2,
width: extentLen,
height: areaWidth
});
rect.x -= extra;
rect.width += 2 * extra;

// _brushController为平行坐标轴上的控制器
this._brushController
.mount({
enableGlobalPan: true,
rotation: builderOpt.rotation,
position: builderOpt.position
})
.setPanels([{
panelId: 'pl',
clipPath: brushHelper.makeRectPanelClipPath(rect),
isTargetByCursor: brushHelper.makeRectIsTargetByCursor(rect, api, coordSysModel),
getLinearBrushOtherExtent: brushHelper.makeLinearBrushOtherExtent(rect, 0)
}])
.enableBrush({
brushType: 'lineX',
brushStyle: areaSelectStyle,
removeOnClick: true
})
.updateCovers(getCoverInfoList(axisModel));
},

单轴 SingleAxis

SingleCreator

SingleCreator文件主要为创建单轴的逻辑代码,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function create(ecModel, api) {
var singles = [];

ecModel.eachComponent('singleAxis', function (axisModel, idx) {

var single = new Single(axisModel, ecModel, api);
single.name = 'single_' + idx;
single.resize(axisModel, api);
axisModel.coordinateSystem = single;
singles.push(single);

});

ecModel.eachSeries(function (seriesModel) {
if (seriesModel.get('coordinateSystem') === 'singleAxis') {
var singleAxisModel = ecModel.queryComponents({
mainType: 'singleAxis',
index: seriesModel.get('singleAxisIndex'),
id: seriesModel.get('singleAxisId')
})[0];
seriesModel.coordinateSystem = singleAxisModel && singleAxisModel.coordinateSystem;
}
});

return singles;
}

AxisModel

axisModel扩展自Component Model,重写了defaultOption属性

SingleAxisView

SingleAxisView使用extend方法扩展自AxisView,重写了render方法,并定义了splitLine方法,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
render: function (axisModel, ecModel, api, payload) {
...
var layout = singleAxisHelper.layout(axisModel);
var axisBuilder = new AxisBuilder(axisModel, layout);
zrUtil.each(axisBuilderAttrs, axisBuilder.add, axisBuilder);
...
SingleAxisView.superCall(this, 'render', axisModel, ecModel, api, payload);
},

_splitLine: function (axisModel) {
var axis = axisModel.axis;
...
var ticksCoords = axis.getTicksCoords({
tickModel: splitLineModel
});

var p1 = [];
var p2 = [];

for (var i = 0; i < ticksCoords.length; ++i) {
var tickCoord = axis.toGlobalCoord(ticksCoords[i].coord);
if (isHorizontal) {
p1[0] = tickCoord;
p1[1] = gridRect.y;
p2[0] = tickCoord;
p2[1] = gridRect.y + gridRect.height;
}
else {
p1[0] = gridRect.x;
p1[1] = tickCoord;
p2[0] = gridRect.x + gridRect.width;
p2[1] = tickCoord;
}
var colorIndex = (lineCount++) % lineColors.length;
splitLines[colorIndex] = splitLines[colorIndex] || [];
splitLines[colorIndex].push(new graphic.Line(
graphic.subPixelOptimizeLine({
shape: {
x1: p1[0],
y1: p1[1],
x2: p2[0],
y2: p2[1]
},
style: {
lineWidth: lineWidth
},
silent: true
})));
}
...
}

坐标系

在上一节中,我们介绍了直角坐标轴、极坐标轴以及雷达坐标轴,我们将在这节解读坐标轴是如何在坐标系中展示的。

公共文件

Axis

Axis对象中定义了scale、dim等属性以及contain、containData、getExtent、getTicksCoords、getViewLabels等方法。

Grid 直角坐标系

Grid为echarts中的直角坐标系组件。

gridSimple

gridSimple通过extendComponentView扩展自Component View,重写了render方法,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
render: function (gridModel, ecModel) {
this.group.removeAll();
if (gridModel.get('show')) {
this.group.add(new graphic.Rect({
shape: gridModel.coordinateSystem.getRect(),
style: zrUtil.defaults({
fill: gridModel.get('backgroundColor')
}, gridModel.getItemStyle()),
silent: true,
z2: -1
}));
}
}

Grid

coord/cartesian/Grid.js,实现直角坐标系的渲染,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function Grid(gridModel, ecModel, api) {
...
this._initCartesian(gridModel, ecModel, api);
...
}
gridProto._initCartesian = function (gridModel, ecModel, api) {
var axisPositionUsed = {
left: false,
right: false,
top: false,
bottom: false
};
var axesMap = {
x: {},
y: {}
};
var axesCount = {
x: 0,
y: 0
};

/// Create axis
// 创建x及y坐标轴
ecModel.eachComponent('xAxis', createAxisCreator('x'), this);
ecModel.eachComponent('yAxis', createAxisCreator('y'), this);
...
/// Create cartesian2d
// 创建
each(axesMap.x, function (xAxis, xAxisIndex) {
each(axesMap.y, function (yAxis, yAxisIndex) {
var key = 'x' + xAxisIndex + 'y' + yAxisIndex;
var cartesian = new Cartesian2D(key);

cartesian.grid = this;
cartesian.model = gridModel;

this._coordsMap[key] = cartesian;
this._coordsList.push(cartesian);

cartesian.addAxis(xAxis);
cartesian.addAxis(yAxis);
}, this);
}, this);

function createAxisCreator(axisType) {
return function (axisModel, idx) {
...
// 创建坐标轴
var axis = new Axis2D(
axisType, createScaleByModel(axisModel),
[0, 0],
axisModel.get('type'),
axisPosition
);
...
};
}
};

Polar 极坐标系

Polar为echarts中的极坐标系组件,每一个极坐标都拥有一个角度轴和一个半径轴。

Polar

Polar定义了angleAxis(new RadiusAxis())、radiusAxis(new AngleAxis())等属性以及containPoint、containData、getAxis、getAngleAxis、getRadiusAxis等方法。

PolarCreator

PolarCreator用于创建极坐标系,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
create: function (ecModel, api) {
var polarList = [];
ecModel.eachComponent('polar', function (polarModel, idx) {
var polar = new Polar(idx);
// Inject resize and update method
polar.update = updatePolarScale;

var radiusAxis = polar.getRadiusAxis();
var angleAxis = polar.getAngleAxis();

var radiusAxisModel = polarModel.findAxisModel('radiusAxis');
var angleAxisModel = polarModel.findAxisModel('angleAxis');

// 设置角度轴和半径轴
setAxis(radiusAxis, radiusAxisModel);
setAxis(angleAxis, angleAxisModel);

resizePolar(polar, polarModel, api);

polarList.push(polar);

polarModel.coordinateSystem = polar;
polar.model = polarModel;
});
...

return polarList;
}

Radar 雷达坐标系

Radar为echarts中的雷达坐标系组件。

###Radar

Radar文件位于coord/radar目录下,定义了getIndicatorAxes、coordToPoint、dataToPoint等方法,并注册了雷达坐标信息,主要代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function Radar(radarModel, ecModel, api) {
...
this._indicatorAxes = zrUtil.map(radarModel.getIndicatorModels(), function (indicatorModel, idx) {
var dim = 'indicator_' + idx;
// 创建雷达坐标轴
var indicatorAxis = new IndicatorAxis(dim, new IntervalScale());
indicatorAxis.name = indicatorModel.get('name');
// Inject model and axis
indicatorAxis.model = indicatorModel;
indicatorModel.axis = indicatorAxis;
this.dimensions.push(dim);
return indicatorAxis;
}, this);
...
}
Radar.create = function (ecModel, api) {
var radarList = [];
ecModel.eachComponent('radar', function (radarModel) {
var radar = new Radar(radarModel, ecModel, api);
radarList.push(radar);
radarModel.coordinateSystem = radar;
});
ecModel.eachSeriesByType('radar', function (radarSeries) {
if (radarSeries.get('coordinateSystem') === 'radar') {
// Inject coordinate system
radarSeries.coordinateSystem = radarList[radarSeries.get('radarIndex') || 0];
}
});
return radarList;
};
CoordinateSystem.register('radar', Radar);

Parallel 平行坐标系

Parallel为echarts中的平行坐标系组件。

Parallel

Parallel定义了rect、dimensions等属性以及init、containPoint、getRect、getAxis、axisCoordToPoint等方法。

ParallelCreator

ParallelCreator用于创建平行坐标系,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function create(ecModel, api) {
var coordSysList = [];

ecModel.eachComponent('parallel', function (parallelModel, idx) {
var coordSys = new Parallel(parallelModel, ecModel, api);

coordSys.name = 'parallel_' + idx;
coordSys.resize(parallelModel, api);

parallelModel.coordinateSystem = coordSys;
coordSys.model = parallelModel;

coordSysList.push(coordSys);
});

// Inject the coordinateSystems into seriesModel
ecModel.eachSeries(function (seriesModel) {
if (seriesModel.get('coordinateSystem') === 'parallel') {
var parallelModel = ecModel.queryComponents({
mainType: 'parallel',
index: seriesModel.get('parallelIndex'),
id: seriesModel.get('parallelId')
})[0];
seriesModel.coordinateSystem = parallelModel.coordinateSystem;
}
});

return coordSysList;
}

CoordinateSystem.register('parallel', {create: create});

geo 地理坐标系

geo.js

geo.js中注册了geoToggleSelect、geoSelect、geoUnSelect等action

GeoMode

GeoModel通过extend方法扩展自Component Model,重写了defaultOption属性以及init、optionUpdated方法,并定义了getRegionModel、getFormattedLabel、setZoom以及setCenter方法。

geoCreator

geoCreator用于创建Geo 地理坐标系

GeoView

GeoView通过extendComponentView扩展自Compnent View,重写了init、render以及dispose方法,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
init: function (ecModel, api) {
var mapDraw = new MapDraw(api, true);
this._mapDraw = mapDraw;

this.group.add(mapDraw.group);
},

render: function (geoModel, ecModel, api, payload) {
...
var mapDraw = this._mapDraw;
if (geoModel.get('show')) {
mapDraw.draw(geoModel, ecModel, api, this, payload);
}
...
},

MapDraw

MapDraw, 主要绘制代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
draw: function (mapOrGeoModel, ecModel, api, fromView, payload) {
...
// 渲染背景
this._updateBackground(geo);
...
zrUtil.each(geo.regions, function (region) {
var regionGroup = nameMap.get(region.name)
|| nameMap.set(region.name, new graphic.Group());

var compoundPath = new graphic.CompoundPath({
shape: {
paths: []
}
});
regionGroup.add(compoundPath);

...

zrUtil.each(region.geometries, function (geometry) {
// 仅支持polygon
if (geometry.type !== 'polygon') {
return;
}
compoundPath.shape.paths.push(new graphic.Polygon({
shape: {
points: geometry.exterior
}
}));

for (var i = 0; i < (geometry.interiors ? geometry.interiors.length : 0); i++) {
compoundPath.shape.paths.push(new graphic.Polygon({
shape: {
points: geometry.interiors[i]
}
}));
}
});

compoundPath.setStyle(itemStyle);
compoundPath.style.strokeNoScale = true;
compoundPath.culling = true;
// Label
...
if (
(isGeo || isDataNaN && (showLabel || hoverShowLabel))
|| (itemLayout && itemLayout.showLabel)
) {
...
var textEl = new graphic.Text({
position: region.center.slice(),
scale: [1 / scale[0], 1 / scale[1]],
z2: 10,
silent: true
});
graphic.setLabelStyle(
textEl.style, textEl.hoverStyle = {}, labelModel, hoverLabelModel,
{
labelFetcher: labelFetcher,
labelDataIndex: query,
defaultText: region.name,
useInsideStyle: false
},
{
textAlign: 'center',
textVerticalAlign: 'middle'
}
);

regionGroup.add(textEl);
}
...
var groupRegions = regionGroup.__regions || (regionGroup.__regions = []);
groupRegions.push(region);
...
regionsGroup.add(regionGroup);
});
// 绑定pan、zoom等事件处理器
this._updateController(mapOrGeoModel, ecModel, api);

updateMapSelectHandler(this, mapOrGeoModel, regionsGroup, api, fromView);

updateMapSelected(mapOrGeoModel, regionsGroup);
}

geoRoam

geoRoam中注册了geoRoam api,trigger中调用了geoModel的setCenter及setZoom方法来更新视图。

Calendar 日历坐标系

calendar.js

calendar.js创建日历坐标系

CalendarModel

CalendarModel通过extend方法扩展自Component Model,重写了defaultOption属性以及init、mergeOption方法。

CalendarView

CalendarView通过extendComponentView扩展自Component View,重写了render方法,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
render: function (calendarModel, ecModel, api) {
var group = this.group;
group.removeAll();
var coordSys = calendarModel.coordinateSystem;
var rangeData = coordSys.getRangeInfo();
var orient = coordSys.getOrient();
// 通过graphic.Rect来绘制day rect
this._renderDayRect(calendarModel, rangeData, group);
// 通过graphic.Polyline绘制分割线
this._renderLines(calendarModel, rangeData, orient, group);
// 通过graphic.Text绘制year label
this._renderYearText(calendarModel, rangeData, orient, group);
// 通过graphic.Text绘制month label
this._renderMonthText(calendarModel, orient, group);
// 通过graphic.Text绘制week label
this._renderWeekText(calendarModel, rangeData, orient, group);
}

问题

在解读的过程中,我们需要带着问题去思考源码的实现,在这里我们来解决echarts是如何渲染展示坐标轴上的label数据这个问题。

存储

首先考虑label data的存储过程,以直角坐标系为🌰,data存储的调用链如下所示:

  • echarts.setOption()
  • Global.setOption()
  • Global.mergeOption()
  • Component.optionUpdated() (Model)
  • AxisModelCreator.optionUpdated(),将OrdinalMeta.createByAxisModel()返回的数据保存在this.__ordinalMeta中
  • OrdinalMeta.createByAxisModel(),主要代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
OrdinalMeta.createByAxisModel = function (axisModel) {
var option = axisModel.option;
var data = option.data;
var categories = data && map(data, getName);

return new OrdinalMeta({
categories: categories,
needCollect: !categories,
// deduplication is default in axis.
deduplication: option.dedplication !== false
});
};
  • Grid.js中通过:
1
2
3
4
5
6
var axis = new Axis2D(
axisType, createScaleByModel(axisModel),
[0, 0],
axisModel.get('type'),
axisPosition
);

创建坐标轴,createScaleByModel方法获取保存在this.__ordinalMeta属性中的数据,并返回OrdinalScale对象,createScaleByModel方法主要代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export function createScaleByModel(model, axisType) {
axisType = axisType || model.get('type');
if (axisType) {
switch (axisType) {
// Buildin scale
case 'category':
return new OrdinalScale(
model.getOrdinalMeta
? model.getOrdinalMeta()
: model.getCategories(),
[Infinity, -Infinity]
);
case 'value':
return new IntervalScale();
// Extended scale, like time and log
default:
return (Scale.getClass(axisType) || IntervalScale).create(model);
}
}
}

Grid创建坐标轴时将返回的OrdinalScale对象数据作为scale属性保存在axis中,主要代码如下:

1
2
3
4
5
6
7
8
var Axis2D = function (dim, scale, coordExtent, axisType, position) {
Axis.call(this, dim, scale, coordExtent);
}
var Axis = function (dim, scale, extent) {
...
this.scale = scale;
...
};

获取

获取

  • AxisBuilder axisTickLabel()
  • AxisBuilder buildAxisLabel()
  • labels = axis.getViewLabels() => coord/Axis.js getViewLabels()
  • 调用createAxisLabels(),返回result.labels
  • axisTickLabelBuilder createAxisLabels()
1
2
3
4
5
6
export function createAxisLabels(axis) {
// Only ordinal scale support tick interval
return axis.type === 'category'
? makeCategoryLabels(axis)
: makeRealNumberLabels(axis);
}
  • axisTickLabelBuilder makeCategoryLabels()
1
2
3
4
5
6
7
8
function makeCategoryLabels(axis) {
var labelModel = axis.getLabelModel();
var result = makeCategoryLabelsActually(axis, labelModel);

return (!labelModel.get('show') || axis.scale.isBlank())
? {labels: [], labelCategoryInterval: result.labelCategoryInterval}
: result;
}
  • axisTickLabelBuilder makeCategoryLabelsActually()
  • axisTickLabelBuilder makeLabelsByCustomizedCategoryInterval()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function makeLabelsByCustomizedCategoryInterval(axis, categoryInterval, onlyTick) {
// axis.scale是我们保存的OrdinalScale对象数据
var ordinalScale = axis.scale;
var labelFormatter = makeLabelFormatter(axis);
var result = [];

zrUtil.each(ordinalScale.getTicks(), function (tickValue) {
var rawLabel = ordinalScale.getLabel(tickValue);
if (categoryInterval(tickValue, rawLabel)) {
result.push(onlyTick
? tickValue
: {
formattedLabel: labelFormatter(tickValue),
rawLabel: rawLabel,
tickValue: tickValue
}
);
}
});

return result;
}

渲染

AxisBuilder通过axis.getViewLabels()获取labels数据后通过graphic.Text进行渲染,通过graphic.setStyle设置文本样式,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
each(labels, function (labelItem, index) {
...
var textEl = new graphic.Text({
// Id for animation
anid: 'label_' + tickValue,
position: pos,
rotation: labelLayout.rotation,
silent: silent,
z2: 10
});

graphic.setTextStyle(textEl.style, itemLabelModel, {
text: formattedLabel,
textAlign: itemLabelModel.getShallow('align', true)
|| labelLayout.textAlign,
textVerticalAlign: itemLabelModel.getShallow('verticalAlign', true)
|| itemLabelModel.getShallow('baseline', true)
|| labelLayout.textVerticalAlign,
textFill: typeof textColor === 'function'
? textColor(
axis.type === 'category'
? rawLabel
: axis.type === 'value'
? tickValue + ''
: tickValue,
index
)
: textColor
});

...
labelEls.push(textEl);
axisBuilder.group.add(textEl);

textEl.decomposeTransform();

});

总结

这篇博文很大,简单介绍了title、legend、axisPointer、坐标系以及坐标轴等组件的渲染细节,也探讨了坐标轴上label 数据的存储、获取以及渲染的过程。

希望能够跟小伙伴们一起进步呀!继续加油!!

本文标题:echarts源码解读《三》:echarts源码之Component分析

文章作者:萌萌哒的邱邱邱邱

发布时间:2019年05月22日 - 16:05

最后更新:2019年06月06日 - 14:06

原始链接:https://qiuruolin.github.io/2019/05/22/echarts-3/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------本文结束感谢您的阅读-------------
感谢您的支持