D3Charts学习笔记

今天同事GX分享了关于D3Charts的内容,这些我今后都会用到,因此这里做个记录。

程序设计

d3charts是基于百度的ZRender来做的,可以用装修房子来理解:

ZRender:工具箱,里面有各种工具,比如锤子、扳手、螺丝刀……

d3charts:小物件,比如凳子、桌子、床……

产品:房子

d3charts, 底层依赖矢量图形库zrender,提供直观、交互丰富、可定制化的数据可视化图表。对渲染引擎支持的基本元素、绘制特性、用户交互事件以及图形动画进行封装。提供如矩形、圆形、扇形等基本图形,并支持图形的组合和删除,最终以Canvas内容进行输出。
整体的架构设计如下图所示
arch

执行流程

  • D3charts.init的过程
    • 初始化一个Zrender实例,提供绘制元素、用户交互事件以及图形动画
    • 初始化Zrender事件
    • 创建全局管理GlobalModel,存储图表组件的属性,依靠存储属性进行后续渲染工作
    • 创建zr.animation,监听组件isDirty属性,来触发视图更新
    • 返回chart实例
  • Chart.setOption
    • 清空原有内容
    • 调用GlobalModel.setOption,对每一个Model的option进行处理、合并默认配置项、设置新的配置项。
    • 遍历GlobalModel将图表和组件设置为,触发下一帧组件的更新
    • 遍历GlobalModel初始化每个Model对应的View
    • 下一帧,由于脏位的设置,导致zr.animation中update函数的触发、接着调用doRender函数
    • doRender函数调用每一个组件View文件中的render方法进行视图渲染
    • 在每一个组件的render方法中首先需要通过model获取option对象的配置内容
    • 调用ShapeStorage封装的setShape函数或者setShapeStorage函数,函数内部的实现即调用zrender中的attr绘图函数、animateTo函数

图形扩展

  • 图形的扩展
    每个图表或组件包括ExampleModel.jsExampleView.js以及index.js三个文件。
    图形扩展流程图如下图所示。
    流程
  • 图形事件的外部扩展
    • ExampleView.js中实现图表和组件相关事件
    • 外部人员通过registerAction注册事件、dispatchAction触发事件中的回调函数做相应的操作

数据结构(配置项)

axisPointer

grid

tooltip

dataZoom

series

markArea(这个有点特殊,data是配置在其内部的,不是外部的)

markPoint

animation

data

重点概念

组件(component)

组件的源码位于src/component/下面

每个组件都有一个type属性,标志其是什么类型,比如Legend的type=”legend”,这个值是定义在model中的。

组件相关的类,继承关系如下(以Legend为例):

inherit

数据模型(model)

数据模型,主要是指配置项,注意不包含具体的data

视图(view)

展示相关的部分,根据model进行渲染

组(Group)

Group是ZRender里面定义的一个数据存储结构,作用是将一组Shape进行打包,当成一个整体来处理样式、位置、事件等。

以图Legend中的每一个小图例为例,先来一张图来进行说明:

legend_item_1

这个图包含了2个小图例的示例,上面一个是图标形式的图例,图标+图标文本这2个元素,构成了一个Group。

而整个Legend其实是有多个这种小图例的,所有的小图例又会构成一个Group。因此有时候Group是会层层嵌套的。

包围盒(BoundingRect)

图上一个/一组内容的边界,我们可以通过BoundingRect获取到元素的位置信息(x、y)和宽高信息(width、height)。

比如我们要获取页面上的Legend的BoundingRect信息,可以这样:

1
let {x, y, width, height} = chart.getViewOfComponentModel(chart.getModel('legend', 0)).group.getBoundingRect()

因为LegendView的内容都是放在group中的,因此这里是先获取group属性,在group属性上调用getBoundingRect()方法。

update函数

render函数

配置说明

配置项主要由数据配置项图形配置项构成

所有的根节点配置(series、axis、markLine等等)都是个数组。

(TODO)data-数据配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data: [
{
originData: [
['Jan', 1850, 200],
['Feb', 900, 200],
['Mar', 760, 200],
['Apr', 1300, 200],
['May', 2250, 200],
['Jun', 1650, 200]
]
},
{
originData: [
['Jan', 1850, 200],
['Feb', 900, 200]
]
}
],

data可以在好几个地方出现;出现的位置不同,其作用域也会有差异。

在根配置中出现,和series和axis同级

这种情况可以在series和axis中使用dataKey和$dataIndex来确定使用具体哪个数据。

dataKey是数据键值,与$dataIndex搭配使用,从数据源中获取的对应的数据信息
若数据源为对象(object)形式,即为对象的键值(key); 若为数组,则是数组下标(0、1、2、…)

配置到具体的series和axis中

这种情况,只能在这个具体的series或者axis中生效,不能在其他地方通过dataKey和$dataIndex进行调用。可以理解为一个局部作用域的data。

originData

既然已经有了data,那么为什么还有originData呢?二者有何区别?

另外data下面也有一个originData属性,这又是为了做什么呢?

询问了ZP,原来这是为了兼容两套写法,可能之前不同的同事有不同的偏好。这算是一个错误的设计吧。

目前推荐的用法,还是上面示例的这种,data内部包含originData。

【重要】$dataIndex、dataKey与axis、series、markLine的关系

文档:https://datav.iwencai.com/platform/doc/configdoc/

数据源的概念

可视化组件是数据驱动渲染的,那么想要画出来各种元素,就肯定得有一个将数据和这些元素绑定的环节。D3Charts中的数据绑定,主要就是依赖于$dataIndex、dataKey、$seriesIndex、$axisIndex、$axisPointerIndex、$gridIndex、$dataZoomIndex等这些配置项来实现的。

数据源是通过data这个一级配置项来指定的,可以同时有多个数据源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data: [
{
originData: [
['Jan', 1850, 200],
['Feb', 900, 200],
['Mar', 760, 200],
['Apr', 1300, 200],
['May', 2250, 200],
['Jun', 1650, 200]
]
},
{
originData: [
['Jan', 1850, 200],
['Feb', 900, 200]
]
}
],

关于originData这个特殊的键名:

数据源配置

$dataIndex

数据类型:number

数据源索引,与data配置项联合使用,注意这是一个number数字。

我们设计了可以有多个数据源,所以需要有索引的设计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let option = {
data: [{
originData:[
["Jan", 1850],
["Feb", 1000],
...
]
},
{
originData:[
["一月", 1850],
["二月", 1000],
...
]
}],
axis: [{
position: "bottom"
type: "point",
$dataIndex: 0
}]
}
dataKey

数据类型:number|string

数据键值,与$dataIndex搭配使用,从数据源中获取的对应的数据信息

若数据源为对象(object)形式,即为对象的键值(key); 若为数组,则是数组下标(0、1、2、…)

比如dataKey为键名的情况:

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
const data = [
{
"date": "3月1日",
"x": 123,
"y": 112,
"z": 142
},
{
"date": "3月2日",
"x": 80,
"y": 100,
"z": 132
},
// .......
];

axis: [
{
position: 'bottom',
type: 'point',
$dataIndex: 0,
$dataZoomIndex: 0,
// 对应data的键名
dataKey: 'date',
line: {
show: false
},
tick: {
show: false
}
}
]

比如dataKey为下标的情况:

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
const data = [
['Jan', 1850, 200],
['Feb', 900, 200],
['Mar', 760, 200],
['Apr', 1300, 200],
['May', 2250, 200],
['Jun', 1650, 200]
];

series: [
{
type: 'bar',
// 对应下标为0的数据源
$dataIndex: 0,
// 对应下标为1的数据
dataKey: 1,
name: '销量',
itemStyle: {
normal: {
fill: '#2F97FF'
}
},
stack: '1'
}
]

数据依赖配置

数据依赖配置用于指定当前配置的这个元素,其数据源依赖于某个其他已有的元素。

$axisIndex

数据类型:number| Array

该配置一般出现在axis、markLine、markPoint、markArea、tooltip等配置中。

注意这个配置的数据类型有点特殊,可以是一个数组

当其值是数组时,一般包含2个值,一个代表x轴的下标,一个代表y轴下标。当然,也可以设置3个甚至更多的值。

数组情况多用于axisPointer、series、markLine等,用于确定该元素依赖于哪些坐标轴,也就是用哪些坐标轴的数值来展示该元素的数据,比如下面这样:

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
axis: [
// 0:X轴
{
position: 'bottom',
type: 'band',
$gridIndex: [0, 1, 2],
xOrY: 'x',
$dataIndex: 0,
dataKey: 't',
paddingInner: 0.3,
paddingOuter: 0.3,
barGap: 0,
},
// 1:左侧的Y轴
{
position: 'left',
type: 'linear',
space: [10, 20],
$gridIndex: 0,
xOrY: 'y',

},
// 2:右侧的Y轴
{
position: 'right',
type: 'linear',
$gridIndex: 2,
domainScale: 1.5,
min: 0,
splitNumber: 3,
xOrY: 'y',
}
]

series: [
{
type: 'hqbar',
name: '价格',
hqbarType: 'kline',
// 用x轴和左侧的y轴
$axisIndex: [0, 1],
$dataIndex: 0,
},
{
type: 'bar',
stack: '活跃度',
// 用x轴和右侧的y轴
$axisIndex: [0, 2],
$dataIndex: 1,
dataKey: 'n',
}
]
$seriesIndex

图形配置项

名词

  • 系列配置项
    • series:系列,如柱状图(bar)、折线图(line)、饼图(pie)等,如下图所示。
      bar line bar
  • 坐标系配置项
    • axis:坐标轴
    • axisPointer:坐标轴指示器
    • grid:绘制区域
  • 组件配置项
    • title:标题
    • legend:图例
    • tooltip: 提示框
    • markPoint: 标注
    • timeLine:时间轴,提供d3charts在多个option之间切换、播放的功能
      timeline
    • dataZoom:数据区域缩放,帮助用户可以关注细节主体、概览整体、去除一些离散点的影响
      dataZoom
    • visualMap:视觉映射,将数据映射到视觉元素
      visualMap
  • 后期datav文档中组件配置项可参考系列配置项,帮助用户快速定位配置项在文档中的位置,配置项内容可以设计对应icon

数据

如何更新图表数据?

通过setOption()方法,传入新的配置,进行更新。

假如页面上已经做了一个饼图(图表变量是chart,配置变量是option),现在要更新其数据,可以这样设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let newData = {
"title": "互联网服务与终端",
"values": [{
"value": 1,
"name": "new名称1",
"color": "rgb(0, 100, 0)"
}, {
"value": 2,
"name": "new名称2",
"color": "rgb(10, 100, 0)"
}, {
"value": 3,
"name": "new名称3",
"color": "rgb(20, 100, 0)"
}, {
"value": 4,
"name": "new名称4",
"color": "rgb(30, 100, 0)"
}]
};


option.series[0].data = newData;
chart.setOption(option)

注意setOption()要传入一个完整的配置信息,不能只传入类似这样的简略信息:

1
2
3
4
5
6
7
chart.setOption({
series: [
{
data: newData
}
]
})

(TODO)如何将series的数据转为坐标数据?

事件

事件是在src/action/event.js中定义的,这个文件给每个类型的组件都定义了可以支持的事件列表,然后在对应组件的view中注册事件监听。

view中的代码类似这样:

1
2
3
4
5
6
7
global.registerAction(model, legendActions.click, function ({ name }) {
if (model.get('selectedMode') === 'single') {
model.toggleSingle(name);
} else {
model.toggleMultiple(name);
}
});

global是Charts对象,在src/d3charts.js中定义。registerAction的源码如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* 注册事件
*
* @param {Object} model 组件model
* @param {string} type 事件名称
* @param {function} callback 回调函数
* @param {any} context
*/
registerAction(model, type, callback, context) {
this.on(`${model.id}_${type}`, callback, context || model);
}

而这个Charts是继承自ZRender的src/mixin/Eventful类的,是林峰写的,其on方法的实现如下:

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
/**
* Bind a handler.
*
* @param {string} event The event name.
* @param {string|Object} [query] Condition used on event filter.
* @param {Function} handler The event handler.
* @param {Object} [context]
*/
on: function (event, query, handler, context) {
return on(this, event, query, handler, context, false);
},


function on(eventful, event, query, handler, context, isOnce) {
var _h = eventful._$handlers;

if (typeof query === 'function') {
context = handler;
handler = query;
query = null;
}

if (!handler || !event) {
return eventful;
}

query = normalizeQuery(eventful, query);

if (!_h[event]) {
_h[event] = [];
}

for (var i = 0; i < _h[event].length; i++) {
if (_h[event][i].h === handler) {
return eventful;
}
}

var wrap = {
h: handler,
one: isOnce,
query: query,
ctx: context || eventful,
// FIXME
// Do not publish this feature util it is proved that it makes sense.
callAtLast: handler.zrEventfulCallAtLast
};

var lastIndex = _h[event].length - 1;
var lastWrap = _h[event][lastIndex];
(lastWrap && lastWrap.callAtLast)
? _h[event].splice(lastIndex, 0, wrap)
: _h[event].push(wrap);

return eventful;
}

怎么注册事件?

1、获取对应的model

1
2
// 比如想对series进行事件绑定
let seriesModel = chart.getModel('series', 0)

2、注册事件监听

1
2
3
4
5
chart.registerAction(seriesModel, "ITEM_CLICK", function (e) {
console.log(e)
// 所点击节点的数据
console.log(e.target.data)
})

这里再给一个给markpoint绑定点击事件的例子:

1
2
3
4
let markPointModel = chart.getModel('markPoint', 0)
chart.registerAction(markPointModel, "MARKPOINT_CLICK", function (e) {
console.log(e)
})

(牛逼)时间触发机制(trigger)与切面拦截方法

zrender的src/mixin/Eventful中实现了trigger,而D3Charts的Charts类继承了这个Eventful类。

所以我们可以通过拦截trigger函数,加入一些我们自己的逻辑进去:

1
2
3
4
5
6
7
8
9
10
// 将chart的this绑定到trigger:因为下一行的chart.trigger断开了堆栈引用,而在chart.trigger最后一行又需要用到原始的chart.trigger逻辑
const trigger = chart.trigger.bind(chart)
chart.trigger = (eventType, eventParams) => {
if (eventType === 'mousemove') {
const dpr = window.devicePixelRatio
eventParams.offsetX /= dpr
eventParams.offsetY /= dpr
}
trigger(eventType, eventParams)
}

如何触发自定义事件?

在view的render()方法中,可以看到,其实是通过监听浏览器默认支持的事件,然后手动调用global.dispatchAction()方法来触发组件事件的:

1
2
3
4
5
shape.on('mouseover', function (e) {
let { _name: name } = this;

global.dispatchAction(model, legendActions.mouseover, { e, name, item, model });
});

这里的shape,是LegendItem对象,每一个LegendItem对象,对应一个Legend数据,因此有几个图例,初始化view的时候就会执行几次事件绑定操作。

我们支持注册哪些事件?

在控制台打印D3Charts.action,可以看到每个元素支持的事件。

可以先看一遍src/action/event.js中定义的事件。

事件对象提供了哪些属性?

了解这个很重要,因为这决定了我们触发事件后可以做哪些事情。

通过上面的代码,打印出来的e就是事件对象,可以查看其属性来进行了解。

props

像这里有offsetX、offsetY属性,表示点击位置的x、y值,可以用于做一些特殊交互效果,比如在点击的位置出现一个tip。

legend点击事件

禁止左右滑动页面(解决在canvas元素上滑动时,触发页面前进后退的问题)

可以通过禁用默认的touchmove事件来解决:

1
document.querySelector('#chart').addEventListener("touchmove", (event) => event.preventDefault(), false);

PS:常规app中,快速滑动的意图就是翻页,所以图表应该对touch事件做个处理:当用户按下超过一定时间时,才触发图表内部的事件;如果是快速滑动,则触发浏览器本身的事件。

延迟交互(移动端通用交互方案)

自定义元素的缩放跟随

比如散点图,要求绘制自定义的图形元素,改元素的定位和具体的散点绑定,当缩放平移时要随之同步移动。

方案一:如果有父group,则跟随父group移动

方案二:拦截缩放事件,重新定位

动画

自定义动画,经常会用到遮罩层+裁剪框来解决。

因为D3Charts有layer的设计,因此裁剪框是可以和series处于同一层级来对齐的,不用担心会把坐标轴给遮住了。

也可以由业务代码来进行裁剪框的位置计算,确保裁剪框大小和想要遮住的图形完全匹配。

如果想要关闭动画,设置option.animation = false即可。

折线图从左到右显示的动画

可以通过控制一个遮罩的矩形的动画来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var chart = D3Charts.init("chart");
chart.setOption(option);
chart.on("VIEW_DID_RENDER_AFTER_SET_OPTION", function () {
const model = chart.getModel("series", 0);
const view = chart.getViewOfComponentModel(model);
const line = view._shapeMap.line;
const area = view._shapeMap.area;
const { x, y, width, height } = area.getBoundingRect();
const rect = new D3Charts.graphic.Rect({
shape: {
x,
y: y - 2,
width: 0,
height: height + 4,
},
});
// 裁剪元素,隐藏超出部分
line.setClipPath(rect);
area.setClipPath(rect);
// API:https://ecomfe.github.io/zrender-doc/public/api.html#zrenderanimatableanimatepath-loop
rect.animate("shape", false).when(1000, { width }).start();
});

鼠标

禁用鼠标滚动

通过option.preventDefaultMouseWheel进行配置。默认是true,即在图形区域滚动鼠标,只会操控图形,不会滚动页面。

坐标系

获取鼠标点击位置的坐标值

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
model = chart.getModel("axis", 0);
let tooltip = document.getElementById("tooltip");
if (!tooltip) {
tooltip = document.createElement("div");
tooltip.style.position = "fixed";
tooltip.style.display = "none";
tooltip.style.pointerEvents = "none";
document.body.appendChild(tooltip);
}
chart.registerAction(model, "AXIS_MOVE", function (e) {
tooltip.style.display = "block";
tooltip.style.right =
window.innerWidth - e.e.event.clientX + 10 + "px";
tooltip.style.top = e.e.event.clientY + "px";
const point = kData[e.dataIndex];

let time = e.axisData;
tooltip.innerHTML = `<div
class="d3charts-tooltip"
style="background-color: rgba(57, 93, 129, 0.6); border-color: rgb(51, 51, 51); border-style: solid; border-width: 0px; border-radius: 5px; padding: 5px; color: white; left: 100px; top: 0px;"
>
<div>
<span>时间:</span>
<span>${time.substr(0, 2)}:${time.substr(2, 2)}</span>
</div>
<div>
<span>最新:</span>
<span>${point.nowp}</span>
</div>
</div>`;
});
chart.registerAction(model, "AXIS_OUT", function (e) {
tooltip.style.display = "none";
});

富文本标签

为什么要叫富文本标签?因为这个只对label生效。

我们这个是完全参考ECharts的富文本标签的设计,因此可以查看ECharts的文档:https://echarts.apache.org/zh/tutorial.html

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"label": {
"show": true,
"padding": 10,
"formatter": (d) => `{first|${d}}\n{second|222}`,
"style": {
"fill": "rgba(51, 51, 51, 0.4)",
"fontSize": 12,
"rich": {
"first": {
"fontSize": 20,
"fill": "red"
},
"second": {
"fontSize": 16,
"fill": "green"
}
}
}
},

注意我们的富文本标签比ECharts弱很多,他们在ZRender之上做了一些关于富文本标签的强化,特别是背景、位置这一块。如果我们想要修改位置,只能通过调整textLineHeighttextPadding等属性进行有限的修改;position和padding是无效的,这种只对legend有效,即本质是对图形上面画文本的这种有效果。

常见配置问题

我通过二个单词量统计和一个上证估值的图形来实践这些配置:

单词量统计:

------

上证估值:

------

style自定义样式

注意:style整体支持自定义函数,但是style下面的子属性是不支持自定义函数的。

比如这样是错误的:

1
2
3
4
5
style: {
fill: function (d) {
return "#F00";
}
}

这样才是正确的:

1
2
3
4
5
style: function (d) {
return {
fill: "#F00"
}
}

颜色

渐变

通过series[x].itemStyle.nomal.fill.colorStops进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
itemStyle: {
normal: {
fill: {
y2: 1,
x2: 0,
colorStops: [
{
offset: 1,
color: "#d74344"
},
{
offset: 0,
color: "#ff6c00"
}
]
}
}
}

线(Line)

折线的点怎么显示?

series.symbol相关的配置。

比如我要配置点的形式为圆圈,且在鼠标选中时变成实心填充,那么可以在series[x]下面这样配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"symbol": {
"normal": {
"show": "all",
"size": 6,
"style": {
"fill": "white",
"stroke": "#2e96ff",
"lineWidth": 2
}
},
// 选中状态
"emphasis": {
"show": true,
"style": {
"fill": "#2e96ff",
"stroke": "#2e96ff"
}
}
}

如何画虚线?

series下面的line下面的lineDash

1
2
3
4
5
6
7
8
9
10
"line": {
"show": true,
"style": {
"normal": {
// [4, 4] 分别为实线、间隔的长度,可以调整疏密
lineDash: [4, 4],
"stroke": "#2F97FF"
}
}
}

如何画横线?比如中位数

option[x].markline

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
markLine: [{
$axisIndex: [0, 1],
$seriesIndex: 0,
data: [{
type: 'average',
value: '平均数:' + average,
lineStyle: {
normal: {
stroke: '#0000FF',
lineDash: [2, 5]
}
}
},
// {
// type: 'max'
// },
{
points: [{
x: 'left',
yValue: 0
},
{
x: 'right',
yValue: 0
},
],
value: '0',
// 文字样式
label: {
normal: {
show: true,
style: {
offset: [0, -10],
position: 'insideLeft'
}
}
},
lineStyle: {
normal: {
fill: '#FF0000',
stroke: '#FF0000',
lineDash: [2, 5]
}
}

}
]
}
],

如何将折线图配置成阶梯状的图形?

通过series[x].curve.type属性进行配置,将其设置为step即可。

如果发现一字板涨停的,线贴到了底部的轴上面,可能是因为 Y 轴设置了最大最小值;这种情况应该要自定义一个 y 轴的 tickValues,因为 y 轴就一个数值。

如何分段着色?

通过series[x].piece来设置,这个值是一个数组。

这个是参考百度的配置的:*//echarts.baidu.com/option.html#visualMap-piecewise.pieces*

参考示例://datav.iwencai.com/platform/chartconfig.html#chartId=229

1
2
3
4
5
6
7
8
9
series: [{
pieces: [{
min: 0,
color: 'rgb(255,86,70)'
}, {
max: 0,
color: 'rgb(45,205,205)'
}]
}]

如何让图形和顶部贴合(涨停价)

domainScale

比如上图就是没有贴合的,这不符合我们的客户端效果。

可以通过将yaxis的domainScale设置为1来实现。

如果是一字板涨停,用上面的方式就行不通了,因为一字板涨停只有一个数值。

针对这种情况,我们需要在图表上,将昨收也画出来,只画一个点,这样和今天的涨停价形成差异,就可以把涨停价画到最上面了。

图形把tooltip遮住了

domainScale

类似这种,而且这个问题只有在ios13的机型下才会复现。
这可能是因为设置了transform: translateZ,导致z-index失效了。
可以给父级设置overflow: hidden试试。

柱子(Bar)

修改柱子宽度

调整series[x].rectWidth属性,仅适用于x轴为连续类型坐标轴(linear、time等),也就是说你得把x轴的axis的type属性设置为linear或者time。

而柱状图一般都是非连续类型的坐标轴,因此设置其宽度,不能通过rectWidth来实现。我们是通过axis[x]下面的paddingInnerpaddingOutter这2个属性的百分比计算来实现的。

还有一种奇特的思路:可以考虑扩充data数据到最小长度(比如给三个柱子的数据但是首位柱子不进行绘制)来做处理。

做出左右布局的柱子效果

bar

想要做出类似上图的效果,可以将下面几个柱子的数值乘以-1

柱子的圆角配置

可以通过bar.rectRadius进行配置。

数值的形状

通过series[x].symbol进行配置

tooltip

tooltip常见的问题是定位,比如最近贝贝问到的:

画图这个tooltip,目前交互规范是手指放在右边tip提示显示在左上方,手指在左边tip显示右上方,如果要实现这种,你们这边有什么方法吗?
类似这样的效果:http://testm.10jqka.com.cn/mobile/info/f10_front/dist/index.html#/cashflowchart

可以通过给tooltip的position属性传入一个function来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"tooltip": [{
"trigger": "axis",
"$axisIndex": 0,
"style": {
"backgroundColor": "rgba(57,93,129,0.6)"
},
"position": function(e){
if(e.x < (myChart.getDom().offsetWidth/2)){
return {x: myChart.getDom().offsetWidth, y: 0}
}else{
return {x: 0, y: 0}
}
},
}],

注意这种方式,仅限有双Y轴的图形。
单Y轴的图形,会因为右侧预留了一块Y轴区域,导致tooltip显示在右侧时,会超出grid范围。

还可以这样通过获取各个元素的包围盒,计算后得出tooltip的位置:

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
tooltip: [{
trigger: 'axis',
$axisIndex: 0,
position: function (d) {
// 整个画布的包围盒信息
let {x, y, width, height} = myChart.getViewOfComponentModel(myChart.getModel('series', 0)).group.getBoundingRect()

// tooltip的包围盒信息
let tooltipBoundingRect = myChart.getViewOfComponentModel(myChart.getModel('tooltip', 0)).group.getBoundingRect()
console.log(tooltipBoundingRect);
console.log(x, y, width, height);
// TODO
if (d.x < (x + width / 2)) {
return {
x: x,
y: y
}
} else {
return {
x: x + width - tooltipBoundingRect.width,
y: y
}
}
}
}]

图例(Legend)

图例太多,显示不下怎么办?

可以获取图例的高度,然后将作图区域下移即可,如下为相关的代码:

1
2
3
4
5
6
7
var gridModel = chart.getModel('grid', 0);
var legendModel = chart.getModel('legend', 0);
// 获取legend包围盒的位置 把grid的top动态下移
chart.registerAction(legendModel, 'VIEW_DID_RENDER', function ({ view }) {
var rect = view.group.getBoundingRect();
gridModel.set('top', rect.y + rect.height + 50);
})

网格grid

双y轴如何只设置一种分割线?

在axis对应的轴设置splitLine: {show: false}

数值

如何对数值做精度处理?

如果是想对坐标轴的数值做处理,可以在option.axis[x].lable.formatter中进行配置

默认展示的数值:

series[x].label.nomal.formatter

鼠标移上去出现的数值:

series[x].label.emphasis.formatter

如何限制显示的数值的最大值/最小值?

axis[x].max

axis[x].min

如何让包含负数的图形,将负数展示在下方区域?

axis[x].line.onZero = true

这个是只针对柱状图的,折线图没有这个需求

如何让线/柱子上的数值一直显示?

如果是柱状图,配置series[x].label.show=all即可

如果是折线图,必须要把symbol开启(配置series[x].symbol.nomal.show=all), 如果不想看到它可以把size设成0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"symbol": {
"normal": {
"show": "all",
"size": 6,
"style": {
"fill": "white",
"stroke": "#2DCDCD",
"lineWidth": 2
}
},
"emphasis": {
"show": true,
"style": {
"fill": "#2DCDCD",
"stroke": "#2DCDCD"
}
}
},

show=all 表示一直显示

show=true 会根据数量自动选择一部分显示

如何配置对数数值?

对数数值要注意负数的情况。

同事ZHQ采用了一个很牛逼的思路:先将数据做对数处理,然后用处理完的数据,做一个线性比例尺,再作图。

坐标轴

自定义label样式

可以通过将lable的style设置为一个Lambda函数来实现个性化样式:

1
2
3
4
5
6
7
8
9
10
11
"label": {
"show": true,
"padding": 20,
"style": (data, index) => {
return {
fill: `rgba(255, 0, 0, 0.5)`,
fontSize: (index + 1) * 5,
textAlign: index % 3 === 0 ? 'left' : 'right'
}
}
},

数值倾斜

axis[x].lable.rotate 设置倾斜角度

1
2
3
4
5
6
7
8
9
"label": {
"show": true,
"padding": 10,
"style": {
"fill": "rgba(0, 0, 0, 0.3)",
"fontSize": 12
},
"rotate": 0
},

自定义坐标轴显示的刻度(数值和数量)

可以先通过你传入的data计算一下你希望显示的x轴上标签的数量和内容,然后通过axis[x].tickValues传入这个数组。

可以参考这个Demo,tickValues取均值显示。

另外还可以设置interval属性来控制显示的坐标值的间隔,比如设置为2,就可以每2个才显示一次:

interval

不显示坐标轴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
axis: [
{
position: 'left',
type: 'linear',
xOrY: 'y',
nice: true,
splitNumber: 4,
// 作图区域的分割线
splitLine: {
show: false
},
tick: {
show: false
},
line: {
show: false
},
label: {
show: false
}
}
],

默认选中某个axisPointer

1
2
3
4
5
6
7
8
9
10
function staicAxisPointer(chart, index, delay) {
let axisModel = chart.getModel('axis', 0)
setTimeout(() => {
let event = axisModel.getMoveDataByIndex(index)
chart.dispatchAction(axisModel, 'AXIS_MOVE', event)
}, delay)
}

// 默认选中某个
staicAxisPointer(chart, 5, 500)

待补充

axisName

tick显示具体某几个值splitNumber、tickValues

回调中的domain

标记线(markLine)

如何绑定数据?

注意事项

markLine一定得定义2个点

markLine如果设置了yValue就不需要和series绑定了(即无需配置$seriesIndex),因为和series绑定就是为了确定Y的值

(待定)markLine中的value,似乎是定义标签内容的,不是确定线的位置的

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
// markLine是根节点配置,所有的根节点配置都是个数组
markLine: [
{
$axisIndex: [0, 2],
// 如果设置了yValue就不需要和series绑定了(即无需配置$seriesIndex),因为和series绑定就是为了确定Y的值
//$seriesIndex: 1,
data: [
{
// markLine一定得定义2个点
points: [
{
x: 'left',
yValue: 30,
},
{
x: 'right',
yValue: 30,
},
],
// markLine中的value,似乎是定义标签内容的,不是确定线的位置的
value: 45,
label: {
show: true,
},
lineStyle: {
normal: {
fill: '#FF0000',
stroke: '#FF0000',
lineDash: [2, 5],
},
},
},
],
},
],

标记点(markPoint)

自定义图标

这部分内容摘自Echarts的文档。

考虑到通用性和扩展性,一般图标我们都是采用svg和自定义图片来实现的

svg就是用户自己传入一个图标的path标签的d路径的值;

自定义图片就是用户传入一个图片的url,一般可以通过 'image://url' 设置为图片,其中 URL 为图片的链接,或者 dataURI

URL 为图片链接例如:

1
'image:////xxx.xxx.xxx/a/b.png'

URL 为 dataURI 例如:

1
'image://data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7'

可以通过 'path://' 将图标设置为任意的矢量路径。这种方式相比于使用图片的方式,不用担心因为缩放而产生锯齿或模糊,而且可以设置为任意颜色。路径图形会自适应调整为合适的大小。路径的格式参见 SVG PathData。可以从 Adobe Illustrator 等工具编辑导出。

例如:

1
'path://M30.9,53.2C16.8,53.2,5.3,41.7,5.3,27.6S16.8,2,30.9,2C45,2,56.4,13.5,56.4,27.6S45,53.2,30.9,53.2z M30.9,3.5C17.6,3.5,6.8,14.4,6.8,27.6c0,13.3,10.8,24.1,24.101,24.1C44.2,51.7,55,40.9,55,27.6C54.9,14.4,44.1,3.5,30.9,3.5z M36.9,35.8c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H36c0.5,0,0.9,0.4,0.9,1V35.8z M27.8,35.8 c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H27c0.5,0,0.9,0.4,0.9,1L27.8,35.8L27.8,35.8z'

给指定的数据设置相同的markPoint

给指定的数据设置不同的markPoint

http://datav.iwencai.com/platform/chartconfig.html#chartId=261

可以通过label.nomal.formatter来设置

如何让svg画的markPoint的边框自适应文字的大小和高宽?

svg是不能自适应文字的,因此这个需要动态计算。

方案一:可以拿到scale,算出x,y,然后自己通过dom实现定位,加上去;然后监听缩放事件,每次触发缩放,重新获取每个元素的位置,去设置自定义的dom的位置。

方案二:动态计算svg的path的值,给不同长度的文本,生成不同大小的外框。

(TODO)通过重绘markPoint实现每个图形加一些特殊效果

参考郑浩琦的例子。鼠标滑过每个柱子时,修改markPoint的数据,进而数据驱动图形变更。

堆叠图

在series下的每个图的配置中,增加stack属性即可,其值可以随便写,类似这样:

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
"series": [{
"type": "line",
"$dataIndex": 0,
"dataKey": "x",
"name": "销量 1",
"stack": "总销量",
"symbol": {
"emphasis": {
"show": true,
"size": 10,
"style": {
"fill": "white",
"stroke": "#2F97FF"
}
}
},
"line": {
"show": true,
"style": {
"normal": {
"stroke": "#2F97FF"
}
}
},
"area": {
"show": true,
"style": {
"normal": {
"fill": "#2F97FF",
"opacity": 0.2
}
}
},
"link": {
"emphasis": {
"show": true,
"style": {
"stroke": "#FFFFFF"
}
}
}
}]

缩放

字体

如何解决引入的字体文件非https协议的问题?

可以引入这个css文件来解决:

1
https://s.thsi.cn/js/d3chart/style/v0.1.0/index.css

兼容性问题

K线的tip在iOS上,文字会下移

compatibility

如果是字的话,可能是安卓使用微软雅黑默认字体,会稍稍偏上一点(安卓的微软雅黑居中就是偏上一些的,所以一般是建议设置固定字体)
解决方法可以是换一个字体试一试。

如果还是不行,可以试试设置textStyle: {y: 3}

其他问题

程序入口在哪里?

HTML页面上初始化D3Charts的时候:

1
D3Charts.init('zr');

不过d3charts.js这个文件中,并未注明导出的类名是D3Charts,那么是在什么地方配置这个类名的呢?

TODO: 不知道是不是在build/pack.js中构建时定义的。

View是怎么最终渲染到页面上的??

TODO : 好像是通过View的setShape方法,传入配置作图的。这个方法实际上是在ShapeStorage这个类中定义的。

这个方法会传入配置数据、图形类型(比如Rect、Circle),然后由对应图形的类去做渲染操作。这个设计很不错,将数据和渲染分离了,采用了配置化思维。

所有shape都是相对父元素做的绝对定位(left, top),因为这是用Canvas实现

render()何时被调用?

是d3charts.js中的update()方法调用的,在下面三种场景会触发:

1、初始化

初始化D3Charts的时候,在构造函数里面,通过zr.animation.on注册了每一帧的事件监听,一旦发现model中有脏数据,就会触发重新渲染。

页面初始化时的渲染,虽然程序中有显式调用chart.setOption()方法,但是实际上仍然是通过frame事件触发的;页面内容修改后,比如我点击一个legend使其不显示,也仍然是触发了frame事件进行的渲染。

2、setOption()

3、resize

TODO : ZRender也有resize,稍后看下怎么实现的:

1
2
3
4
resize(opts) {
this._zr.resize(opts);
this.update(true);
}

脏(dirty)的设计

这个设计来自ZRender,不是我们自己想的。

(TODO)控制台这么多默认输出,怎么清理掉?

为什么LegendView中会传入seriesModel对象?

因为Legend的有些配置,是直接根据series来设置的,比如图例的颜色、名称等等。因此这个seriesModel不可或缺。

(TODO)为什么Echarts的源码无法点击跟进,而D3Charts可以?

不是全部都不可用,是有的可以有的不行。

TODO : 需要找到原因。

Echarts的defaultOption的设计?

不是作为类属性,只是默认值而已。

TODO : 应该在extendComponentModel中有自动将未定义的属性挂上去的操作吧?

为什么import的ZRender里面的内容无法跟进去?

为什么VSCode只显示一个打开的文件?

如何阅读源码?

1、从页面元素的渲染入手

2、从事件触发后的流程入手

如何修改某个元素的定位?

获取元素的model引用

1
let tooltipModel = chart.getModel('tooltip', 0)

获取位置信息

通过事件来获取:

1
2
3
chart.registerAction(seriesModel, "ITEM_CLICK", function (e) {
console.log(e)
})

修改定位信息

比如修改坐标信息、样式信息。

不能直接通过model.xxx来修改model的属性,因为这样不会触发重新绘制;必须通过model.set()来设置。

(TODO:未生效)tooltip没有left和top,但是有offset属性和position属性,默认position=undefined

1
2
3
4
5
6
let tooltipModel = chart.getModel('tooltip', 0)
tooltipModel.set('offset', [50, 50])

let style = tooltipModel.get('style')
style.display = 'block'
tooltipModel.set('style', style)

legend的group有left和top属性,但是没有offset属性;有个position属性,里面包含left、right、top、width:

1
2
3
var legendModel = chart.getModel('legend', 0)
legendModel.set('left', 200)
legendModel.set('top', 200)

series的定位,则不同形状不一样,以弧形来说,就是修改其center属性,即圆心位置:

1
2
let seriesModel = chart.getModel('series', 0)
seriesModel.set('center', ['20%', '50%'])

怎么在触发事件后,显示tip/如何定位tooltip?

(TODO)怎么实现各种圆润的动画?

比如tip的浮动、出现、隐藏等等,要有平滑感和科技感

我们的移动端是怎么实现的?也是media么?

不是,我们没有考虑自适应,是通过应用方自己设置高宽来解决的

我们的D3Charts是基于ZRender哪个版本?相当于Echarts哪个版本?

ZRender4.0.7,是2019.02.25发布的,算比较新的

(TODO)Canvas的重绘到底有多高效?我看拖拽这种是每次都会重绘,为什么我之前画的那个棋盘就很卡呢?

我们有grapic么?

没有,不过有markpoint可以自定义形状,但是注意这个markPoint是series的下属属性,而grapic是和series平级的属性,可以在画布上随意画图形
ECharts也有markPoint

我们的可视化新人培训教程,看是否科学

时间上感觉够呛,还有就是参考资料不够多,估计还是需要一对一讲解把。
我缺少了CSS、JS基础、行情图、数据池、扩展、实践等内容

如果我去教别人用d3charts,我会怎么教?

难在入门。一旦入门,就很简单了。

入门阶段不要怕问人,这个阶段问人的性价比极高。

名词定义

设计原理

常用配置的实现

把这个页面的图形,都去配置一遍:

//activity.10jqka.com.cn/acmake/cache/523.html#/index

概念名词和API讲解

这个过程可能会比较痛苦一些,尽量压缩在一两天内完成,然后借助Anki背诵、复习、记忆。

要想玩的溜,必须要把小的东西(名词、常用概念、常用API)掌握得特别熟。这一关免不了。

跟着上面的执行流程走一遍代码

遇到问题怎么查资料?

时间评估

SXY:

从开始接触d3charts到画出竹节图,大约耗时2周

这次的Legend实现,SXY评估需要3天

如何编写新的组件?

README文档

文档先行,这是首要原则。

跟业务方的开发确认好API后,就不要改动API了,不然别人使用会很麻烦。

如何编写组件

我们有一个组件模板,通过该模板编写成npm组件,上传到公司内部的组件库,其他部门的同事就可以安装使用了。

内部npm仓库:

1
2
3
4
//think.10jqka.com.cn/cnpm/browse/keyword/datav      

需要配置host:
172.19.80.195 think.10jqka.com.cn

上传npm的组件,推荐使用 thsc-datav-xxxx格式的命名,比如thsc-datav-waterwave,避免和第三方组件重名。

可以参考这个组件

如何上传组件Demo到datav平台?

具体信息见我的Evernote笔记《如何上传组件到datav?》

后台地址:https://172.20.207.139/zfontology/

这个后台是用Zend框架写的,和问财其他后台耦合到一起了目前暂时不接过来,由张浩继续维护;后面有大的修改再接过来,用Node重写下。

Demo的源码在http://gitlab.10jqka.com.cn/datav/chart-config-doc这个项目下:

  • chart-config:Demo

Demo右侧的代码编辑区域,是通过Code Mirror插件实现的。

最近增加了配置高亮功能,是根据url参数,添加class样式高亮来实现的。

  • chart-config-better:ZHQ弄的高性能版
  • chartPlatform:文档类的都在这里

文档类的,是先通过js文件编写,然后通过工具转为markdown格式,再通过工具转为html格式的。

极大提升表现力的技巧

D3Charts+自定义DOM元素+定位

如果你想要在页面上展示一些很个性的内容,那么可以这样处理。

比如下面这个图:

pie_self_defined

就可以获取“持仓占比XX%”这个label的位置,然后自己定义一个DOM,放到左侧或者右侧,展示类似“货币基金”那一块的内容。

获取label位置的代码如下:

1
let {x, y, width, height} = chart.getViewOfComponentModel(chart.getModel('legend', 0)).group.getBoundingRect()

如何解决布局问题?

个性化问题,绝大部分都是布局问题和局部元素的定制化问题。

如果我能够把这2个问题抽象出来解决掉,那就可以极大扩展D3Charts的能力了。

比如下面这个需求:

pie_self_defined

这就是一个布局问题,不同的数据,布局不一样。这种如何方便、可扩展的实现呢?