关于flutter:千相千面图形语法

46次阅读

共计 5527 个字符,预计需要花费 14 分钟才能阅读完成。

在 Flutter 可视化库 Graphic 的新版本中,优化了申明式定义的语法,使其更好的体现图形语法的实质。

本文通过 Graphic 的图形语法定义变换,一步步将柱状图演变为饼图,展现图形语法的灵便丰盛。同时也让初学者理解图形语法基本概念。

如果你从未接触过图形语法,不影响本文的浏览。本文能够看作 Graphic 的入门教程。

柱状图和饼图都是数据可视化中常见的类型,它们乍一看迥异,但在图形语法中,却有着雷同的实质,这是为什么?让咱们从柱状图一步步变换成饼图,来理解其中的原因。

首先从最常见的柱状图开始说起。数据采纳和 ECharts 的入门示例 一样:

const data = [{'category': 'Shirts', 'sales': 5},
  {'category': 'Cardigans', 'sales': 20},
  {'category': 'Chiffons', 'sales': 36},
  {'category': 'Pants', 'sales': 10},
  {'category': 'Heels', 'sales': 10},
  {'category': 'Socks', 'sales': 20},
];

申明式定义

Graphic 采纳申明式定义,所有的可视化语法都在图表组件 Chart 的构造函数中体现:

Chart(
  data: data,
  variables: {
    'category': Variable(accessor: (Map map) => map['category'] as String,
    ),
    'sales': Variable(accessor: (Map map) => map['sales'] as num,
    ),
  },
  elements: [IntervalElement()],
  axes: [
    Defaults.horizontalAxis,
    Defaults.verticalAxis,
  ],
)

数据与变量

图表的数据通过 data 字段引入,能够是任意类型的数组。在图表的外部,这些数据项将被转换成规范的 Tuple 类型。数据项如何转换为 Tuple 中的字段值则由变量(Variable)定义。

从代码能够看出,定义的语法是很简短的,但 variables 却占据了一半篇幅。Dart 是一种类型严格的语言,为了能容许任意类型输出数据,具体的 Variable 定义是必不可少的。

几何元素

图形语法最重要的特点是辨别了形象的数据图(graph)和具体的图形(graphic)。

比方,数据形容的是一段区间(interval)还是一个独自的点(point),这称之为 graph;而在图上是体现为长条还是三角,多高多宽,这称之为 graphic。生成 graph 和 graphic 的环节别离被称之为几何(geometry)和具象(aesthetic)。

Graph 和 graphic 的概念,触达了数据与图形之间的实质关系,是图形语法跳出了传统图表分类解放的要害。

而承载这两者定义称为几何元素(GeomElement)。它的类型决定了 graph,分为:

  • PointElement:点
  • LineElement:点连成的线
  • AreaElement:线之间的区域
  • IntervalElement:两点之间的区间
  • PolygonElement:宰割立体的多边形

柱状图的柱高,体现的是 0 到数据值这段区间,因而选用 IntervalElement。这样,咱们就失去了最常见的 柱状图

回到结尾的问题,饼图的张角也是表白一个区间,该当也属于 IntervalElement,但为什么柱状图是条形,饼图是扇面?

坐标系

坐标系将不同的变量调配到立体上不同的维度中。对于直角坐标系(RectCoord),维度别离是程度和垂直,对于极坐标系(PolarCoord),维度则别离是角度和半径。

目前示例中没有指明 coord 字段,所以坐标系是默认的直角坐标系。既然饼图是通过张角表白区间,那该当应用极坐标系。咱们增加一行定义指定应用极坐标系:

coord: PolarCoord()

则图形变为 玫瑰图

仿佛开始靠近饼图了。不过这个“一键切换”失去的图形还很不欠缺,须要一些解决。

度量

第一个问题是,扇面半径的比例,仿佛和 sales 数据的比例不一样。

解决这个问题,就波及到图形语法中的一个重要概念:度量(Scale)。

原始数据的值可能是数值、字符串、工夫。即便同为数值,尺度也可能相差好几个数量级。因而图表应用它们前,须要将其标准化,这个过程就称之为度量。

对于连续型的数据,比方数值、工夫,要将它们归一化到 [0, 1] 上;对于离散型的数据,比方字符串,要将它们映射到 0, 1, 2, 3… 这样的自然数索引。

每个变量都有一个对应的度量,在 Variable 的 scale 字段中设置。Tuple 中的变量值可能是数值(num)、工夫(DateTime)、字符串(String)三者之一,因而度量依据解决的原始数据类型,分为:

  • LinearScale:将区间数值线性归一到 [0, 1] 上,连续型
  • TimeScale:将区间工夫线性归一成 [0, 1] 上的数值,连续型
  • OrdinalScale:按程序将字符串映射成自然数索引,连续型

对于数值,默认的 LinearScale 会依据图表的数据范畴确定区间,因而最小值不肯定是 0。这对于柱状图来说,能让图形很好的聚焦高度差,但对于玫瑰图就不太适合了,因为人们偏向于认为半径反映的是比例关系。

因而,须要手动设置 LinearScale 区间的最小值为 0。

'sales': Variable(accessor: (Map map) => map['sales'] as num,
  scale: LinearScale(min: 0),
),

具象属性

第二个问题是,不同的扇面挨在一起,须要色彩辨别一下,而且玫瑰图中人们更习惯用标签而不是坐标轴进行标注。

相似色彩、标签等,人们用来感知图形的,称之为具象属性(aesthetic attribute)。Graphic 中有如下具象属性类型:

  • position:地位
  • shape:具体形态
  • color:色彩
  • gradient:渐变色,可代替color
  • elevation:暗影高度
  • label:标签
  • size:尺寸

position 外,每种具象属性在 GeomElement 中通过对应的 Attr 类进行定义。通过定义字段的不同,分为以下几种形式:

  • 间接通过 value 指定属性值。
  • 通过 variablevaluesstops 指定关联的变量,以及指标属性值,变量值依据类型的不同将被插值或索引映射为属性值。这种属性称为通道属性(ChannelAttr)。
  • 通过 encoder 间接定义数据项映射属性值的办法。

在示例中,咱们别离通过 colorlabel 为每个扇面配置不同的色彩和标签:

elements: [IntervalElement(
  color: ColorAttr(
    variable: 'category',
    values: Defaults.colors10,
  ),
  label: LabelAttr(encoder: (tuple) => Label(tuple['category'].toString(),),
  ),
)]

这样,就失去了一个较为欠缺的玫瑰图:

如何从玫瑰图变为饼图?

坐标系转置

数据的不同变量之间,往往是函数关系:y = f(x),咱们称函数定义域所在的维度为定义域维度(domain dimension),罕用 x 示意;称函数值域所在的维度为值域维度(measure dimension),罕用 y 示意。习惯上对于立体,直角坐标系定义域维度对应程度方向,值域维度对应垂直方向;极坐标系定义域维度对应角度,值域维度对应半径。

玫瑰图用半径示意值,而饼图用角度示意值,因而两者互相转换,第一步是要将坐标系中维度与立体的对应关系调换一下,这称为坐标系转置(transpose):

coord: PolarCoord(transposed: true)

则图形变为 竞速图

仿佛更靠近饼图了。

变量转换

在饼图中,所有扇面加起来刚好形成一个圆周,每个扇面所占的弧长是这个数据项在总和中的占比。而上图中所有弧段拼接起来,显然超过了一个圆周。

一种方法是,咱们将 sales 的度量的区间设置为 0 至所有 sales 值之和,那样恰好每个 sales 值通过度量之后就是它在总和中的占比。但对于动静的数据,咱们在定义图表时往往并不知道理论数据是多少。

还有一种方法是,如果值域变量就是每个 sales 值在总和中的占比,那只有定义这个变量度量的原始区间为[0, 1] 就能够了。

这时能够用到变量转换(VariableTransform),它能对现有的变量数据进行统计转换,批改变量数据或生成新的变量。这里应用 Proportion,它算出每个 sales 在总和中的占比,生成新的 percent 变量,并为这个变量设置原始区间的 [0, 1] 的度量:

transforms: [
  Proportion(
    variable: 'sales',
    as: 'percent',
  ),
]

图形代数

在设置完变量转换后,咱们遇到了一个新的问题。原来 Tuple 中只有 categorysales 两个变量,它们恰好能够调配给定义域和值域两个维度,不言自明。但当初多出了个 percent 变量,三个栗子如何分给两个猴子,那就必须要指定分明了。

定义变量与维度的关系,须要用到图形代数(graphic algebra)。

图形代数通过一个表达式,用运算符连贯变量汇合 Varset,来定义变量之间的关系,以及它们如何调配给各维度。图形代数有三种运算符:

  • *:称为 cross,将两边的变量按程序调配给不同的维度。
  • +:称为 blend,将两边的变量按程序调配给同一个维度。
  • /:称为 nest,按左边的变量对所有数据进行分组

咱们须要将 category 和转换得来的 percent 变量别离调配给定义域和值域两个维度,得益于 Dart 的类运算符重载,Graphic 通过 Varset 类实现所有图形代数运算,因而图形代数通过 position 定义如下:

position: Varset('category') * Varset('percent')

这样设置完变量转换和图形代数后,图形变为:

分组与调整

每个弧段的长度处理完毕了,接着就是要“拼接”它们了。拼接的第一步,是在角度上将它们地位调整到首尾相连。

这种地位调整,通过 Modifier 进行定义。调整针对的对象不是单个的数据项,所以咱们要先将所有的数据依照 category 进行分组,对于示例的数据,这样分组后每个数据项就是一组。分组通过图形代数中的 nest 运算符定义。而后咱们设置“重叠调整”(StackModifier):

elements: [IntervalElement(
  ...
  position: Varset('category') * Varset('percent') / Varset('category'),
  modifiers: [StackModifier()],
)]

因为后面曾经使得弧长总和是一个圆周,因而重叠后在角度上就达到了首尾相连的成果,算得上是 夕阳图

坐标维度

就差最初一步了:每个弧段的角度曾经就位了,只有让他们都撑满整个半径范畴,整体上就造成一个饼了。

咱们察看半径维度,刚刚通过图形代数,将 category 这个变量调配给了它,因而每个弧段按程序落在了不同的“赛道”中。但事实上咱们心愿半径地位不要有辨别,只有角度这一个维度起作用。换言之,咱们心愿这个极坐标系,是只有角度的一维坐标系。

咱们只有指定坐标系的维度数量为 1,同时代数表达式中移除 category

coord: PolarCoord(
  transposed: true,
  dimCount: 1,
)
...
position: Varset('percent') / Varset('category')

这样各个弧段就无差别的撑满整个半径范畴,饼图绘制实现:


饼图残缺的定义如下:

Chart(
  data: data,
  variables: {
    'category': Variable(accessor: (Map map) => map['category'] as String,
    ),
    'sales': Variable(accessor: (Map map) => map['sales'] as num,
      scale: LinearScale(min: 0),
    ),
  },
  transforms: [
    Proportion(
      variable: 'sales',
      as: 'percent',
    ),
  ],
  elements: [IntervalElement(position: Varset('percent') / Varset('category'),
    groupBy: 'category',
    modifiers: [StackModifier()],
    color: ColorAttr(
      variable: 'category',
      values: Defaults.colors10,
    ),
    label: LabelAttr(encoder: (tuple) => Label(tuple['category'].toString(),
        LabelStyle(Defaults.runeStyle),
      ),
    ),
  )],
  coord: PolarCoord(
    transposed: true,
    dimCount: 1,
  ),
)

在这个过程中,咱们通过扭转坐标、度量、具象属性、变量转换、图形代数、调整等图形语法定义,使得图形一直变换,失去了传统图表分类中的柱状图、玫瑰图、竞速图、夕阳图、饼图。

能够看出,图形语法的定义,跳出了传统图表类型的解放,能够排列组合出更多的可视化图形,具备更好的灵活性和扩展性。更重要的是,它揭示了不同可视化图形实质的分割和区别,为数据可视化迷信的倒退提供了实践根底。

正文完
 0