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

2023-11-14

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

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

多层图表

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

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

"data": {
    "values": [
    { "user_comments": 0, "active_users": 0, "date": "2019-10-01" },
    { "user_comments": 3, "active_users": 2, "date": "2019-10-02" },
    { "user_comments": 1, "active_users": 0, "date": "2019-10-03" },
    { "user_comments": 1, "active_users": 1, "date": "2019-10-04" },
    { "user_comments": 2, "active_users": 0, "date": "2019-10-05" },
    { "user_comments": 1, "active_users": 0, "date": "2019-10-06" },
    { "user_comments": 2, "active_users": 1, "date": "2019-10-07" }
    ]
  },

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

{
      "mark": {"type": "area", "color": "#e0e0e0", "interpolate": "monotone"},
      "encoding": {
        "x":{
           "field": "date",
           "type": "ordinal",
           "timeUnit": "yearmonthdate",
           "axis": {"title": "Date", "labelAngle": -45}
        },
         "y": {
            "field": "user_comments",
	        "type": "quantitative",
            "axis": {
                "title": "User Comments",
                "format": "d",
                "values": [1,2,3]
            }
        }
      }
  }

user comments

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

...
"layer":[
    {
      "mark": {"type": "area", "color": "#e0e0e0", "interpolate": "monotone"},
      "encoding": {
        "x":{
           "field": "date",
           "type": "ordinal",
           "timeUnit": "yearmonthdate",
           "axis": {"title": "Date", "labelAngle": -45}
        },
        "y": {
            "field": "user_comments",
            "type": "quantitative",
            "axis": {
                "title": "User Comments",
                "format": "d",
                "values": [1,2,3]
             }
          }
      }
    }
  ],
  ...

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

"layer":[
    {
      "mark": {"type": "area", "color": "#e0e0e0", "interpolate": "monotone"},
      "encoding": {
        "x":{
           "field": "date",
           "type": "ordinal",
           "timeUnit": "yearmonthdate",
           "axis": {"title": "Date", "labelAngle": -45}
        },
        "y": {
            "field": "user_comments",
            "type": "quantitative",
            "axis": {
                "title": "User Comments",
                "format": "d",
                "values": [1,2,3]
             }
          }
      }
    },
    {
      "mark": {"type": "area", "color": "#0084FF", "interpolate": "monotone"},
      "encoding": {
        "x": {
          "field": "date",
          "type": "ordinal",
          "timeUnit": "yearmonthdate",
          "axis": {"title": "Date", "labelAngle": -45}
        },
        "y": {
          "field": "active_users",
          "type": "quantitative",
          "axis": {
          "title": "Active Users",
          "format": "d",
          "values": [1,2]
          }
        }
      }
    }
  ],

当当~ 多层图表出现了。

多层图表

增加图例

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

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

在任一图层中加入 stroke

...
{
      "mark": {"type": "area", "color": "#e0e0e0", "interpolate": "monotone"},
      "encoding": {
        "x":{
           "field": "date",
           "type": "ordinal",
           "timeUnit": "yearmonthdate",
           "axis": {"title": "Date", "labelAngle": -45}
        },
        "y": {
            "field": "user_comments",
            "type": "quantitative",
            "axis": {
                "title": "User Comments",
                "format": "d",
                "values": [1,2,3]
             }
          },
        "stroke": {
          "field": "symbol",
          "type": "ordinal",
          "scale": {
            "domain": ["User Comments", "Active Users"],
            "range": ["#e0e0e0", "#0084FF"]
          }
        }
      }
    },
    ...

图中出现了丑丑的图例:

丑图例

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

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

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

漂亮图例

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

示意图:
多层图

在 React 项目中使用

import React from 'react';
import { Vega } from 'react-vega';

// chart config
const jobpalBlue = '#e0e0e0';
const jobpalLightGrey = '#0084FF';
const jobpalDarkGrey = '#9e9e9e';

const areaMark = {
  type: 'area',
  color: jobpalBlue,
  interpolate: 'monotone',
};

const getDateXObj = rangeLen => ({
  field: 'date',
  type: `${rangeLen > 30 ? 'temporal' : 'ordinal'}`,
  timeUnit: 'yearmonthdate',
  axis: {
    title: 'Date',
    labelAngle: -45,
  },
});

const getQuantitativeYObj = (field, title, values) => ({
  field,
  type: 'quantitative',
  axis: {
    title,
    format: 'd',
    values,
  },
});

const legendConfig = {
  title: null,
  offset: -106,
  padding: 5,
  strokeColor: jobpalDarkGrey,
  strokeWidth: 2,
  symbolType: 'stroke',
  symbolOffset: 0,
  symbolStrokeWidth: 10,
  labelOffset: 0,
  cornerRadius: 10,
  symbolSize: 100,
  clipHeight: 20,
};

const getSpec = (yAxisValues = [], rangeLen = 0) => ({
  $schema: 'https://vega.github.io/schema/vega-lite/v4.json',
  title: 'Demo Chart',
  layer: [
    {
      mark: {
        ...areaMark,
        color: jobpalLightGrey,
      },
      encoding: {
        x: getDateXObj(rangeLen),
        y: getQuantitativeYObj('user_comments', '', yAxisValues),
        stroke: {
          field: 'symbol',
          type: 'ordinal',
          scale: {
            domain: ['User Comments', 'Active Users'],
            range: [jobpalLightGrey, jobpalBlue],
          },
        },
      },
    }, {
      mark: areaMark,
      encoding: {
        x: getDateXObj(rangeLen),
        y: getQuantitativeYObj('active_users', '', yAxisValues),
      },
    },
  ],
  config: {
    legend: legendConfig,
  },
})

const data = [
    { "user_comments": 0, "active_users": 0, "date": "2019-10-01" },
    { "user_comments": 3, "active_users": 2, "date": "2019-10-02" },
    { "user_comments": 1, "active_users": 0, "date": "2019-10-03" },
    { "user_comments": 1, "active_users": 1, "date": "2019-10-04" },
    { "user_comments": 2, "active_users": 0, "date": "2019-10-05" },
    { "user_comments": 1, "active_users": 0, "date": "2019-10-06" },
    { "user_comments": 2, "active_users": 1, "date": "2019-10-07" }
  ]

const App = () => {
  // get max value from data arary
  const yAxisMaxValueFor = (...keys) => {
    const maxList = keys.map(key => data.reduce(
         // find the item containing the max value
        (acc, cur) => (cur[key] > acc[key] ? cur : acc)
      )[key]
    );
    return Math.max(...maxList);
  };

  const yAxisValues = Array.from(
    { length: yAxisMaxValueFor('active_users', 'user_comments') },
  ).map((v, i) => (i + 1));


  const spec = getSpec(yAxisValues, data.length);

  return (
    <div className="App">
      <Vega
        spec={{
          ...spec,
          autosize: 'fit',
          resize: true,
          contains: 'padding',
          width: 400,
          height: 300,
          data: { values: data },
        }}
        actions={{
          export: true,
          source: false,
          compiled: false,
          editor: false,
        }}
        downloadFileName={'Just Name It'}
      />
    </div>
  );
}

export default App;

resize

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

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

即:

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

示例代码如下:

import React from 'react';
import { Vega } from 'react-vega';

// chart config
const jobpalBlue = '#e0e0e0';
const jobpalLightGrey = '#0084FF';
const jobpalDarkGrey = '#9e9e9e';

const areaMark = {
  type: 'area',
  color: jobpalBlue,
  interpolate: 'monotone',
};

const getDateXObj = rangeLen => ({
  field: 'date',
  type: `${rangeLen > 30 ? 'temporal' : 'ordinal'}`,
  timeUnit: 'yearmonthdate',
  axis: {
    title: 'Date',
    labelAngle: -45,
  },
});

const getQuantitativeYObj = (field, title, values) => ({
  field,
  type: 'quantitative',
  axis: {
    title,
    format: 'd',
    values,
  },
});

const legendConfig = {
  title: null,
  offset: -106,
  padding: 5,
  strokeColor: jobpalDarkGrey,
  strokeWidth: 2,
  symbolType: 'stroke',
  symbolOffset: 0,
  symbolStrokeWidth: 10,
  labelOffset: 0,
  cornerRadius: 10,
  symbolSize: 100,
  clipHeight: 20,
};

const getSpec = (yAxisValues = [], rangeLen = 0) => ({
  $schema: 'https://vega.github.io/schema/vega-lite/v4.json',
  title: 'Demo Chart',
  layer: [
    {
      mark: {
        ...areaMark,
        color: jobpalLightGrey,
      },
      encoding: {
        x: getDateXObj(rangeLen),
        y: getQuantitativeYObj('user_comments', '', yAxisValues),
        stroke: {
          field: 'symbol',
          type: 'ordinal',
          scale: {
            domain: ['User Comments', 'Active Users'],
            range: [jobpalLightGrey, jobpalBlue],
          },
        },
      },
    }, {
      mark: areaMark,
      encoding: {
        x: getDateXObj(rangeLen),
        y: getQuantitativeYObj('active_users', '', yAxisValues),
      },
    },
  ],
  config: {
    legend: legendConfig,
  },
})

const data = [
    { "user_comments": 0, "active_users": 0, "date": "2019-10-01" },
    { "user_comments": 3, "active_users": 2, "date": "2019-10-02" },
    { "user_comments": 1, "active_users": 0, "date": "2019-10-03" },
    { "user_comments": 1, "active_users": 1, "date": "2019-10-04" },
    { "user_comments": 2, "active_users": 0, "date": "2019-10-05" },
    { "user_comments": 1, "active_users": 0, "date": "2019-10-06" },
    { "user_comments": 2, "active_users": 1, "date": "2019-10-07" }
  ];

// get max value from data arary
const yAxisMaxValueFor = (...keys) => {
  const maxList = keys.map(key => data.reduce(
    // find the item containing the max value
    (acc, cur) => (cur[key] > acc[key] ? cur : acc)
  )[key]
  );
  return Math.max(...maxList);
};

const { addEventListener, removeEventListener } = window;

class App extends React.Component {

  state = {
    width: 400,
    height: 300,
  }

  componentDidMount() {
    addEventListener('resize', this.resizeListener, { passive: true, capture: false });
  }

  componentWillUnmount() {
    removeEventListener('resize', this.resizeListener, { passive: true, capture: false });
  }

  resizeListener = () => {
    if (!this.chartWrapper) return;

    const child = this.chartWrapper.querySelector('div');
    child.style.display = 'none';

    const {
      clientWidth,
      clientHeight: height,
    } = this.chartWrapper;
    const width = clientWidth - 40; // as padding: "0 20px"
    this.setState({ width, height });

    child.style.display = 'block';
  }

  refChartWrapper = el => {
    this.chartWrapper = el
    if (el) this.resizeListener();
  }

  yAxisValues = Array.from(
    { length: yAxisMaxValueFor('active_users', 'user_comments') },
  ).map((v, i) => (i + 1));

  render() {
    const {width, height, yAxisValues} = this.state;

    const spec = getSpec(yAxisValues, data.length);

    return (
      <div
        ref={this.refChartWrapper}
        style={{ margin: '10vh 10vw', width: '80vw', height: '50vh' }}
        >
        <Vega
          spec={{
            ...spec,
            autosize: 'fit',
            resize: true,
            contains: 'padding',
            width,
            height,
            data: { values: data },
          }}
          actions={{
            export: true,
            source: false,
            compiled: false,
            editor: false,
          }}
          downloadFileName={'Just Name It'}
        />
      </div>
    );
  }
}

export default App;

动图演示:
gif demo

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

数据可视化:在 React 项目中使用 Vega 图表 (二) 的相关文章

  • 如何在 C# Windows 应用程序中引用网站用户控件?

    我在 Windows C 应用程序中有一个解决方案 其中我也有一个网站 在网站中我有 1 个用户控件 现在我希望在同一解决方案的其他 C 项目中使用用户控件 怎么做 那是不可能的 您必须为 Windows 应用程序重新创建它 您可以重用的是
  • Web 服务器内置 Azure 应用服务

    背景 我正在托管一个静态 Web 应用程序 由 NUXTJS 在 Azure 应用服务上生成 现在我想通过以下方式配置更多内容web config比如为多个域设置 CORS 设置 但我找不到任何文档来确定 Azure 应用程序服务中集成 安
  • Django 中的 Meta 到底是什么?

    我想简单地知道 Django 中的 Meta 类是什么以及它们的作用 from django db import models Class Author models Model first name models CharField ma
  • 在 Foundation 中动态设置 Sass 变量

    如何在 Foundation 中动态设置 Sass 变量 根据他们的文档 http foundation zurb com docs components tables html 您可以借助一些 Sass 变量来自定义表格 settings
  • 表单标签的 CSS 样式

    据我所知 一个
  • VM1550 installHook.js:1860 在控制台中记录为双行

    我正在使用反应应用程序并构建一个简单的应用程序 当我使用 console log 方法在控制台上记录某些内容时 控制台上会出现第二条日志 它似乎来自第 1860 行的文件名 installHook js 我已经尝试过寻找它 但我没有找到 我
  • 如何判断是哪个控件导致ViewState加载失败?

    我的页面面临 Viewstate 加载问题 页面有一个登录工具来登录管理员和非管理员用户 当非管理员用户登录页面并单击启用了自动回发的复选框时 会出现奇怪的行为 错误详情如下 后来我发现 在左侧的导航面板中 承载链接 侧边栏如下图所示 Se
  • 构建网站翻译文件

    我在建立网站时多次遇到这个问题 我将以使用 PHP 和 Laravel 为例进行解释 但这个问题在多个平台中都很常见 这已经在几个问题中得到了解决 post1 https stackoverflow com questions 317854
  • 网页编码,设置矛盾[重复]

    这个问题在这里已经有答案了 如果一个网页有 但http标头有 Content Type text html charset UTF 8 那么假设什么编码呢 在 HTML5 中 优先级定义为 用户浏览器设置 字节顺序标记 HTTP 标头 or
  • 从网站获取数据的vba代码

    我是这个网站和 VBA 编程的新手 我遇到了一个问题 我必须从中获取数据这一页 http www kieskeurig nl zoeken index html q 4960999543345 我需要有超链接网址Check Rates 10
  • 当url中有空格时htaccess重定向

    我想从仍然出现在谷歌搜索中的旧网址重定向到新网址 旧的网址是这样的 http www marionettecolla org file 20 mostra milano mostra marionette milano htm 我想将其重定
  • URL中的gs_upl是什么意思?

    在任何谷歌搜索 URL 中 gs upl 是什么意思 例如 那么 gs upl 1045l1663l0l3648l4l4l0l0l0l0l258l682l0 3 1l4l0 在这里意味着什么 从构建的脚本gs upl j 我找到 funct
  • 使用 hg 存储库作为网站

    这与我的安全问题有些相关here https stackoverflow com questions 2361626 security deny access to hg via mod rewrite 对实时网站使用 hg Mercuri
  • 在 html 中创建子页面 [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 假设我有一个网站http www example com http www example com 如何为此页面创建更多子页面 即 w
  • JavaScript 中的对象解构[重复]

    这个问题在这里已经有答案了 gt a a true Statement lt a true 上面的语句是赋值true to a 为什么上面的语句在chrome控制台中没有报错 虽然下面的语句给出了错误 gt a a true Stateme
  • 从 Harp.js 中的 EJS 模板调用另一个文件上的 javascript 函数

    尝试使用 Harp js 制作一个网站 我使用 ejs 模板 并希望将一些有用的 javascript 函数存储在中央文件中 我怎么做 我尝试使用 但它不起作用 似乎js文件没有被解析 有任何想法吗 谢谢 尽管有多种方法 有时 可以实现这一
  • 如何保护我的网站免遭 HTTrack 或其他软件的翻录?

    我最近获得了批准的网站模板主题森林 http themeforest net 我的网站流量过多 并注意到我在 Themeforest 上的演示被 HTTrack 等某些软件破坏 如果这种情况持续下去 该产品的销量最终可能会下降 那么 有什么
  • 为什么使用HTTP协议时需要指定端口号?

    即使我们使用HTTP协议 为什么还需要用IP地址指定端口号 例如 http xyz 8080 这到底是什么意思 我们已经知道 在使用 HTTP 时 请求将在端口 80 上提供服务 那么为什么我们要显式指定端口呢 HTTP 的默认端口为 80
  • 为什么要使用除 div 以外的任何东西? [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • Java selenium - 如何在 TimeoutException 之后正确刷新网页?

    ChromeOptions options new ChromeOptions options addExtensions new File extension 6 2 5 0 crx ZenMate options addExtensio

随机推荐

  • teamSpeak Server搭建Linux

    1 需要用到的 Xshell以及Xftp 操作云服务器以及上传teamSpeak Linux文件 还需要一台云服务器 2 开放服务器防火墙端口 1 如果Xshell连接不上就是22端口没开放 3 下载teamSpeak Linux文件 1
  • 平行云受邀参与撰写信通院XRMA联盟《3D数字内容生产技术白皮书》

    本周 由中国信息通信研究院发起编制的 3D数字内容生产技术白皮书 首次全体研讨会在线上举行 来自信通院 平行云 北航虚拟现实国家重点实验室 中国联通研究院 中国电信研究院 Cocos等多家单位的专家参与本次研讨 会上 信通院专家介绍项目研究
  • 华为OD机试 Python 最佳植树距离

    描述 小明得到了一项任务 在一个沙地里种树 不过 有些地方不能种树 只能选特定的位置来种 为了让树之间的空间尽可能大 从而更好地防沙 你要帮小明决定每棵树应该种在哪里 举个例子 如果合适的种树地点是1 3 5 6 7 10 13 你有3棵树
  • tortoisegit:TortoiseGitPlink Fatal Error - No supported authentication methods available

    TortoiseGit gt Settings gt Network gt 将SSH client设置成C Program Files Git usr bin ssh exe
  • ajax如何传对象去后台,ajax如何传一个实体对象到后台呢

    实体声明 function target code content isnt temgix temgix1 temgix2 temgix3 gruw this code code this content content this isnt
  • js 正则表达式

    js 正则表达式 http tool oschina net regex https developer mozilla org zh CN docs Web JavaScript Guide Regular Expressions 11
  • 深入理解计算机系统-程序链接

    链接 就是将不同部分的代码和数据手机和组合成为一个单一文件的过程 这个文件可被加载到存储器并执行 链接过程可以在如下时间执行 编译时 在源代码翻译成机器代码时 加载时 程序被加载器加载到存储器并执行时 运行时 由应用程序去完成这个工作 OS
  • 笛卡尔积的解释和作用

    一 笛卡尔积的解释 例 给出二个域 假设集合A a b 集合B 0 1 2 则两个集合的笛卡尔积为 a 0 a 1 a 2 b 0 b 1 b 2 类似的例子有 如果A表示某学校学生的集合 B表示该学校所有课程的集合 则A与B的笛卡尔积表示
  • c语言浮点数出错_20192020第二学期C语言练习题03

    一 单选题 共100题 50分 1 若有定义int a 10 p a 则表达式p 5表示 A 元素a 5 的地址 B 元素a 6 的地址 C 元素a 5 的值 D 元素a 6 的值 正确答案 A 2 题号 8281若有定义 int aa 8
  • FFmpeg将编码后数据保存成mp4

    以下测试代码实现的功能是 持续从内存块中获取原始数据 然后依次进行解码 编码 最后保存成mp4视频文件 可保存成单个视频文件 也可指定每个视频文件的总帧数 保存多个视频文件 为了便于查看和修改 这里将可独立的程序段存放在单个函数中 1 线程
  • Android判断有无外置SD卡(TF卡),并读写文件

    转载来源 https blog csdn net qq 27061049 article details 94178875 1 相关方法说明 2 Android Manifest xml 添加权限
  • 自制USB-HUB一分四扩展器

    自制USB HUB一分四扩展器 电脑只有俩USB2 0接口 不够用 所以决定自己做一个 使用到的芯片是sl2 1a 考虑到是自己用 所以成本还是比较重要的 这款芯片价格便宜 而且做扩展器也是够用的 适用于USB2 0 下面是原理图 其实这个
  • 您的计算机已被.balckhoues-V-XXXXXXX勒索病毒感染?恢复您的数据的方法在这里!

    引言 网络威胁不断进化 勒索病毒成为了当今数字世界中的一大威胁 在这些恶意软件中 balckhoues V XXXXXXX 勒索病毒以其高级的加密技术和威胁手段引起了广泛关注 本文91数据恢复将深入介绍 balckhoues V XXXXX
  • RAS非对称加密,前端js加密,node后端解密,jsencrypt

    纯HTML引入jsencrypt js文件 node后端解密也是独立文件jsencryptnode js 前端加密 下面是公钥
  • 实战经验分享:如何通过HTTP代理解决频繁封IP问题

    在网络爬虫和数据采集等应用中 频繁遇到目标网站封锁或限制IP的情况是非常常见的 为了解决这个问题 使用HTTP代理是一种有效的方法 本文将与您分享一些实战经验 帮助您通过HTTP代理解决频繁封IP问题 确保您的数据采集工作顺利进行 一 了解
  • 查看Oracle数据库的用户名和密码

    运行 cmd 按如下输入命令 sqlplus as sysdba sysdba为超级用户 alter user 用户名 account unlock 解除锁定 必须带 号 注意用英文字符 alter user 用户名 identified
  • 网页版百度网盘倍速方法

    打开控制台在console中输入以下代码 videojs getPlayers video player html5player tech setPlaybackRate 2 然后回车即可 后面数字即为速度可以自由定义 附 打开控制台办法
  • Selenium Python2022(二)

    您可能需要在多种情况下针对不同的浏览器 例如Firefox Chrome Internet Explorer Edge 测试代码 跨不同浏览器测试网站的做法称为自动浏览器测试 要使用Selenium自动化测试执行自动浏览器测试 您应该在单元
  • 元宇宙时代超高清视音频技术白皮书关于流媒体协议和媒体传输解读

    流媒体协议 元宇宙业务场景对流媒体传输的实时性和互动性提出了更高的要求 这就需要在传统的 RTMP SRT HLS 等基础上增加实时互动的支持 实时互动 指在远程条件下沟通 协作 可随时随地接入 实时地传递虚实融合的多维信息 身临其境的交互
  • 数据可视化:在 React 项目中使用 Vega 图表 (二)

    上一篇讲了如何在 React 项目中用 Vega Lite 绘制基本的 area chart 图表 本篇将介绍如何绘制多层图表 如何添加图例 多层图表 通过上一篇文章 我们知道了可以通过 mark encoding 等来描述我们想要的图表