D3Charts-日历图的实现原理

如果我来做,我会怎么实现?

页面展示的内容

首先对日历的展示内容做一个拆解,大致包括:

  • 主标题(比如年份)
  • 子标题(比如月份)
  • 日期的整体容器格子
  • 单个日期的方格子
  • 单个日期的具体显示内容(设计时考虑到高扩展性)

数据结构

日历数据

1
2
3
4
{
"date": "20200527",
"value": 30
}

配置项

  • 语言(中英文)
  • 布局(横向、纵向)
  • 显示内容的自定义函数(formatter)
  • 页面上每个元素的样式配置信息、位置配置信息

事件

  • 点击
  • hover

具体实现的解析

程序如何识别当前的配置是个日历图?

如何自定义每个表格的内容?

可以参考我们新扩展的自定义类型来实现。

散点图是怎么画上去的?

component下的calendar的代码,并没有实现散点图的功能。

这个是配置在series下面的,应该是每个组件都是和series是合作作图的,组件会将既定的位置用series设置的图形进行绘制。

那么这个结合具体是怎么实现的呢?

在CalendarModel.js中,有一个calcPoint方法,用于计算每个日期的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
calcPoint(date) {
let { days } = this;
let cellSize = this.get('cellSize');
let left = this.get('left');
let top = this.get('top');
let finded;

date = new Date(date);
finded = find(days, function(day) {
let d = day.d;
return d.getFullYear() === date.getFullYear() && d.getMonth() === date.getMonth() && d.getDate() == date.getDate();
});

if (finded) {
finded.x = left + cellSize * (finded.xi + 0.5);
finded.y = top + cellSize * (finded.yi + 0.5);

return finded;
}
}

该方法在src/chart/scatter/ScatterModel.js中的updateByCalendar()方法中会被调用(EffectScatterModel继承自ScatterModel,所以这个方法也会被EffectScatterModel调用),计算出每个元素的位置后,挂载到model.points属性上:

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
updateByCalendar() {
let { globalModel } = this;
let data = this.getData();
let calendarModel = globalModel.getComponentByIndex('calendar', this.get('$calendarIndex'));
let points = [];

each(data, (d, i) => {
if (d) {
let date = d[0];
let point = [];
let _point = calendarModel.calcPoint(date);

if (_point) {
point = [_point.x, _point.y, d, i];
} else {
point = null;
}

points.push(point);
}
});

this.data = data;
this.startIndex = 0;
this.lastPoints = this.points;
this.points = points;
}

然后在EffectScatterView中,就会用到这个定位信息,去画图了:

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
renderAllSymbol(model, globalModel, global, showPoints) {
let group = this.group;
let symbolOption = model.get('symbol').normal;
let { isScatter } = model;
let cursor = model.get('cursor');
let attrs = [];

each(showPoints, function (point, index) {
if (point) {
let itemData = point[2];

let attr = {
shape: getScatterShape(model, itemData, false),
// 这里用到的就是前面计算好的位置信息
position: [point[0], point[1]],
style: getScatterStyle(model, itemData, false, point[3]),
key: point[3],
seriesIndex: model.index,
dataIndex: point[3],
rectHover: true,
cursor,
model
};

attrs.push(attr);
}
});

this.setShapeGroup('symbolGroup', EffectSymbol, attrs);
}

如何做事件监听?

这和之前的事件监听是一样的,绑定到series即可:

1
2
3
4
let seriesModel = chart.getModel('series', 0)
chart.registerAction(seriesModel, "ITEM_CLICK", function (e) {
console.log(e)
})

一些技巧

先确定布局和数据结构,并制定见文知意的英文名

这样你对于该怎么设计代码中的数据结构、配置项,就会很明确了。后续和别人交流起来也因为定义好了名字,更容易沟通。

给你拆分好的每一类页面元素,都设置对应的样式配置项

这样可以保证你不会遗漏,也会最大化页面表现力的自由度。

将页面上每个元素的渲染独立开

比如类似这样:

1
2
3
4
5
6
7
render(model, globalModel) {
this.renderDays(...arguments);
this.renderSplitLine(...arguments);
this.renderDayLabel(...arguments);
this.renderMonthLabel(...arguments);
this.renderYearLabel(...arguments);
}

将所有绘图操作交给绘图层ZRender

如果你在view中有操作DOM元素的代码,那说明你肯定用错了。

view中只应该有操作属性和配置ZRender既有元素(比如Text、Rect、Polygon等)的代码。

用组合的方式拆分页面元素

比如calendar和series的拆分,就是一个很好的例子,这样扩展性强,表现力更好。