D3Charts-日历图的实现原理
如果我来做,我会怎么实现?
页面展示的内容
首先对日历的展示内容做一个拆解,大致包括:
- 主标题(比如年份)
- 子标题(比如月份)
- 日期的整体容器格子
- 单个日期的方格子
- 单个日期的具体显示内容(设计时考虑到高扩展性)
数据结构
日历数据
{
"date": "20200527",
"value": 30
}
配置项
- 语言(中英文)
- 布局(横向、纵向)
- 显示内容的自定义函数(formatter)
- 页面上每个元素的样式配置信息、位置配置信息
事件
- 点击
- hover
具体实现的解析
程序如何识别当前的配置是个日历图?
如何自定义每个表格的内容?
可以参考我们新扩展的自定义类型来实现。
散点图是怎么画上去的?
component下的calendar的代码,并没有实现散点图的功能。
这个是配置在series下面的,应该是每个组件都是和series是合作作图的,组件会将既定的位置用series设置的图形进行绘制。
那么这个结合具体是怎么实现的呢?
在CalendarModel.js中,有一个calcPoint方法,用于计算每个日期的位置:
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属性上:
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中,就会用到这个定位信息,去画图了:
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即可:
let seriesModel = chart.getModel('series', 0)
chart.registerAction(seriesModel, "ITEM_CLICK", function (e) {
console.log(e)
})
一些技巧
先确定布局和数据结构,并制定见文知意的英文名
这样你对于该怎么设计代码中的数据结构、配置项,就会很明确了。后续和别人交流起来也因为定义好了名字,更容易沟通。
给你拆分好的每一类页面元素,都设置对应的样式配置项
这样可以保证你不会遗漏,也会最大化页面表现力的自由度。
将页面上每个元素的渲染独立开
比如类似这样:
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的拆分,就是一个很好的例子,这样扩展性强,表现力更好。