ECharts实践——子弹图

5,729 阅读5分钟

需求:子弹图

实例

虽然 AntVG2 有实现示例,但是因为项目上一直暂时只使用了 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);

使用echarts实现子弹图实例代码

主要思路

  • 是使用堆叠的柱子当作背景,不需要鼠标的交互效果,将 silent 设为 true
  • 表示指标数值的柱子在背景柱上方,且宽度较小。
  • 表示目标值的竖线利用散点表示(原本也考虑过利用 markLine,但因为还要把目标值在图例中展示出来,就只能通过增加一项 typescatterseries 实现),将 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>

我从其中拿出 pathd 属性,设置为 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 函数找到针对居中的处理逻辑。 🎉

图例自定义icon时能否自动或可设置居中 #11734