G2 带你一图速览巴以冲突的残酷10天!

最近巴以冲突是人们关注的热点,这里我们就使用 G2 可视化一下其时间线,最后的效果如下图所示:

那接下来就来看看如何用 G2 来绘制这张图,看完你将有如下的收获:

  • G2 如何设置图表的 title
  • G2 如何设置图表的背景颜色
  • G2 中 fold 数据处理的使用方法
  • G2 如何自定义映射颜色
  • G2 如何绘制数据驱动的 label
  • G2 如何绘制 html label
  • G2 如何自定义图例的文本

可交互的版本可以通过如下渠道看:

接下来我们就开始吧!

准备

首先我们了解一下数据,该数据是一份表格数据,每一个数据行包含时间、事件以及当前两方的伤亡情况。

我们简单封装一个工具函数来简化可视化声明,这里用到了 G2 5.0 新的 Spec API:

javascript 复制代码
function g2(options) {
  const chart = new G2.Chart();
  chart.options(options);
  chart.render();
  return chart.getContainer();
}

思路

在开始写代码前,我们简单来梳理一下可视化的思路。上面这张图看起来复杂,其实主体本质上就是一个柱形图 + 面积图 + 数据标签,而我们可以通过类似"叠图层"的方式,将它们组合成一个图表,最后再优化一下样式即可。

下面就到了写代码时间。

视图

我们所有的内容都是放在如下的一个视图里的,然后通过 children 配置去添加不同的标记。

javascript 复制代码
g2({
  type: "view",
  width: 800, // 一些布局信息
  height: 1200,
  insetTop: 15,
  insetRight: 10,
  marginRight: 150,
  data, // 可视化数据
  title: { // 图表标题
    title: "巴以冲突时间线",
    subtitle: "数据来源:新华网|统计时间:2023.10.17",
    titleFontWeight: "bolder",
    titleFontSize: 20
  },
  coordinate: { transform: [{ type: "transpose" }] }, // 向 y 方向绘制
  encode: { x: "date", y: (d) => d.death + d.injured }, // 一些共用的编码信息
  axis: false, // 隐藏坐标轴
  style: { viewFill: "rgb(253, 247, 239)" }, // 设置背景颜色
  children: [ // 添加标记,具体的配置见下面
    { type: "area", /* ... */ },
    { type: "interval", /* ... */ },
    {
      type: "line", /* ... */
      labels: [{/* ... */}, {/* ... */}]
    },
    { type: "point", /* ... */ },
    { type: "text", /* ... */ }
  ]
})

面积图

我们首先添加一个 Area Mark 绘制一个面积图,来作为我们的背景:

javascript 复制代码
g2({
  // ...
  children: [
    {
      type: "area",
      tooltip: false,
      style: { fillOpacity: 0.1, fill: "red" }
    },
    // ...
  ]
})

条形图

接下来我们使用 Interval Mark 绘制图中的条形图:

这里我们首先需要使用 fold 数据处理方法将数据展开,使得每一个条对应一条数据,然后自定义颜色、图例文本和独立 x 通道的比例尺。

javascript 复制代码
g2({
  // ...
  children: [
    // ...
    {
      type: "interval",
      scale: {
        x: { key: "interval", paddingInner: 0.15 }, // 独立 x 通道比例尺
        color: {
          range: [ // 自定义颜色
            "rgba(0, 0, 0, 0.8)",
            "rgba(0, 0, 0, 0.5)",
            "rgba(236, 61, 11, 0.8)",
            "rgba(236, 61, 11, 0.5)"
          ]
        }
      },
      legend: {
        color: {
          position: "bottom",
          labelFormatter: (d) => {
            const labelByValue = { // 自定义图例文本
              israelDeath: "以色列死亡人数",
              hamasDeath: "哈马斯死亡人数",
              israeInjured: "以色列受伤人数",
              hamasInjured: "哈马斯受伤人数"
            };
            return labelByValue[d];
          }
        }
      },
      data: {
        transform: [
          { // 展开数据
            type: "fold",
            fields: [
              "israelDeath",
              "hamasDeath",
              "israeInjured",
              "hamasInjured"
            ],
            key: "type",
            value: "value"
          }
        ]
      },
      // 根据展开数据指定通道
      encode: { y: "value", color: "type", series: "type" }
    },
    // ...
  ]
})

线和数据标签

这之后我们绘制那根代表伤亡总人数的趋势线,以及为时间和事件添加标注文本:

这里我们需要添加一个 Line Mark,以及通过其 labels 属性,给它添加两个数据标签。第一个用来展示时间,第一个用来展示事件。需要注意的是,因为事件文本需要突出伤亡的具体信息(数字),对文本的展示和布局有较高的要求,所以这里我们使用 html 绘制:

javascript 复制代码
g2({
  // ...
  children: [
    // ...
    {
      type: "line", // 绘制趋势线
      style: {
        stroke: "black",
        shape: "smooth",
        strokeWidth: 10,
        lineCap: "round"
      },
      tooltip: false,
      labels: [
        { // 展示日期,属性都是数据驱动的
          text: (d) => {
            const N = d.date.split("-");
            return `${N[1]}.${N[2]}`;
          },
          fontFamily: "Alibaba Sans 102",
          letterSpacing: -1,
          fontSize: 32,
          fontWeight: "bold",
          dy: (d) => (+d.date.split("-").pop() === 17 ? -15 : 0),
          dx: (d) => (+d.date.split("-").pop() < 13 ? 30 : -30),
          textAnchor: (d) => (+d.date.split("-").pop() < 13 ? "start" : "end"),
          fillOpacity: 1
        },
        { // 展示事件信息
          text: (d) => d.event,
          // render 函数返回的 string 会作为 innerHTML 渲染到 DOM 树中
          render: (text, d) => {
            const date = +d.date.split("-").pop();
            const right = date < 13;
            const dx = right ? 65 : -380;
            const dy = right ? 15 : date === 17 ? -100 : -90;
            const span = (text) => {
              return `<span style="color:red;font-size:18px;font-weight:bold">${text}</span>`;
            };
            const textHtml = text
              .replace(d.injured, span(d.injured))
              .replace(d.death, span(d.death));
            const width = date === 12 ? 250 : 300;
            return `<div style="
                      width: ${width}px; 
                      font-size: 12px; 
                      transform: translate(${dx}px, ${dy}px);
                      padding: 5px;
                      color: black;
                      font-family: 'Alibaba Sans 102'"
                    >${textHtml}</div>`;
          }
        }
      ]
    },
    // ...
  ]
})

圆圈和 Emoji

最后我们来绘制背景的圆圈和 Emoji 表情:

javascript 复制代码
g2({
  // ...
  children: [
    // ...
    { // 背景圆
      type: "point",
      encode: { size: 14 },
      style: {
        shape: "point",
        fill: "white",
        stroke: "red",
        strokeWidth: 3,
        fillOpacity: 1
      },
      tooltip: false
    },
    { // 用 text 标记去绘制 Emoji
      type: "text",
      encode: { text: "date" },
      scale: {
        text: { // 映射文本内容
          type: "ordinal",
          range: ["🚀", "🔥", "⚡️", "🏠", "✈️", "👊", "👊", "🙅"]
        }
      },
      style: {
        textAlign: "center",
        textBaseline: "middle",
        dx: 2,
        fontSize: 15
      }
    }
    // ...
  ]
})

小结

这篇文章简单教大家如何用 G2 去绘制比较复杂的可视化,同时也给大家展示了 G2 在保证一定易用性的基础上,给用户的所提供的灵活性。如果觉得还不错的,欢迎去 Github 点一个 Star,然后一起让 G2 变得更好。

最后,愿天下没有战争。

附录

最后附上 Live Demo 的地址和完整代码。

javascript 复制代码
g2({
  type: "view",
  width: 800,
  height: 1200,
  insetTop: 15,
  insetRight: 10,
  marginRight: 150,
  data,
  title: {
    title: "巴以冲突时间线",
    subtitle: "数据来源:新华网|统计时间:2023.10.17",
    titleFontWeight: "bolder",
    titleFontSize: 20
  },
  coordinate: { transform: [{ type: "transpose" }] },
  encode: { x: "date", y: (d) => d.death + d.injured },
  axis: false,
  style: { viewFill: "rgb(253, 247, 239)" },
  children: [
    {
      type: "area",
      tooltip: false,
      style: { fillOpacity: 0.1, fill: "red" }
    },
    {
      type: "interval",
      scale: {
        x: { key: "interval", paddingInner: 0.15 },
        color: {
          range: [
            "rgba(0, 0, 0, 0.8)",
            "rgba(0, 0, 0, 0.5)",
            "rgba(236, 61, 11, 0.8)",
            "rgba(236, 61, 11, 0.5)"
          ]
        }
      },
      legend: {
        color: {
          position: "bottom",
          labelFormatter: (d) => {
            const labelByValue = {
              israelDeath: "以色列死亡人数",
              hamasDeath: "哈马斯死亡人数",
              israeInjured: "以色列受伤人数",
              hamasInjured: "哈马斯受伤人数"
            };
            return labelByValue[d];
          }
        }
      },
      data: {
        transform: [
          {
            type: "fold",
            fields: [
              "israelDeath",
              "hamasDeath",
              "israeInjured",
              "hamasInjured"
            ],
            key: "type",
            value: "value"
          }
        ]
      },
      encode: { y: "value", color: "type", series: "type" }
    },
    {
      type: "line",
      style: {
        stroke: "black",
        shape: "smooth",
        strokeWidth: 10,
        lineCap: "round"
      },
      tooltip: false,
      labels: [
        {
          text: (d) => {
            const N = d.date.split("-");
            return `${N[1]}.${N[2]}`;
          },
          fontFamily: "Alibaba Sans 102",
          letterSpacing: -1,
          fontSize: 32,
          fontWeight: "bold",
          dy: (d) => (+d.date.split("-").pop() === 17 ? -15 : 0),
          dx: (d) => (+d.date.split("-").pop() < 13 ? 30 : -30),
          textAnchor: (d) => (+d.date.split("-").pop() < 13 ? "start" : "end"),
          fillOpacity: 1
        },
        {
          text: (d) => d.event,
          render: (text, d) => {
            const date = +d.date.split("-").pop();
            const right = date < 13;
            const dx = right ? 65 : -380;
            const dy = right ? 15 : date === 17 ? -100 : -90;
            const span = (text) => {
              return `<span style="color:red;font-size:18px;font-weight:bold">${text}</span>`;
            };
            const textHtml = text
              .replace(d.injured, span(d.injured))
              .replace(d.death, span(d.death));
            const width = date === 12 ? 250 : 300;
            return `<div style="
                      width: ${width}px; 
                      font-size: 12px; 
                      transform: translate(${dx}px, ${dy}px);
                      padding: 5px;
                      color: black;
                      font-family: 'Alibaba Sans 102'"
                    >${textHtml}</div>`;
          }
        }
      ]
    },
    {
      type: "point",
      encode: { size: 14 },
      style: {
        shape: "point",
        fill: "white",
        stroke: "red",
        strokeWidth: 3,
        fillOpacity: 1
      },
      tooltip: false
    },
    {
      type: "text",
      encode: { text: "date" },
      scale: {
        text: {
          type: "ordinal",
          range: ["🚀", "🔥", "⚡️", "🏠", "✈️", "👊", "👊", "🙅"]
        }
      },
      style: {
        textAlign: "center",
        textBaseline: "middle",
        dx: 2,
        fontSize: 15
      }
    }
  ]
})
相关推荐
数字新视界18 小时前
【无标题】
物联网·数据可视化·数据中心·数据中心基础设施管理·dcim管理系统·数据中心dcim
MindUp20 小时前
AI 能帮我做数据分析吗?从使用场景看三类辅助方式
人工智能·数据挖掘·数据分析
柳杉1 天前
用 Codex 完成智慧高速 3D 可视化大屏,从 18 张行业设计稿到可交互的运行监测中心
前端·ai编程·数据可视化
SelectDB1 天前
从 StarRocks 走向 Apache Doris:如何实现引擎平滑替换与架构升级?
大数据·数据库·数据分析
总要学点什么2 天前
第79篇:数据可视化-模拟迁徙效果——把三十条航线画成会飞的弧
3d·信息可视化·数据挖掘·数据分析·gis·webgl·cesium
Apok'2 天前
零基础学Python量化(1):研究大纲与流程
python·机器学习·金融·数据挖掘·数据分析
benchmark_cc2 天前
如何为 QuantDash Python SDK 编写一个最小化的连接验证脚本?
python·数据分析·pandas·量化交易·股票数据·quantdash
MetWeave2 天前
一根杆与一张场:把你的机场画上地图
前端·数据可视化
效率工作实验室2 天前
商业智能报告趋势解析:标准化报告为何正被个性化分析取代
数据分析·agent·商业智能报告·标准化报告·个性化分析
钱工的门窗技术笔记2 天前
铝木复合门窗中木材的角色分类与界面可分解性:结构判定、失效路径与维护设计
网络·物联网·数据分析