仿照git马赛克墙实现的JS原生热力图数据可视化工具,支持自定义日期、支持自定义横纵坐标等javascript
先上效果图css
说一说实现的心理历程,原本想像码云那个同样用div来实现的,可是脑子灵光一闪,我怎么不用canvas试一试呢,以前没什么机会接触过,恰好来练练手。 因而就实现了一波:java
this.init = (dom) => {
var section = this.option.size + this.option.gap
dom.width = section * this.option.xAxis.length
dom.height = section * this.option.yAxis.length
// 调用canvas对象的getContext()方法,c变量就包含了指向2d渲染环境的引用
var context = dom.getContext('2d')
var levelGap = (this.option.max - this.option.min) / 4
this.option.data.forEach((item) => {
// [x, y, value]
var differ = this.option.max - item[2]
switch(true) {
case (item[2] >= this.option.max) :
color = 'rgb(25, 97, 39)'
break
case (differ < levelGap) :
color = 'rgb(35, 154, 59)'
break
case (differ < levelGap * 2) :
color = 'rgb(123, 201, 111)'
break
case (differ < levelGap * 3) :
color = 'rgb(198, 228, 139)'
break
default:
color = 'rgb(235, 237, 240)'
}
context.fillStyle = color
context.fillRect(item[0] * section, item[1] * section, this.option.size, this.option.size)
})
}
复制代码
写到一半,发现不行啊,人家canvas是一整张画布,可是我是须要一个个小方格能够实现鼠标悬浮显示详细信息的呀,不妥不妥。因而我又想到了svg这个好家伙,F12看了下git,正好人家也是使用svg实现的。git
实现过程不复杂,就是比较绕,感兴趣怎么实现的小伙伴,我把项目地址贴出来了噢。若是个人组件是你须要用到的,那欢迎使用哈哈哈哈~~,下面说一说怎么使用,还有一些思考 github
仿照git马赛克墙实现了个JS原生小工具,支持自定义日期、支持自定义横纵坐标npm
使用起来很简单,只要引入css文件和js文件便可canvas
引入css:bash
<link rel="stylesheet" href="./heatMap.css" />
复制代码
引入js:数据结构
<script src="./heatMap.js"></script>
复制代码
<script>
var heatMap = new HeatMapDate()
var option = {
gap: 6,
type: 'custom',
xAxis: ['11', '22', '33', '44'],
yAxis: ['aa', 'bb'],
data: [
[0, 0, 0],
// ...
[3, 1, 1]
],
min: 0,
max: 6,
tip: {
show: true,
formatter: '第{y}月的第{x}天有{b}个提交'
}
}
heatMap.setOption(option)
heatMap.init(document.getElementById('mySvg'))
<\script>
复制代码
heatMap.setOption(option)
复制代码
heatMap.init(dom)
复制代码
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
type | 热力图类型,分两种,date-日历型和custom-自定义型 | String | date/custom | date |
xAxis | 横坐标的label,当type为custom类型时须要传递该参数 | Array | - | - |
yAxis | 纵坐标的label,当type为custom类型时须要传递该参数 | Array | - | - |
gap | 方格之间的间隔 | Number | - | 3 |
data | 数据,若是type是date,那data是Object类型,格式{yyyy-MM-dd: value ...};若是type是custom,那data是Array类型,格式[[x,y,value]...] | Object/Array | - | - |
dateStart | 当type为date类型时起做用,表示起始日期,日期格式:yyyy-MM-dd | String | - | 去年的今天 |
rect | 方格的相关属性 | Object | - | - |
dateEnd | 当type为date类型时起做用,表示结束日期,日期格式:yyyy-MM-dd | String | - | 今天 |
min | 分级的最低值,默认总共五个等级 | Number | - | 0 |
max | 分级的最高值,默认总共五个等级 | Number | - | data里头的值的最大值 |
tip | 方格顶部鼠标悬浮小气泡的相关属性 | Object | - | - |
tip.show | 鼠标悬浮是否显示小气泡 | Boolean | true/false | true |
tip.formatter | 小气泡的文本内容。type为date的时候{a}表示日期,{b}表示数值;type为custom的时候{x}表示x轴对应的值,{y}对应y轴的值,{b}表示数值;若是在替换字符串前加反斜杠(例如/{b}),则不会替换该字符串 | String | - | - |
rect | 方格的相关属性 | Object | - | - |
rect.stroke | 方格边框的相关属性 | Object | - | - |
rect.stroke.show | 是否显示方格边框 | Boolean | true/false | false |
rect.stroke.background | 方格边框颜色 | String | - | #333333 |
rect.stroke.opacity | 方格边框透明度 | Float | 0~1 | 0.6 |
rect.colourMatching | 方格配色方案,能够自定义(custom)也可使用现有的配色方案 | String | custom/green/pink/blue/orange/gray | green |
rect.stroke.backgroundArr | 当type为custom类型时起做用,方格配色方案具体颜色,多少个颜色就表示多少个等级,等级由重到轻,第一个颜色表示等级最重 | Array | - | - |
说一说实现过程当中的痛点:app
日历型我知道横轴就是月份,纵轴就是星期,因此我new新的热力图的时候,传的数据里头只要有日期和值就行。但自定义就不同了,自定义的横纵坐标都是后来定义的,因此为了知道具体的哪一个小方格的值,传的数据里头必须像[x, y, value]
这样的结构。
日历型热力图默认是当前时间往前推一年这样一个时间跨度,因此我必须轮询出这一年里的全部日期,再拼装组合起来。后期考虑到了灵活性,因此这个时间跨度也是能够自定义的。
小方格的size关乎到左边区域的大小。总的来讲就是当前容器的宽度减去左边区域大小,再除以列数,写的时候的逻辑比较绕,跟写高数题似的,想起了当年被支配的恐惧。。
顶部悬浮气泡实际上我只定义了一个容器,每次调用的时候,改变定位和文字内容。
配色这一块,有多少个配色就有多少个等级。我不但愿把颜色定死,因此本身组合定义了五种配色方案:green、pink、blue、orange、gray。考虑到有改变等级和配色的须要,配置项能够自定义。
总的来讲,实现的思路并不难,就是实现的过程比较蛋疼。后期我打算增长一些自定义功能,而后作成根据屏幕大小来动态改变布局。有想法将它封装成npm工具,也算是个人第一个正儿八经开源小工具啦,源码我已经贴出了来,有须要一块儿学习的小伙伴自行clone~~笔芯
这就是我实现这个小工具的最核心代码:
/** * 对Date的扩展,将 Date 转化为指定格式的String * 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 能够用 1-2 个占位符, * 年(y)能够用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字) * 例子: * (new Date()).Format("yyyy-MM-dd hh:mm:ss.S") ==> 2006-07-02 08:09:04.423 * (new Date()).Format("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18 * @param fmt 日期格式 */
Date.prototype.format = function (fmt) {
var o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o)
if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return fmt;
}
/** * 字符串转换为日期对象 * @param dateStr Date 格式为yyyy-MM-dd HH:mm:ss,必须按年月日时分秒的顺序,中间分隔符不限制 */
Date.prototype.strToDate = function (dateStr) {
var data = dateStr
var reCat = /(\d{1,4})/gm
var t = data.match(reCat)
t[1] = t[1] - 1
eval('var d = new Date(' + t.join(',') + ')')
return d
}
/** * 获取日期列表,不传参默认当前时间为截止日期,去年的今天为起始日期 * @param dateStart 起始日期 格式为yyyy-MM-dd,必须按年月日的顺序,中间分隔符不限制 * @param dateEnd 截止日期 格式为yyyy-MM-dd,必须按年月日的顺序,中间分隔符不限制 */
Date.prototype.getDateList = function (dateStart, dateEnd) {
try {
var date, diff, list = {}
// 缺一不可,缺任何一个都采起默认时间
if (!dateStart || !dateEnd) {
// 当前时间
date = new Date()
// 当前时间往前推一年
date.setFullYear(date.getFullYear() - 1)
// 若是去年的今天不是星期天的话,补充天数直到起始日期是星期天为止
while (date.getDay() > 0) {
date.setDate(date.getDate() - 1)
}
// 计算相差的天数
diff = parseInt((new Date().getTime() - date.getTime()) / (1000 * 60 * 60 * 24))
} else {
// 转换为日期对象
var start = this.strToDate(dateStart)
var end = this.strToDate(dateEnd)
// 补充天数直到起始日期是星期天为止
while (start.getDay() > 0) {
start.setDate(start.getDate() - 1)
}
// 计算相差的天数
diff = parseInt((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))
date = start
}
// 实际上连最后一天也要算上,因此diff加2
for (i = 1; i < diff + 2; i++) {
list[date.format("yyyy-MM-dd")] = date.getDay()
date.setDate(date.getDate() + 1)
}
return list
} catch(e) {
return {}
}
}
var weekMap = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
var monthMap = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
var colorMap = {
'blue': ['#003C9D', '#409EFF', '#87CEFA', '#E0FFFF', '#EBEDF0'],
'pink': ['#990099', '#CC00CC', '#FF88C2', '#FFB7DD', '#EBEDF0'],
'green': ['#196127', '#239A3B', '#7BC96F', '#C6E48B', '#EBEDF0'],
'orange': ['#A42D00', '#CC6600', '#EE7700', '#FFAA33', '#EBEDF0'],
'gray': ['#303133', '#444444', '#808080', '#C0C0C0', '#EBEDF0']
}
// 合并两个object,以mainObj为基准
function matchObj(mainObj, obj) {
var resultObj = {}
for (var key in mainObj) {
if (!obj.hasOwnProperty(key)) {
resultObj[key] = mainObj[key]
} else if (Object.prototype.toString.call(mainObj[key]) === '[Object Object]' && key !== 'data') {
resultObj[key] = matchObj(mainObj[key], obj[key])
} else {
resultObj[key] = obj[key]
}
}
return resultObj
}
function HeatMapDate() {
this.option = {
type: 'date', // 类型:date-日历型,custom-自定义型
xAxis: [], // 横坐标的label,type=custom起做用
yAxis: [], // 纵坐标的label,type=custom起做用
gap: 3, // 方格之间的间隔
/* 数据 * 若是type是date。那data是Object类型,格式是{ 'yyyy-MM-dd' : value } * 若是type是custom。那data是Array类型,格式是[[x, y, value], ..., [x, y, value]] */
data: {},
rect: {
stroke: {
show: false,
background: '#333', // 正方形的边框颜色
opacity: 0.6 // 正方形的边框透明度
},
colourMatching: '', //配色方案,有custom-自定义和reen、pink、blue、orange、gray五种渐变色
backgroundArr: [] // 自定义配色方案,程度由重到轻
},
dateStart: '',
dateEnd: '',
min: 0, // 分级最低值,总共五个等级,不传默认值是0
max: 0, // 分级最高值,总共五个等级,不传默认值是
tip: { // 顶部鼠标悬浮小气泡
show: true, // 是否展现
/** 文本内容 * type为date的时候{a}表示日期,{b}表示数值 * type为custom的时候表示{x}x轴对应的值,{y}y轴对应的值,{b}表示数值 * 若是在替换字符串前加反斜杠(例如/{b}),则不会替换该字符串 */
formatter: ''
}
}
// 初始化参数,没传的就使用默认值
this.setOption = (obj) => {
this.option = matchObj(this.option, obj)
if (!this.option.max) {
this.option.max = Object.values(obj.data).sort(function(a, b) {
return b - a
})[0] || 0
}
}
this.init = (dom) => {
// 初始化dom的样式
dom.setAttribute('style', 'width:100%;height:100%;position:relative;')
// 获取父级dom的宽度
var parentWidth = dom.offsetWidth
// 经过createElementNS建立svg元素并设置属性
var svg = document.createElementNS('http://www.w3.org/2000/svg','svg')
svg.setAttribute('version', '1.1')
svg.setAttribute('class', 'svg-container')
dom.appendChild(svg) // 挂载元素。SVG元素添加到页面内显示
// 显示顶部提示小气泡
if (this.option.tip.show) {
// 建立tip容器并设置属性
var tip = document.createElement('div')
tip.setAttribute('class', 'svg-tip svg-tip-one-line')
var title = document.createElement('strong')
tip.appendChild(title) // 挂载到父节点上
dom.appendChild(tip) // 挂载元素。挂载顶部提示气泡
}
// 建立svg的group元素并设置属性
var group = document.createElementNS('http://www.w3.org/2000/svg', 'g')
var translateX = 20 // 横轴方向的偏移值
var translateY = 40 // 纵轴方向的偏移值
group.setAttribute('transform', 'translate(' + translateX + ',' + translateY + ')')
svg.appendChild(group) // 挂载到父节点上
var maxYLabelFontSize = 12
var labelPadding = 10
var maxStrLength = 0
if (this.option.type === 'date') {
// 获取所有天数列表
var dateList = (new Date()).getDateList(this.option.dateStart, this.option.dateEnd)
var columnCount = Math.ceil(Object.keys(dateList).length / 7)
var size = Math.floor((parentWidth - translateX / 2 - maxYLabelFontSize * 2 - labelPadding - this.option.gap * columnCount) / columnCount)
var section = size + this.option.gap
// 纵轴的label值,这里是星期值
for (var w = 0; w < 7; w++) {
//建立矩形元素并设置属性
var yText = document.createElementNS('http://www.w3.org/2000/svg', 'text')
yText.style.fontSize = section * 0.7 > maxYLabelFontSize ? maxYLabelFontSize : section * 0.7 // 字体大小响应,最大是12px
yText.setAttribute('dx', -labelPadding)
yText.setAttribute('dy', w * section + size / 2 + 4)
yText.setAttribute('class', 'wday')
yText.innerHTML = weekMap[w]
group.appendChild(yText)
if (maxStrLength < yText.getBBox().width) {
maxStrLength = yText.getBBox().width
}
}
var index = 0 // 天数列表索引
var column = 0 // 分组索引,完整的一周为一组
for (var dateKey in dateList) {
// 完整的一周为一组
if (index === 0 || index % 7 === 0) {
// 建立svg的group元素并设置属性,一周为一组
var xGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g')
// 设置偏移值
xGroup.setAttribute('transform', 'translate(' + (column * section + maxStrLength) + ', 0)')
group.appendChild(xGroup) // 挂载到父节点
column++ // 递增组数索引
// 判断在哪一个分组的上方增长横轴的label值,在这里是月份
if (index > 0) {
var startMonth = dateKey.split('-')[1]
var preStartMonth = Object.keys(dateList)[index - 7].split('-')[1]
if (Math.abs(Number(startMonth) - Number(preStartMonth)) > 0) {
//建立text元素并设置属性
var fontSize = section * 0.8 > maxYLabelFontSize ? maxYLabelFontSize : section * 0.8 // 字体大小响应,最大是14px
var xText = document.createElementNS('http://www.w3.org/2000/svg', 'text')
xText.style.fontSize = fontSize
xText.setAttribute('x', column * section)
xText.setAttribute('y', -labelPadding)
xText.setAttribute('class', 'month')
xText.innerHTML = monthMap[Number(startMonth) - 1]
group.appendChild(xText)
}
}
}
if(this.option.data.hasOwnProperty(dateKey)) {
// 开始画正方形啦~建立矩形元素并设置属性
var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('x', 0)
rect.setAttribute('y', dateList[dateKey] * section)
rect.setAttribute('id', dateKey) // 设置日期为id值
rect.setAttribute('week', dateList[dateKey]) // 设置星期几属性
rect.setAttribute('column', column) // 设置分组的组索引属性
rect.setAttribute('width', size)
rect.setAttribute('height', size)
color = '#fff' // 默认颜色是白色,就是啥也没有时候的颜色,以你的背景色为准
var colorSelect = ''
// 选择的颜色系列
if (this.option.rect.colourMatching === 'custom') {
colorSelect = this.option.rect.backgroundArr
} else {
colorSelect = colorMap[this.option.rect.colourMatching]
}
// 默认绿色为基本配色方案
colorSelect = colorSelect ? colorSelect : colorMap['green']
// 分为五个等级,小于最小值最低等级,大于最大值最高等级。中间还应该有三个等级(可自定义)
var levelGap = (this.option.max - this.option.min) / (colorSelect.length - 2)
if (this.option.data.hasOwnProperty(dateKey)) {
var differ = this.option.max - this.option.data[dateKey]
// 小正方形的颜色决定于他的值处在哪一个level里头
switch(true) {
case (this.option.data[dateKey] >= this.option.max) :
color = colorSelect[0]
break
case (differ < levelGap) :
color = colorSelect[1]
break
case (differ < levelGap * 2) :
color = colorSelect[2]
break
case (differ < levelGap * 3) :
color = colorSelect[3]
break
default:
color = colorSelect[4]
}
// 设置小矩形的颜色
rect.setAttribute('style', 'fill:' + color)
// 显示顶部提示小气泡
if (this.option.tip.show) {
//矩形元素绑定鼠标事件实现动态效果
// 鼠标移入
rect.onmouseover = (e) => {
if (this.option.rect.stroke.show) {
e.srcElement.setAttribute('stroke-width', 1)
e.srcElement.setAttribute('stroke', this.option.rect.stroke.background)
e.srcElement.setAttribute('stroke-opacity', this.option.rect.stroke.opacity)
}
// 提示小气泡的文本内容,默认==>日期:值
if (this.option.tip.formatter) {
var tipText = this.option.tip.formatter.replace(/(?<!\/){a}/g, e.target.id).replace(/(?<!\/){b}/g, this.option.data[e.target.id])
tip.innerHTML = tipText
} else {
tip.innerHTML = e.target.id + ':' + this.option.data[e.target.id]
}
tip.style.display = 'block'
tip.style.top = (e.target.attributes.week.value * section + dom.querySelector('.svg-tip').offsetHeight / 2 - translateY / 2 - labelPadding) + 'px'
tip.style.left = ((e.target.attributes.column.value - 1) * section + size / 2 + translateX / 2 + maxStrLength + labelPadding - dom.querySelector('.svg-tip').offsetWidth / 2) + 'px'
}
// 鼠标移出
rect.onmouseout = (e) => {
if (this.option.rect.stroke.show) {
e.srcElement.setAttribute('stroke-width', 0)
}
tip.style.display = 'none'
}
}
}
xGroup.appendChild(rect) //挂载矩形元素添加到小分组元素内
}
index++
}
// 设置svg元素的宽高
svg.style.width = Math.ceil(section * columnCount + translateX + maxStrLength + labelPadding)
svg.style.height = section * 7 + translateY
}
if (this.option.type === 'custom') {
var size = parseInt(parentWidth / this.option.xAxis.length) - this.option.gap * this.option.xAxis.length
var section = size + this.option.gap
var fontSize = section * 0.7 > maxYLabelFontSize ? maxYLabelFontSize : section * 0.7 // 字体大小响应,最大是12px
this.option.xAxis.forEach((xItem,xIndex) => {
var xGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g')
group.appendChild(xGroup)
this.option.yAxis.forEach((yItem,yIndex) => {
if (xIndex === 0) {
//建立矩形元素并设置属性
var yText = document.createElementNS('http://www.w3.org/2000/svg', 'text')
yText.style.fontSize = fontSize
yText.setAttribute('dx', -labelPadding)
yText.setAttribute('dy', yIndex * section + size / 2 + 4)
yText.setAttribute('class', 'wday')
yText.innerHTML = yItem
group.appendChild(yText)
if (maxStrLength < yText.getBBox().width) {
maxStrLength = yText.getBBox().width
}
}
this.option.data.some((elem, i) => {
if (elem[0] === xIndex && elem[1] === yIndex) {
color = 'rgb(255, 255, 255)' // 默认颜色是白色,就是啥也没有时候的颜色,以你的背景色为准
var colorSelect = ''
// 选择的颜色系列
if (this.option.rect.colourMatching === 'custom') {
colorSelect = this.option.rect.backgroundArr
} else {
colorSelect = colorMap[this.option.rect.colourMatching]
}
// 默认绿色为基本配色方案
colorSelect = colorSelect ? colorSelect : colorMap['green']
// 分为五个等级,小于最小值最低等级,大于最大值最高等级。中间还应该有三个等级(可自定义)
var levelGap = (this.option.max - this.option.min) / (colorSelect.length - 2)
var differ = this.option.max - elem[2]
// 小正方形的颜色决定于他的值处在哪一个level里头
switch(true) {
case (elem[2] >= this.option.max) :
color = colorSelect[0]
break
case (differ < levelGap) :
color = colorSelect[1]
break
case (differ < levelGap * 2) :
color = colorSelect[2]
break
case (differ < levelGap * 3) :
color = colorSelect[3]
break
default:
color = colorSelect[4]
}
//建立矩形元素并设置属性
var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('x', 0)
rect.setAttribute('y', elem[1] * section)
rect.setAttribute('id', elem[0] + '_' + elem[1]) // 设置日期为id值
rect.setAttribute('row', yIndex)
rect.setAttribute('column', xIndex)
rect.setAttribute('width', size)
rect.setAttribute('height', size)
rect.setAttribute('style', 'fill:' + color)
// 显示顶部提示小气泡
if (this.option.tip.show) {
//矩形元素绑定鼠标事件实现动态效果
// 鼠标移入
rect.onmouseover = (e) => {
if (this.option.rect.stroke.show) {
e.srcElement.setAttribute('stroke-width', 1)
e.srcElement.setAttribute('stroke', this.option.rect.stroke.background)
e.srcElement.setAttribute('stroke-opacity', this.option.rect.stroke.opacity)
}
var xyArr = e.target.id.split('_')
// 提示小气泡的文本内容,默认==>x_y:值
if (this.option.tip.formatter) {
var tipText = this.option.tip.formatter.replace(/(?<!\/){x}/g, this.option.xAxis[xyArr[0]]).replace(/(?<!\/){y}/g, this.option.yAxis[xyArr[1]]).replace(/(?<!\/){b}/g, elem[2])
tip.innerHTML = tipText
} else {
tip.innerHTML = e.target.id + ':' + elem[2]
}
tip.style.display = 'block'
tip.style.top = (e.target.attributes.row.value * section + dom.querySelector('.svg-tip').offsetHeight / 2 - translateY / 2 - labelPadding) + 'px'
tip.style.left = (e.target.attributes.column.value * section + size / 2 + translateX / 2 + maxStrLength + labelPadding - dom.querySelector('.svg-tip').offsetWidth / 2) + 'px'
}
// 鼠标移出
rect.onmouseout = (e) => {
if (this.option.rect.stroke.show) {
e.srcElement.setAttribute('stroke-width', 0)
}
tip.style.display = 'none'
}
}
//将矩形元素添加到SVG元素内
xGroup.appendChild(rect)
return true
} else {
return false
}
})
})
xGroup.setAttribute('transform', 'translate(' + (xIndex * section + maxStrLength) + ', 0)')
//建立text元素并设置属性
var xText = document.createElementNS('http://www.w3.org/2000/svg', 'text')
xText.style.fontSize = fontSize
xText.setAttribute('x', xIndex * section + maxStrLength)
xText.setAttribute('y', -labelPadding)
xText.setAttribute('class', 'month')
xText.innerHTML = xItem
group.appendChild(xText)
xText.setAttribute('textLength', size < xText.getBBox().width ? size : xText.getBBox().width)
})
// 设置svg元素的宽高
svg.style.width = section * this.option.xAxis.length + translateX / 2 + labelPadding + maxStrLength
svg.style.height = section * this.option.yAxis.length + translateY
}
}
}
复制代码