数据可视化:在 React 项目中使用 Vega 图表 (二)

待我称王封你为后i 2023-06-11 08:27 15阅读 0赞

效果图
上一篇讲了如何在 React 项目中用 Vega-Lite 绘制基本的 area chart 图表。

本篇将介绍如何绘制多层图表,如何添加图例。

多层图表

通过上一篇文章,我们知道了可以通过 mark, encoding 等来描述我们想要的图表。要实现多层图表,只需要把多个包含上述属性的图表对象放进 layer 数组中就可以。就像栈一样, 从栈顶压入,后压入的(index 大的)图层在上层。

我们在之前的数据中加入用户评论数量 “user_comments”:

  1. "data": {
  2. "values": [
  3. { "user_comments": 0, "active_users": 0, "date": "2019-10-01" },
  4. { "user_comments": 3, "active_users": 2, "date": "2019-10-02" },
  5. { "user_comments": 1, "active_users": 0, "date": "2019-10-03" },
  6. { "user_comments": 1, "active_users": 1, "date": "2019-10-04" },
  7. { "user_comments": 2, "active_users": 0, "date": "2019-10-05" },
  8. { "user_comments": 1, "active_users": 0, "date": "2019-10-06" },
  9. { "user_comments": 2, "active_users": 1, "date": "2019-10-07" }
  10. ]
  11. },

按照与上篇文章案例相同的 Vega-Lite 语法,写一个描述 user_comments 的单层图表。
其实只需要替换部分 y 轴的信息即可。

  1. {
  2. "mark": {"type": "area", "color": "#e0e0e0", "interpolate": "monotone"},
  3. "encoding": {
  4. "x":{
  5. "field": "date",
  6. "type": "ordinal",
  7. "timeUnit": "yearmonthdate",
  8. "axis": {"title": "Date", "labelAngle": -45}
  9. },
  10. "y": {
  11. "field": "user_comments",
  12. "type": "quantitative",
  13. "axis": {
  14. "title": "User Comments",
  15. "format": "d",
  16. "values": [1,2,3]
  17. }
  18. }
  19. }
  20. }

user comments

接下来,创建 layer 数组。把上述对象放入数组中,图表没有任何变化,此时仍然是单层图表。

  1. ...
  2. "layer":[
  3. {
  4. "mark": {"type": "area", "color": "#e0e0e0", "interpolate": "monotone"},
  5. "encoding": {
  6. "x":{
  7. "field": "date",
  8. "type": "ordinal",
  9. "timeUnit": "yearmonthdate",
  10. "axis": {"title": "Date", "labelAngle": -45}
  11. },
  12. "y": {
  13. "field": "user_comments",
  14. "type": "quantitative",
  15. "axis": {
  16. "title": "User Comments",
  17. "format": "d",
  18. "values": [1,2,3]
  19. }
  20. }
  21. }
  22. }
  23. ],
  24. ...

把上一篇中 Active Users 的对象加入数组,列在 User Comments 之后:

  1. "layer":[
  2. {
  3. "mark": {"type": "area", "color": "#e0e0e0", "interpolate": "monotone"},
  4. "encoding": {
  5. "x":{
  6. "field": "date",
  7. "type": "ordinal",
  8. "timeUnit": "yearmonthdate",
  9. "axis": {"title": "Date", "labelAngle": -45}
  10. },
  11. "y": {
  12. "field": "user_comments",
  13. "type": "quantitative",
  14. "axis": {
  15. "title": "User Comments",
  16. "format": "d",
  17. "values": [1,2,3]
  18. }
  19. }
  20. }
  21. },
  22. {
  23. "mark": {"type": "area", "color": "#0084FF", "interpolate": "monotone"},
  24. "encoding": {
  25. "x": {
  26. "field": "date",
  27. "type": "ordinal",
  28. "timeUnit": "yearmonthdate",
  29. "axis": {"title": "Date", "labelAngle": -45}
  30. },
  31. "y": {
  32. "field": "active_users",
  33. "type": "quantitative",
  34. "axis": {
  35. "title": "Active Users",
  36. "format": "d",
  37. "values": [1,2]
  38. }
  39. }
  40. }
  41. }
  42. ],

当当~ 多层图表出现了。

多层图表

增加图例

与之前的图表相比,横轴没什么变化,竖轴的位置显示了两层图表的 title。但这样表意不够清晰,用户不能一眼看明白哪个颜色代表哪个数据。所以我们需要引进图例(legend)。

创建图例的方式并不唯一,我通过 stroke 创建图例,用 legend 来优化它的样式。

在任一图层中加入 stroke

  1. ...
  2. {
  3. "mark": {"type": "area", "color": "#e0e0e0", "interpolate": "monotone"},
  4. "encoding": {
  5. "x":{
  6. "field": "date",
  7. "type": "ordinal",
  8. "timeUnit": "yearmonthdate",
  9. "axis": {"title": "Date", "labelAngle": -45}
  10. },
  11. "y": {
  12. "field": "user_comments",
  13. "type": "quantitative",
  14. "axis": {
  15. "title": "User Comments",
  16. "format": "d",
  17. "values": [1,2,3]
  18. }
  19. },
  20. "stroke": {
  21. "field": "symbol",
  22. "type": "ordinal",
  23. "scale": {
  24. "domain": ["User Comments", "Active Users"],
  25. "range": ["#e0e0e0", "#0084FF"]
  26. }
  27. }
  28. }
  29. },
  30. ...

图中出现了丑丑的图例:

丑图例

化妆师 legend 登场,赶紧打扮一下。在顶层的 config 中添加 legend 对象:

  1. ...
  2. "legend": {
  3. "offset": -106, // 调节图例整体水平移动距离
  4. "title": null,
  5. "padding": 5,
  6. "strokeColor": "#9e9e9e",
  7. "strokeWidth": 2,
  8. "symbolType": "stroke",
  9. "symbolOffset": 0,
  10. "symbolStrokeWidth": 10,
  11. "labelOffset": 0,
  12. "cornerRadius": 10,
  13. "symbolSize": 100,
  14. "clipHeight": 20
  15. }

现在顺眼多啦!
其实现在不要竖轴的 title 都可以,将 y.axis 对象的 title 删除或置空即可,效果如文章首图。

漂亮图例

当图层多的时候,也可以搭配使用 area chart 和 line chart,效果也不错,只需要把该图层的 mark.type 改为 line 即可。

示意图:
多层图

在 React 项目中使用

  1. import React from 'react';
  2. import { Vega } from 'react-vega';
  3. // chart config
  4. const jobpalBlue = '#e0e0e0';
  5. const jobpalLightGrey = '#0084FF';
  6. const jobpalDarkGrey = '#9e9e9e';
  7. const areaMark = {
  8. type: 'area',
  9. color: jobpalBlue,
  10. interpolate: 'monotone',
  11. };
  12. const getDateXObj = rangeLen => ({
  13. field: 'date',
  14. type: `${rangeLen > 30 ? 'temporal' : 'ordinal'}`,
  15. timeUnit: 'yearmonthdate',
  16. axis: {
  17. title: 'Date',
  18. labelAngle: -45,
  19. },
  20. });
  21. const getQuantitativeYObj = (field, title, values) => ({
  22. field,
  23. type: 'quantitative',
  24. axis: {
  25. title,
  26. format: 'd',
  27. values,
  28. },
  29. });
  30. const legendConfig = {
  31. title: null,
  32. offset: -106,
  33. padding: 5,
  34. strokeColor: jobpalDarkGrey,
  35. strokeWidth: 2,
  36. symbolType: 'stroke',
  37. symbolOffset: 0,
  38. symbolStrokeWidth: 10,
  39. labelOffset: 0,
  40. cornerRadius: 10,
  41. symbolSize: 100,
  42. clipHeight: 20,
  43. };
  44. const getSpec = (yAxisValues = [], rangeLen = 0) => ({
  45. $schema: 'https://vega.github.io/schema/vega-lite/v4.json',
  46. title: 'Demo Chart',
  47. layer: [
  48. {
  49. mark: {
  50. ...areaMark,
  51. color: jobpalLightGrey,
  52. },
  53. encoding: {
  54. x: getDateXObj(rangeLen),
  55. y: getQuantitativeYObj('user_comments', '', yAxisValues),
  56. stroke: {
  57. field: 'symbol',
  58. type: 'ordinal',
  59. scale: {
  60. domain: ['User Comments', 'Active Users'],
  61. range: [jobpalLightGrey, jobpalBlue],
  62. },
  63. },
  64. },
  65. }, {
  66. mark: areaMark,
  67. encoding: {
  68. x: getDateXObj(rangeLen),
  69. y: getQuantitativeYObj('active_users', '', yAxisValues),
  70. },
  71. },
  72. ],
  73. config: {
  74. legend: legendConfig,
  75. },
  76. })
  77. const data = [
  78. { "user_comments": 0, "active_users": 0, "date": "2019-10-01" },
  79. { "user_comments": 3, "active_users": 2, "date": "2019-10-02" },
  80. { "user_comments": 1, "active_users": 0, "date": "2019-10-03" },
  81. { "user_comments": 1, "active_users": 1, "date": "2019-10-04" },
  82. { "user_comments": 2, "active_users": 0, "date": "2019-10-05" },
  83. { "user_comments": 1, "active_users": 0, "date": "2019-10-06" },
  84. { "user_comments": 2, "active_users": 1, "date": "2019-10-07" }
  85. ]
  86. const App = () => {
  87. // get max value from data arary
  88. const yAxisMaxValueFor = (...keys) => {
  89. const maxList = keys.map(key => data.reduce(
  90. // find the item containing the max value
  91. (acc, cur) => (cur[key] > acc[key] ? cur : acc)
  92. )[key]
  93. );
  94. return Math.max(...maxList);
  95. };
  96. const yAxisValues = Array.from(
  97. { length: yAxisMaxValueFor('active_users', 'user_comments') },
  98. ).map((v, i) => (i + 1));
  99. const spec = getSpec(yAxisValues, data.length);
  100. return (
  101. <div className="App">
  102. <Vega
  103. spec={
  104. {
  105. ...spec,
  106. autosize: 'fit',
  107. resize: true,
  108. contains: 'padding',
  109. width: 400,
  110. height: 300,
  111. data: { values: data },
  112. }}
  113. actions={
  114. {
  115. export: true,
  116. source: false,
  117. compiled: false,
  118. editor: false,
  119. }}
  120. downloadFileName={'Just Name It'}
  121. />
  122. </div>
  123. );
  124. }
  125. export default App;

resize

在实际项目中,我们必须保证图表大小能跟随窗口大小变化。接下来,我们来实现这个功能。

图表在绘制完成后不会重新绘制,但我们可以通过 React 组件接管宽高值来实现重新绘制。

即:

  • state 中管理 widthheight
  • 通过 setState 刷新来实现图表的重绘
  • 在生命周期方法中设置事件监听函数来监听 resize 事件
  • 结合 css 和 ref, 通过图表外的 warper 层得到此时图表正确的宽高值

示例代码如下:

  1. import React from 'react';
  2. import { Vega } from 'react-vega';
  3. // chart config
  4. const jobpalBlue = '#e0e0e0';
  5. const jobpalLightGrey = '#0084FF';
  6. const jobpalDarkGrey = '#9e9e9e';
  7. const areaMark = {
  8. type: 'area',
  9. color: jobpalBlue,
  10. interpolate: 'monotone',
  11. };
  12. const getDateXObj = rangeLen => ({
  13. field: 'date',
  14. type: `${rangeLen > 30 ? 'temporal' : 'ordinal'}`,
  15. timeUnit: 'yearmonthdate',
  16. axis: {
  17. title: 'Date',
  18. labelAngle: -45,
  19. },
  20. });
  21. const getQuantitativeYObj = (field, title, values) => ({
  22. field,
  23. type: 'quantitative',
  24. axis: {
  25. title,
  26. format: 'd',
  27. values,
  28. },
  29. });
  30. const legendConfig = {
  31. title: null,
  32. offset: -106,
  33. padding: 5,
  34. strokeColor: jobpalDarkGrey,
  35. strokeWidth: 2,
  36. symbolType: 'stroke',
  37. symbolOffset: 0,
  38. symbolStrokeWidth: 10,
  39. labelOffset: 0,
  40. cornerRadius: 10,
  41. symbolSize: 100,
  42. clipHeight: 20,
  43. };
  44. const getSpec = (yAxisValues = [], rangeLen = 0) => ({
  45. $schema: 'https://vega.github.io/schema/vega-lite/v4.json',
  46. title: 'Demo Chart',
  47. layer: [
  48. {
  49. mark: {
  50. ...areaMark,
  51. color: jobpalLightGrey,
  52. },
  53. encoding: {
  54. x: getDateXObj(rangeLen),
  55. y: getQuantitativeYObj('user_comments', '', yAxisValues),
  56. stroke: {
  57. field: 'symbol',
  58. type: 'ordinal',
  59. scale: {
  60. domain: ['User Comments', 'Active Users'],
  61. range: [jobpalLightGrey, jobpalBlue],
  62. },
  63. },
  64. },
  65. }, {
  66. mark: areaMark,
  67. encoding: {
  68. x: getDateXObj(rangeLen),
  69. y: getQuantitativeYObj('active_users', '', yAxisValues),
  70. },
  71. },
  72. ],
  73. config: {
  74. legend: legendConfig,
  75. },
  76. })
  77. const data = [
  78. { "user_comments": 0, "active_users": 0, "date": "2019-10-01" },
  79. { "user_comments": 3, "active_users": 2, "date": "2019-10-02" },
  80. { "user_comments": 1, "active_users": 0, "date": "2019-10-03" },
  81. { "user_comments": 1, "active_users": 1, "date": "2019-10-04" },
  82. { "user_comments": 2, "active_users": 0, "date": "2019-10-05" },
  83. { "user_comments": 1, "active_users": 0, "date": "2019-10-06" },
  84. { "user_comments": 2, "active_users": 1, "date": "2019-10-07" }
  85. ];
  86. // get max value from data arary
  87. const yAxisMaxValueFor = (...keys) => {
  88. const maxList = keys.map(key => data.reduce(
  89. // find the item containing the max value
  90. (acc, cur) => (cur[key] > acc[key] ? cur : acc)
  91. )[key]
  92. );
  93. return Math.max(...maxList);
  94. };
  95. const { addEventListener, removeEventListener } = window;
  96. class App extends React.Component {
  97. state = {
  98. width: 400,
  99. height: 300,
  100. }
  101. componentDidMount() {
  102. addEventListener('resize', this.resizeListener, { passive: true, capture: false });
  103. }
  104. componentWillUnmount() {
  105. removeEventListener('resize', this.resizeListener, { passive: true, capture: false });
  106. }
  107. resizeListener = () => {
  108. if (!this.chartWrapper) return;
  109. const child = this.chartWrapper.querySelector('div');
  110. child.style.display = 'none';
  111. const {
  112. clientWidth,
  113. clientHeight: height,
  114. } = this.chartWrapper;
  115. const width = clientWidth - 40; // as padding: "0 20px"
  116. this.setState({ width, height });
  117. child.style.display = 'block';
  118. }
  119. refChartWrapper = el => {
  120. this.chartWrapper = el
  121. if (el) this.resizeListener();
  122. }
  123. yAxisValues = Array.from(
  124. { length: yAxisMaxValueFor('active_users', 'user_comments') },
  125. ).map((v, i) => (i + 1));
  126. render() {
  127. const {width, height, yAxisValues} = this.state;
  128. const spec = getSpec(yAxisValues, data.length);
  129. return (
  130. <div
  131. ref={this.refChartWrapper}
  132. style={
  133. { margin: '10vh 10vw', width: '80vw', height: '50vh' }}
  134. >
  135. <Vega
  136. spec={
  137. {
  138. ...spec,
  139. autosize: 'fit',
  140. resize: true,
  141. contains: 'padding',
  142. width,
  143. height,
  144. data: { values: data },
  145. }}
  146. actions={
  147. {
  148. export: true,
  149. source: false,
  150. compiled: false,
  151. editor: false,
  152. }}
  153. downloadFileName={'Just Name It'}
  154. />
  155. </div>
  156. );
  157. }
  158. }
  159. export default App;

动图演示:
gif demo

至此,图表已经基本完善。

发表评论

表情:
评论列表 (有 0 条评论,15人围观)

还没有评论,来说两句吧...

相关阅读