需求:子弹图
实例
虽然 AntV 的 G2 有实现示例,但是因为项目上一直暂时只使用了 echarts 一个库,所以依然选择使用 echarts 参考 G2 的子弹图示例实现子弹图,示例如下:
option = {
title: {
text: "子弹图示例"
},
yAxis: [{
type: 'category',
data: ['利润'],
axisLine: {
show: false
},
axisTick: {
show: false
}
}, {
type: 'category',
data: [''],
axisLine: {
show: false
},
axisTick: {
show: false
}
}],
xAxis: {
type: 'value',
axisLine: {
show: false
},
axisTick: {
show: false
}
},
tooltip: {
formatter: '{a}: {c}'
},
legend: {
data: ['差', '良', '优', '实际值', {
name: '目标值',
icon: 'path://M0 0M443.733333 0 h145.066667 v1024 H443.733333z'
}],
selectedMode: false
},
grid: {
containLabel: true,
width: "99%",
height: 120,
left: 0,
top: 50
},
series: [{
name: "差",
data: [60],
type: 'bar',
yAxisIndex: 0,
stack: "range",
silent: true,
barWidth: 90,
color: "#F5B4AE"
}, {
name: "良",
data: [30],
type: 'bar',
yAxisIndex: 0,
stack: "range",
silent: true,
barWidth: 90,
color: "#FADCA9"
}, {
name: "优",
data: [10],
type: 'bar',
yAxisIndex: 0,
stack: "range",
silent: true,
barWidth: 90,
color: "#BFE9C3"
}, {
name: "实际值",
data: [75],
type: 'bar',
yAxisIndex: 1,
barWidth: 60,
color: "#434778",
z: 3,
// markLine: {
// // silent:true,
// animation: false,
// symbol: "",
// label: {
// show: false
// },
// lineStyle: {
// normal: {
// width: 8,
// type: "solid",
// color: "#000000"
// },
// emphasis: {
// width: 8,
// type: "solid",
// color: "#000000"
// }
// },
// data: [{
// xAxis: 85,
// tooltip: {
// formatter: '目标值: {c}'
// }
// }]
// }
}, {
name: "目标值",
type: "scatter",
symbol: "rect",
symbolSize: [8, 110],
data: [85],
color: "#000000",
hoverAnimation:false,
z: 4
}]
};
setInterval(function() {
option.series[3].data[0] = Math.round(Math.random() * 100);
myChart.setOption(option, true);
}, 2000);
主要思路
- 是使用堆叠的柱子当作背景,不需要鼠标的交互效果,将
silent设为true。 - 表示指标数值的柱子在背景柱上方,且宽度较小。
- 表示目标值的竖线利用散点表示(原本也考虑过利用
markLine,但因为还要把目标值在图例中展示出来,就只能通过增加一项type为scatter的series实现),将symbol设为rect,通过设置symbolSize即可画出长方形。
关于柱子重叠
对于官方提供的配置项属性 barGap 描述如下:
series[i]-bar.barGap | string [ default: 30% ] 不同系列的柱间距离,为百分比(如 '30%',表示柱子宽度的 30%)。 如果想要两个系列的柱子重叠,可以设置 barGap 为 '-100%'。这在用柱子做背景的时候有用。 在同一坐标系上,此属性会被多个 'bar' 系列共享。此属性应设置于此坐标系中最后一个 'bar' 系列上才会生效,并且是对此坐标系中所有 'bar' 系列生效。
但在使用中发现若在期望重叠的柱宽不同的情况下,设置 barGap 为 -100%,并没有像我们期望的那样展示,轴标签文字并不是总是相对柱子中轴线居中。
所以采用了另一种实现方案,通过创建两个y轴,并将柱子分别放在不同的y轴,来避免使用 barGap 遇到不居中的问题。
barWidth不同时设置barGap实现重叠,柱子没有相对轴标签居中 #11665
关于图例
对于官方提供的配置项属性 icon 描述如下:
legend.data[i].icon | string 图例项的 icon。 ECharts 提供的标记类型包括 'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none' 可以通过 'image://url' 设置为图片,其中 URL 为图片的链接,或者 dataURI。 URL 为图片链接例如:
'image://http://xxx.xxx.xxx/a/b.png'URL 为 dataURI 例如:'image://data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7'可以通过 'path://' 将图标设置为任意的矢量路径。这种方式相比于使用图片的方式,不用担心因为缩放而产生锯齿或模糊,而且可以设置为任意颜色。路径图形会自适应调整为合适的大小。路径的格式参见 SVG PathData。可以从 Adobe Illustrator 等工具编辑导出。 例如:'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'
可自定义图例 icon 可以满足我将目标值的图例 icon 自定义为一条竖线的需求,于是找到 ue 帮我画了一条竖线,ue 发给我的 SVG 格式如下:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" t="1574837703218" class="icon" viewBox="0 0 1024 1024" version="1.1" p-id="8173" width="128" height="128"><defs><style type="text/css"/></defs><path d="M443.733333 0h145.066667v1024H443.733333z" fill="#526688" p-id="8174"/></svg>
我从其中拿出 path 的 d 属性,设置为 icon。
发现设置生效了,但是却明显看得出是靠左的,没有居中。
由于对 SVG 的用法不是特别了解,于是去提了 issues 询问。 🤦♀️
感激大佬回复,告诉我将 M443.733333 0h145.066667v1024H443.733333z 改为 M0 0M443.733333 0 h145.066667 v1024 H443.733333z。
竟然就真的成功了,只是要先 move 到 0, 0 点就可以居中了,这点让我觉得很神奇。
于是去瞄了一眼源码,果然找到了 ECharts 针对居中的特殊处理。
//incubator-echarts/src/component/legend/LegendView.js
export default echarts.extendComponentView({
...,
_createItem: function (
name, dataIndex, itemModel, legendModel,
legendSymbolType, symbolType,
itemAlign, color, borderColor, selectMode
) {
...
var itemIcon = itemModel.get('icon');
var tooltipModel = itemModel.getModel('tooltip');
var legendGlobalTooltipModel = tooltipModel.parentModel;
// Use user given icon first
legendSymbolType = itemIcon || legendSymbolType;
var legendSymbol = createSymbol(
legendSymbolType,
0,
0,
itemWidth,
itemHeight,
isSelected ? color : inactiveColor,
// symbolKeepAspect default true for legend
symbolKeepAspect == null ? true : symbolKeepAspect
);
itemGroup.add(
setSymbolStyle(
legendSymbol, legendSymbolType, legendModelItemStyle,
borderColor, inactiveBorderColor, isSelected
)
);
...
},
...
});
我们可以通过上面的代码看到使用 createSymbol 根据自定义 icon 或者 icon 类型绘制图例。
//incubator-echarts/src/util/symbol.js
/**
* Create a symbol element with given symbol configuration: shape, x, y, width, height, color
* @param {string} symbolType
* @param {number} x
* @param {number} y
* @param {number} w
* @param {number} h
* @param {string} color
* @param {boolean} [keepAspect=false] whether to keep the ratio of w/h,
* for path and image only.
*/
export function createSymbol(symbolType, x, y, w, h, color, keepAspect) {
// TODO Support image object, DynamicImage.
var isEmpty = symbolType.indexOf('empty') === 0;
if (isEmpty) {
symbolType = symbolType.substr(5, 1).toLowerCase() + symbolType.substr(6);
}
var symbolPath;
if (symbolType.indexOf('image://') === 0) {
symbolPath = graphic.makeImage(
symbolType.slice(8),
new BoundingRect(x, y, w, h),
keepAspect ? 'center' : 'cover'
);
}
else if (symbolType.indexOf('path://') === 0) {
symbolPath = graphic.makePath(
symbolType.slice(7),
{},
new BoundingRect(x, y, w, h),
keepAspect ? 'center' : 'cover'
);
}
else {
symbolPath = new SymbolClz({
shape: {
symbolType: symbolType,
x: x,
y: y,
width: w,
height: h
}
});
}
symbolPath.__isEmptyBrush = isEmpty;
symbolPath.setColor = symbolPathSetColor;
symbolPath.setColor(color);
return symbolPath;
}
可以通过上面的代码看到 针对 SVG PathData 的 格式 使用 graphic.makePath 进行绘制。
/**
* Create a path element from path data string
* @param {string} pathData
* @param {Object} opts
* @param {module:zrender/core/BoundingRect} rect
* @param {string} [layout=cover] 'center' or 'cover'
*/
export function makePath(pathData, opts, rect, layout) {
var path = pathTool.createFromString(pathData, opts);
if (rect) {
if (layout === 'center') {
rect = centerGraphic(rect, path.getBoundingRect());
}
resizePath(path, rect);
}
return path;
}
/**
* Get position of centered element in bounding box.
*
* @param {Object} rect element local bounding box
* @param {Object} boundingRect constraint bounding box
* @return {Object} element position containing x, y, width, and height
*/
function centerGraphic(rect, boundingRect) {
// Set rect to center, keep width / height ratio.
var aspect = boundingRect.width / boundingRect.height;
var width = rect.height * aspect;
var height;
if (width <= rect.width) {
height = rect.height;
}
else {
width = rect.width;
height = width / aspect;
}
var cx = rect.x + rect.width / 2;
var cy = rect.y + rect.height / 2;
return {
x: cx - width / 2,
y: cy - height / 2,
width: width,
height: height
};
}
看到这里终于centerGraphic 函数找到针对居中的处理逻辑。 🎉