手绘风格的数据可视化

数据可视化可以通过将数据图形化的方式帮助更加直观的理解和分析统计数据。在近些年,经常看到一些公司所出的用户报告或是blog中采用手绘风格的数据可视化使得其更加内容和风格都亲切十足。本文罗列了一些常用的数据可视化工具及适用图表。

工具 rough + draw.io matplotlib.pyplot.xkcd chart.xkcd & cutecharts instad.io
适用范围 适用于已有的draw.io图表,svg图表或需直接在画布上画图的图表 适用于数据可视化图表,尤其是通过matplotlib或者seaborn所生成的图表。可内嵌到jupyter lab/notebook中 适用于有交互需求的数据可视化图表。可内嵌到网页或jupyter lab/notebook中 适用于已有的svg或者spreadsheet的图表。可将其直接转化为手绘风
适用图表 适用于任意图表,尤其适合直接做图的图表如流程图,类图或时间轴图等 适用于大多数数据可视化图表,如线图,柱图,饼图,等高图等 仅支持'bar', 'line', 'pie', 'radar', 'scatter' 适用于任意图表,只要求输入格式为DOM的SVG或PDF图表

画图式

rough + draw.io

rough是一个非常强大的手绘风格基础工具,可以实现基本的绘画元素在Canvas和SVG上的手绘风格的实现。rough是一个非常小的js库(~9KB),该库定义了用于绘制直线、曲线、弧线、多边形、圆和椭圆的基本单元。我们可以通过在js端定义一些基本单元来实现Canvas上手绘风格的图片,其简单示例如下所示。

rough渲染代码
<script src="https://unpkg.com/roughjs@latest/bundled/rough.js"></script>
<canvas id="canvas" width="800" height="600"></canvas>
<script>
const rc = rough.canvas(document.getElementById("canvas"));

//line and rectangle
rc.rectangle(10, 10, 100, 100, {
fill: "red",
stroke: "blue",
hachureAngle: 60,
hachureGap: 10,
fillWeight: 5,
strokeWidth: 5,
});

//overlapping circles
rc.circle(200, 60, 80, {
stroke: "red",
strokeWidth: 4,
fill: "rgba(0,255,0,1)",
fillWeight: 4,
hachureGap: 6,
});
rc.circle(250, 60, 80, {
stroke: "blue",
strokeWidth: 4,
fill: "rgba(255,255,0,1)",
fillWeight: 4,
hachureGap: 6,
});

// arcs
rc.arc(450, 100, 200, 180, Math.PI, Math.PI * 1.6, true);
rc.arc(450, 100, 200, 180, 0, Math.PI / 2, true, {
stroke: "red",
strokeWidth: 4,
fill: "rgba(255,255,0,0.4)",
fillStyle: "solid",
});
rc.arc(450, 100, 200, 180, Math.PI / 2, Math.PI, true, {
stroke: "blue",
strokeWidth: 2,
fill: "rgba(255,0,255,0.4)",
});

// draw sine curve
let points = [];
for (let i = 0; i < 20; i++) {
// 4pi - 400px
let x = (400 / 20) * i + 10;
let xdeg = (Math.PI / 100) * x;
let y = Math.round(Math.sin(xdeg) * 90) + 250;
points.push([x, y]);
}
rc.curve(points, {
roughness: 1.2,
stroke: "red",
strokeWidth: 3,
});
</script>

正因为其小巧易用,rough被嵌入到很多软件以实现手绘风格,比如笔者用的比较多的draw.io。draw.io是一款免费的在线图表编辑器,其小巧且功能丰富,且支持即时存储及多人在线编辑,非常适合平时学习和工作使用。draw.io在2020年左右通过rough支持了手绘风格作图,并有相关的官方博客介绍了这一功能。简单而言,我们首先需要在右侧的整体图表风格处选择手绘风格(sketch),如果需要对每个元素的手绘风格微调,则需要通过每个元素的填充方法(fill)和线条(line)选择对应的,甚至于通过绘图单元的属性(property)来更改手绘风格(sketch style)。如需要文字,字体最好改为Comic Sans MS。其简单流程如下图所示。

通过draw.io可以将绝大多数日常所使用的图表风格化为手绘风,如流程图等。

matplotlib.pyplot.xkcd

matplotlib是python中最常用的数据可视化库,matplotlib自身也支持手绘风格。matplotlib通过其matplotlib.pyplot.xkcd api来加载和控制手绘风格,在使用时只需要将在原始matplotlib绘图放在with matplotlib.pyplot.xkcd()中即可将图像转换为手绘风。该方法也适用于基于matplotlib的seaborn数据可视化库。一个例子如下图所示,图一代表原始matplotlib所绘制的饼图,图二代表将原始matplotlib绘图放在with matplotlib.pyplot.xkcd()中将饼图转换为手绘风格的。更多示例可以参考

matplotlib.原始渲染代码
import matplotlib.pyplot as plt
plt.figure(dpi=150)
patches, texts, autotexts = plt.pie(
x=[1, 2, 3], #返回三个对象
labels=['A', 'B', 'C'],
colors=['#dc2624', '#2b4750', '#45a0a2'],
autopct='%.2f%%',
explode=(0.1, 0, 0))
texts[1].set_size('20') #修改B的大小

#matplotlib.patches.Wedge
patches[0].set_alpha(0.3) #A组分设置透明度
patches[2].set_hatch('|') #C组分添加网格线
patches[1].set_hatch('x')

plt.legend(
patches,
['A', 'B', 'C'], #添加图例
title="Pie Learning",
loc="center left",
fontsize=15,
bbox_to_anchor=(1, 0, 0.5, 1))

plt.title('Lovely pie', size=20)
plt.show()
matplotlib.xkcd渲染代码
import matplotlib.pyplot as plt
with plt.xkcd(
scale=4, #相对于不使用xkcd的风格图,褶皱的幅度
length=120, #褶皱长度
randomness=2): #褶皱的随机性
plt.figure(dpi=150)
patches, texts, autotexts = plt.pie(
x=[1, 2, 3], #返回三个对象
labels=['A', 'B', 'C'],
colors=['#dc2624', '#2b4750', '#45a0a2'],
autopct='%.2f%%',
explode=(0.1, 0, 0))
texts[1].set_size('20') #修改B的大小

#matplotlib.patches.Wedge
patches[0].set_alpha(0.3) #A组分设置透明度
patches[2].set_hatch('|') #C组分添加网格线
patches[1].set_hatch('x')

plt.legend(
patches,
['A', 'B', 'C'], #添加图例
title="Pie Learning",
loc="center left",
fontsize=15,
bbox_to_anchor=(1, 0, 0.5, 1))

plt.title('Lovely pie', size=20)
plt.show()

当使用matplotlib或seaborn做数据可视化时,这种方法可以简单方便的将图案转化成手绘风格。

交互式:chart.xkcd & cutecharts

当谈论到手绘风格的数据可视化,chart.xkcdcutecharts可能是较为熟知的两个工具了。

chart.xkcd是一个用于交互式手绘风格的数据可视化JavaScript库,而cutecharts是将chart.xkcd封装到python。

chart.xkcd的手绘风格效果如下图所示。

chart.xkcd渲染代码
<script src="https://cdn.jsdelivr.net/npm/chart.xkcd@1.1/dist/chart.xkcd.min.js"></script>

<svg class="line-chart" style="text-align: center; width: 100%;"></svg>
<script>
// querySelector() 方法返回文档中匹配指定 CSS 选择器的一个元素。获取文档中 class=".line-chart" 的元素。
const svg = document.querySelector('.line-chart');

// chartXkcd.Line 创建一个折线图
const lineChart = new chartXkcd.Line(svg, {
//图表的标题
title: 'Monthly income of an indie developer',
// 图表的 x 标签
xLabel: 'Month',
// 图表的 y 标签
yLabel: '$ Dollors',
// 需要可视化的数据
data: {
// x 轴数据
labels: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'],
// y 轴数据
datasets: [{
// 第一组数据
label: 'Plan',
data: [30, 70, 200, 300, 500, 800, 1500, 2900, 5000, 8000],
}, {
// 第二组数据
label: 'Reality',
data: [0, 1, 30, 70, 80, 100, 50, 80, 40, 150],
}],
},
// 可选配置以自定义图表的外观
options: {
// 自定义要在 y 轴上看到的刻度号(默认为 3)
yTickCount: 3,
// 指定要放置图例的位置
legendPosition: chartXkcd.config.positionType.upLeft
}
});
</script>

cutecharts的api非常接近pyechart,示例如下图所示。

cutecharts渲染代码
from cutecharts.charts import Bar

def bar_base() -> Bar:
chart = Bar("MVP of LOL Bar")
chart.set_options(
labels=['Faker', 'Easyhoon', 'Pawn'],
x_label='LOLers',
y_label='MVPs')
chart.add_series('MVP', [3, 2, 1])
return chart

bar_base().render_notebook()
# bar_base().render()

类似于pyechart,使用bar_base().render()则会将图表生成为html文件,即上图所渲染的图案,生成的代码如下。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/chart.xkcd@1.1/dist/chart.xkcd.min.js"></script>
</head>
<body>
<div id="68967376df93415eb5377f0bb8ef5c60" class="chart-container" style="width: 800px">
<svg id="chart_68967376df93415eb5377f0bb8ef5c60"></svg>
</div>
<script>
const svg_68967376df93415eb5377f0bb8ef5c60 = document.querySelector('#chart_68967376df93415eb5377f0bb8ef5c60')
const chart_68967376df93415eb5377f0bb8ef5c60 = new chartXkcd.Bar(svg_68967376df93415eb5377f0bb8ef5c60, {"title": "MVP of LOL Bar", "data": {"datasets": [{"label": "MVP", "data": [3, 2, 1]}], "labels": ["Faker", "Easyhoon", "Pawn"]}, "xLabel": "LOLers", "yLabel": "MVPs", "options": {"yTickCount": 3}});
</script>
</body>
</html>

比较遗憾的是chart.xkcd和cutecharts,支持图形类别有限,仅支持'bar', 'line', 'pie', 'radar', 'scatter'几类图。其api也不支持很多功能,比如辅助线或者额外的标注等。并且开发者已于2019年左右停止开发,所以在可见的未来也很难像plotly,echart或pyechart一样支持多种多类的图表和功能。

与chart.xkcd和cutecharts类似的还有基于rough和d3的rough-chartsroughViz的JavaScript库,以及基于roughViz封装的python库py-roughviz也实现了手绘风格数据可视化。但其功能于chart.xkcd和cutecharts类似,api相对繁复,以及或多或少都停止了开发,在此也不赘述了。具体可参看官方文档和示例。

转换式:instad.io

除了上述几类以外,我们还可以将已有的svg(可通过pyechart或matplotlib等库或draw.io,PS,AI等软件生成)或者spreadsheet的图表通过instad.io转化为手绘风。其效果如下图所示。具体的示例参考官网

instad.io的原理非常简单,从一个给定的根DOM元素开始,找到所有的SVG对象,然后递归寻找所有的子元素,读出子元素的基本属性,利用roughjs创建一个手绘风格的元素拷贝,隐藏原始元素。这样手绘风格的SVG元素就取代了原始的图形。当需要回到初始状态的时候,只要重现所有隐藏的原始元素,移除后加入的手绘元素即可。