前端数据可视化3——svg实现柱状图

549 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第23天,点击查看活动详情

我正在参加 码上掘金体验活动,详情:show出你的创意代码块

前言

上篇文章利用canvas实现了一个简单的柱状图,本文我们来写一版svg的。

具体实现

首先,我们基于代码的bug率来判断一个人写的代码质量。来回顾一下数据格式。

数据格式

const data = [
    {name: 'zhangsan', one: 1, two: 1, three: 0, four: 2, lines: 900},
    {name: 'lisi', one: 0, two: 0, three: 1, four: 1, lines: 300},
    {name: 'wangwu', one: 1, two: 0, three: 6, four: 0, lines: 1000}
]

对数据进行处理

然后就是对数据的简单处理,变成一堆我们需要的数组,和canvas步骤相同,话不多说,直接上代码。

// 条形图的宽度
const chartWidth = 480;
// 条形图的高度
const chartHeight = 300;
// 条形图的外边距
const margin = 15;

// 容器的宽度
const containerWidth = chartWidth + margin * 2;
// 容器的高度
const containerHeight = chartHeight + margin * 2;

/* 
    布局
*/

// 将需要编码的属性对应的值提取出来
const names = Array.from(data, item => item.name);
const values = Array.from(data, item => {
    const value = item.one * 5 + item.two * 3 + item.three * 1 + item.four * 0.1
    const result = Math.floor(value*1000/item.lines)
    return result
});

const indices = Array.from(data, (item,index) => index);

// 计算每一个条左下顶点的横坐标,位置和在数组中的index有关
const step = chartWidth / names.length;
const barWidth = step * 0.8;
const xs = Array.from(indices, i => i * step);

// 计算每一个条左下顶点的纵坐标,因为所有条底部都是对齐的,所以就是图表的高度
const y = chartHeight;

/* 
    映射
*/

// 获得每一个条的高度,条的高度应该和 value 线性相关
const vmax = Math.max(...values);
const barHeights = Array.from(values, item => chartHeight * (item / vmax));

//获得每一个条的颜色
const nameColor  = {
    'zhangsan': "#5b8ff9",
    'lisi': "#61ddaa",
    'wangwu': "#65789b"
};
const colors = Array.from(names, item => nameColor[item])

svg

接下来就是svg渲染了。回顾一下svg,里面有g元素,rect元素,text元素等。所以我们需要一个函数帮助我们创建这些元素。

function createSVGElement(type) {
    return document.createElementNS("http://www.w3.org/2000/svg", type);
}

接下来就是渲染画布了,我们需要一个g元素来帮助我们处理柱状图。

const svg = document.getElementById("container-svg");

// 设置 svg 的坐标原点和大小
svg.setAttribute("width", containerWidth);
svg.setAttribute("height", containerHeight);
svg.setAttribute("viewBox", [0, 0, containerWidth, containerHeight]);

// 创建一个 g 元素用于平移
const g = createSVGElement("g");
g.setAttribute("transform", `translate(${margin}, ${margin})`);
svg.appendChild(g)

接下来就是将一条一条的柱子放进去。

for(const index of indices){
    // 取得对应的属性
    const color = colors[index]
    const x = xs[index];
    const barHeight = barHeights[index];
    const value = values[index];
    const xname = names[index]

    // 绘制条
    const rect = createSVGElement("rect");
    rect.setAttribute("x", x);
    rect.setAttribute("y", y - barHeight);
    rect.setAttribute("fill", color);
    rect.setAttribute("width", barWidth);
    rect.setAttribute("height", barHeight);
    g.appendChild(rect)

    // 绘制值
    const text = createSVGElement("text");
    text.textContent = value
    text.setAttribute("text-anchor", "middle");
    text.setAttribute("fill", "white");
    text.setAttribute("font-familyr", "PingFangSC-Regular, sans-serif");
    text.setAttribute("font-size", 25);
    text.setAttribute("alignment-baseline", "middle");
    text.setAttribute("x", x + barWidth / 2);
    text.setAttribute("y", y - barHeight / 2);
    g.appendChild(text)

    const xtext = createSVGElement("text");
    xtext.textContent = xname
    xtext.setAttribute("text-anchor", "middle");
    xtext.setAttribute("fill", "black");
    xtext.setAttribute("font-familyr", "PingFangSC-Regular, sans-serif");
    xtext.setAttribute("font-size", 25);
    xtext.setAttribute("alignment-baseline", "middle");
    xtext.setAttribute("x", x + barWidth / 2);
    xtext.setAttribute("y", y - 20);
    g.appendChild(xtext)
}

来看下最终效果吧。嗯,和canvas渲染一致。

image.png

完整代码如下:

总结

本次实现了柱状图的svg版本,和canvas比起来,处理数据步骤都相同,对于canvas来说,不用频繁创建dom节点。写起来比较省事,对于svg来说,不必担心图像迷糊问题,写起来比较放心。