於群峰之上,更覺長風浩蕩

Liquid Formatter AntV嵌入能力

2023.05.21

摘要

AntV X6 是基于 HTML 和 SVG 的图编辑引擎,提供低成本的定制能力和开箱即用的内置扩展,方便开发者快速搭建 DAG 图、ER 图、流程图、血缘图等应用。AntV G2 是一套简洁的渐进式可视化语法,可用于报表搭建、数据探索以及可视化叙事。这两款都是由阿里巴巴研发的AntV系列的标准版基础产品,使用Halo Liquid Formatter可以使博客具备这两个功能的嵌入能力。
Halo Liquid Formatter是基于Halo 2.x进行开发的多模态内容嵌入插件,使用Java、Kotlin、C++、Javascript等语言进行开发。该插件旨在为更多的博客用户群体带来更多文章内容的强化功能,其中AntV X6与AntV G2的演示读者可以向下浏览。

AntV X6

所有图表 | X6 (antgroup.com)

ER图

const LINE_HEIGHT = 24
const NODE_WIDTH = 150

X6.Graph.registerPortLayout(
  'erPortPosition',
  (portsPositionArgs) => {
    return portsPositionArgs.map((_, index) => {
      return {
        position: {
          x: 0,
          y: (index + 1) * LINE_HEIGHT,
        },
        angle: 0,
      }
    })
  },
  true,
)

X6.Graph.registerNode(
  'er-rect',
  {
    inherit: 'rect',
    markup: [
      {
        tagName: 'rect',
        selector: 'body',
      },
      {
        tagName: 'text',
        selector: 'label',
      },
    ],
    attrs: {
      rect: {
        strokeWidth: 1,
        stroke: '#5F95FF',
        fill: '#5F95FF',
      },
      label: {
        fontWeight: 'bold',
        fill: '#ffffff',
        fontSize: 12,
      },
    },
    ports: {
      groups: {
        list: {
          markup: [
            {
              tagName: 'rect',
              selector: 'portBody',
            },
            {
              tagName: 'text',
              selector: 'portNameLabel',
            },
            {
              tagName: 'text',
              selector: 'portTypeLabel',
            },
          ],
          attrs: {
            portBody: {
              width: NODE_WIDTH,
              height: LINE_HEIGHT,
              strokeWidth: 1,
              stroke: '#5F95FF',
              fill: '#EFF4FF',
              magnet: true,
            },
            portNameLabel: {
              ref: 'portBody',
              refX: 6,
              refY: 6,
              fontSize: 10,
            },
            portTypeLabel: {
              ref: 'portBody',
              refX: 95,
              refY: 6,
              fontSize: 10,
            },
          },
          position: 'erPortPosition',
        },
      },
    },
  },
  true,
)

const graph = new X6.Graph({
  container: document.getElementById('graph_1'),
  width: ${(<800:occupied)(800)},
  height: 450,
  connecting: {
    router: {
      name: 'er',
      args: {
        offset: 25,
        direction: 'H',
      },
    },
    createEdge() {
      return new Shape.Edge({
        attrs: {
          line: {
            stroke: '#A2B1C3',
            strokeWidth: 2,
          },
        },
      })
    },
  },
})

fetch('https://x6.antv.antgroup.com/data/er.json')
  .then((response) => response.json())
  .then((data) => {
    const cells = []
    data.forEach((item) => {
      if (item.shape === 'edge') {
        cells.push(graph.createEdge(item))
      } else {
        cells.push(graph.createNode(item))
      }
    })
    graph.resetCells(cells)
    graph.zoomToFit({ padding: 10, maxScale: 1 })
  })

泳道图

X6.Graph.registerNode(
  'lane',
  {
    inherit: 'rect',
    markup: [
      {
        tagName: 'rect',
        selector: 'body',
      },
      {
        tagName: 'rect',
        selector: 'name-rect',
      },
      {
        tagName: 'text',
        selector: 'name-text',
      },
    ],
    attrs: {
      body: {
        fill: '#FFF',
        stroke: '#5F95FF',
        strokeWidth: 1,
      },
      'name-rect': {
        width: 200,
        height: 30,
        fill: '#5F95FF',
        stroke: '#fff',
        strokeWidth: 1,
        x: -1,
      },
      'name-text': {
        ref: 'name-rect',
        refY: 0.5,
        refX: 0.5,
        textAnchor: 'middle',
        fontWeight: 'bold',
        fill: '#fff',
        fontSize: 12,
      },
    },
  },
  true,
)

X6.Graph.registerNode(
  'lane-rect',
  {
    inherit: 'rect',
    width: 100,
    height: 60,
    attrs: {
      body: {
        strokeWidth: 1,
        stroke: '#5F95FF',
        fill: '#EFF4FF',
      },
      text: {
        fontSize: 12,
        fill: '#262626',
      },
    },
  },
  true,
)

X6.Graph.registerNode(
  'lane-polygon',
  {
    inherit: 'polygon',
    width: 80,
    height: 80,
    attrs: {
      body: {
        strokeWidth: 1,
        stroke: '#5F95FF',
        fill: '#EFF4FF',
        refPoints: '0,10 10,0 20,10 10,20',
      },
      text: {
        fontSize: 12,
        fill: '#262626',
      },
    },
  },
  true,
)

X6.Graph.registerEdge(
  'lane-edge',
  {
    inherit: 'edge',
    attrs: {
      line: {
        stroke: '#A2B1C3',
        strokeWidth: 2,
      },
    },
    label: {
      attrs: {
        label: {
          fill: '#A2B1C3',
          fontSize: 12,
        },
      },
    },
  },
  true,
)

const graph = new X6.Graph({
  container: document.getElementById('graph_2'),
  width: ${(<800:occupied)(800)},
  height: 500,
  connecting: {
    router: 'orth',
  },
  translating: {
    restrict(cellView) {
      const cell = cellView.cell
      const parentId = cell.prop('parent')
      if (parentId) {
        const parentNode = graph.getCellById(parentId)
        if (parentNode) {
          return parentNode.getBBox().moveAndExpand({
            x: 0,
            y: 30,
            width: 0,
            height: -30,
          })
        }
      }
      return cell.getBBox()
    },
  },
})

fetch('https://x6.antv.antgroup.com/data/swimlane.json')
  .then((response) => response.json())
  .then((data) => {
    const cells = []
    data.forEach((item) => {
      if (item.shape === 'lane-edge') {
        cells.push(graph.createEdge(item))
      } else {
        cells.push(graph.createNode(item))
      }
    })
    graph.resetCells(cells)
    graph.zoomToFit({ padding: 10, maxScale: 1 })
  })

AntV G2

所有图表 | G2 (antgroup.com)

分面帧动画

fetch('https://gw.alipayobjects.com/os/bmw-prod/7fbb7084-cf34-4e7c-91b3-09e4748dc5e9.json')
  .then((res) => res.json())
  .then((data) => {
    const chart_4 = new G2.Chart({
      container: 'chart_4',
      theme: 'classic',
      width: ${(<800:occupied)(800)},
    });
    const padding = (node) =>
      node.attr('paddingRight', 86).attr('paddingLeft', 54);
    const encode = (node) =>
      node
        .encode('shape', 'smooth')
        .encode('x', (d) => new Date(d.date))
        .encode('y', 'unemployed')
        .encode('color', 'industry')
        .encode('key', 'industry');
    const utcX = (node) => node.scale('x', { utc: true });
    const keyframe = chart_4
      .timingKeyframe()
      .attr('direction', 'alternate')
      .attr('iterationCount', 2);
    keyframe
      .facetRect()
      .call(padding)
      .data(data)
      .encode('y', 'industry')
      .area()
      .attr('class', 'area')
      .attr('frame', false)
      .call(encode)
      .call(utcX)
      .scale('y', { facet: false })
      .style('fillOpacity', 1)
      .animate('enter', { type: 'scaleInY' });
    keyframe
      .area()
      .call(padding)
      .data(data)
      .attr('class', 'area')
      .transform({ type: 'stackY', reverse: true })
      .call(encode)
      .call(utcX)
      .style('fillOpacity', 1);
    keyframe
      .area()
      .call(padding)
      .data(data)
      .attr('class', 'area')
      .call(encode)
      .call(utcX)
      .style('fillOpacity', 0.8);
    chart_4.render();
  });

离散力导向图

const chart_2 = new G2.Chart({
	container: 'chart_2',
	theme: 'classic',
	width: ${(<=500:occupied)(410)},
	height: ${(<=500:occupied)(410)},
});

chart_2
  .forceGraph()
  .data({
    type: 'fetch',
    value: 'https://assets.antv.antgroup.com/g2/miserable-disjoint.json',
  })
  .layout({
    joint: false,
  });

chart_2.render();

矩行分面

const chart_3 = new G2.Chart({
	container: 'chart_3',
	theme: 'classic',
	paddingRight: 80,
	paddingBottom: 50,
	paddingLeft: 50,
	width: ${(<=500:occupied)(410)},
});
const facetRect = chart_3
	.facetRect()
	.data({
		type: 'fetch',
		value: 'https://assets.antv.antgroup.com/g2/penguins.json',
		transform: [
			{
				type: 'map',
				callback: ({
					culmen_depth_mm: depth,
					culmen_length_mm: length,
					...d
				}) => ({
					...d,
					culmen_depth_mm: depth === 'NaN' ? NaN : depth,
					culmen_length_mm: length === 'NaN' ? NaN : length,
				}),
			},
		],
	})
	.encode('x', 'sex')
	.encode('y', 'species');
facetRect
	.point()
	.attr('facet', false)
	.attr('frame', false)
	.encode('x', 'culmen_depth_mm')
	.encode('y', 'culmen_length_mm')
	.style('fill', '#ddd')
	.style('strokeWidth', 0);
facetRect
	.point()
	.encode('x', 'culmen_depth_mm')
	.encode('y', 'culmen_length_mm')
	.encode('color', 'island');
chart_3.render();

桑基图

const chart_1 = new G2.Chart({
  container: 'chart_1',
  theme: 'classic',
  width: ${(<600:occupied)(600)},
  height: ${(<600:occupied)(600)} / 1.5,
});

chart_1
  .sankey()
  .data({
    type: 'fetch',
    value: 'https://assets.antv.antgroup.com/g2/energy.json',
    transform: [
      {
        type: 'custom',
        callback: (data) => ({ links: data }),
      },
    ],
  })
  .layout({
    nodeAlign: 'center',
    nodePadding: 0.03,
  })
  .style('labelSpacing', 3)
  .style('labelFontWeight', 'bold')
  .style('nodeStrokeWidth', 1.2)
  .style('linkFillOpacity', 0.4);

chart_1.render();

柏林噪声场

fetch('https://gw.alipayobjects.com/os/antfincdn/OJOgPypkeE/poisson-disk.json')
  .then((res) => res.json())
  .then((poisson) => {
    const chart_5 = new G2.Chart({
      container: 'chart_5',
      theme: 'classic',
      height: ${(<600:occupied)(600)},
      width: ${(<600:occupied)(600)},
    });
    const data = poisson.map(([x, y]) => ({
      x,
      y,
      size: (noise(x + 2, y) + 0.5) * 24,
      rotate: noise(x, y) * 360,
    }));
    chart_5
      .vector()
      .data(data)
      .encode('x', 'x')
      .encode('y', 'y')
      .encode('rotate', 'rotate')
      .encode('size', 'size')
      .encode('color', 'black')
      .scale('size', { range: [6, 20] })
      .axis('x', { grid: false })
      .axis('y', { grid: false })
      .legend(false)
      .tooltip([
        { channel: 'x', valueFormatter: '.2f' },
        { channel: 'y', valueFormatter: '.2f' },
      ]);
    chart_5.render();
  });
const p = [
  151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140,
  36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234,
  75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237,
  149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48,
  27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105,
  92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73,
  209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86,
  164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38,
  147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189,
  28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101,
  155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232,
  178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12,
  191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31,
  181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
  138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215,
  61, 156, 180, 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233,
  7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148,
  247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57,
  177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165,
  71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133,
  230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1,
  216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116,
  188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124,
  123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16,
  58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163,
  70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110,
  79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193,
  238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107,
  49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45,
  127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128,
  195, 78, 66, 215, 61, 156, 180,
];

const noise = octave(perlin2, 2);

function octave(noise, octaves) {
  return function (x, y, z) {
    let total = 0;
    let frequency = 1;
    let amplitude = 1;
    let value = 0;
    for (let i = 0; i < octaves; ++i) {
      value += noise(x * frequency, y * frequency, z * frequency) * amplitude;
      total += amplitude;
      amplitude *= 0.5;
      frequency *= 2;
    }
    return value / total;
  };
}

function perlin2(x, y) {
  const xi = Math.floor(x),
    yi = Math.floor(y);
  const X = xi & 255,
    Y = yi & 255;
  const u = fade((x -= xi)),
    v = fade((y -= yi));
  const A = p[X] + Y,
    B = p[X + 1] + Y;
  return lerp(
    v,
    lerp(u, grad2(p[A], x, y), grad2(p[B], x - 1, y)),
    lerp(u, grad2(p[A + 1], x, y - 1), grad2(p[B + 1], x - 1, y - 1)),
  );
}

function fade(t) {
  return t * t * t * (t * (t * 6 - 15) + 10);
}

function grad2(i, x, y) {
  const v = i & 1 ? y : x;
  return i & 2 ? -v : v;
}

function lerp(t, a, b) {
  return a + t * (b - a);
}

評論