Implement A Sankey Chart Using Vega In React

Introduction

This article demonstrates how to create and use sankey chart using vega in reactjs. This article starts with the introduction of the react-vega package. After that, it demonstrates how vega json file works for creating any chart using its json formatted data.

What is Vega?

Vega is a visualization grammar, a declarative language for creating, saving, and sharing interactive visualization designs.With Vega, you can describe the visual appearance and interactive behavior of a visualization in a JSON format, and generate web-based views using Canvas or SVG.

Reference from:Learn more about Vega from here

To achieve this feature we will use a npm package npm i react-vega using this you can modify the json file and create it according to your requirement.

Prerequisites

  • Basic knowledge of ReactJS
  • Visual Studio Code
  • Node and NPM installed

Step 1. Install NPM dependencies Create a React.js Project

Let's create a new React project by using the following command.

npx create-react-app react-sankeychart

Step 2. Install NPM dependencies

npm i react-vega

Step 3. Creating a Component

Now go to the src folder and create a new folder for sankey chart and inside in it create a component, 'sankey-chart.js'. Add the following code to this component.

import React from 'react';
import { Vega } from 'react-vega';
import sankychart from './sankychart.json';

export const Sankychart = () => {

  const spec ={
    "$schema": "https://vega.github.io/schema/vega/v5.2.json",
    "height": 400,
    "width": 600,
      "data": [
        {
          "name": "rawData",
          "values": sankychart,
          "transform": [
            {
              "type": "formula",
              "expr": "datum['organizationReference.legalName']",
              "as": "stk1"
            },
            {
              "type": "formula",
              "expr": "datum.municipality",
              "as": "stk2"
            },
            {
              "type": "formula",
              "expr": "datum.sum_amount",
              "as": "size"
            }],
          "transform": [
            {
              "type": "formula",
              "expr": "datum['organizationReference.legalName']",
              "as": "stk1"
            },
            {
              "type": "formula",
              "expr": "datum.municipality",
              "as": "stk2"
            },
            {
              "type": "formula",
              "expr": "datum.sum_amount",
              "as": "size"
            }
          ]
        },
        {
          "name": "nodes",
          "source": "rawData",
          "transform": [
            {
              "type": "filter",
              "expr": "!groupSelector || groupSelector.stk1 == datum.stk1 || groupSelector.stk2 == datum.stk2"
            },
            {
              "type": "formula",
              "expr": "datum.stk1+datum.stk2",
              "as": "key"
            },
            {
              "type": "fold",
              "fields": [
                "stk1",
                "stk2"
              ],
              "as": [
                "stack",
                "grpId"
              ]
            },
            {
              "type": "formula",
              "expr": "datum.stack == 'stk1' ? datum.stk1+' '+datum.stk2 : datum.stk2+' '+datum.stk1",
              "as": "sortField"
            },
            {
              "type": "stack",
              "groupby": [
                "stack"
              ],
              "sort": {
                "field": "sortField",
                "order": "descending"
              },
              "field": "size"
            },
            {
              "type": "formula",
              "expr": "(datum.y0+datum.y1)/2",
              "as": "yc"
            }
          ]
        },
        {
          "name": "groups",
          "source": "nodes",
          "transform": [
            {
              "type": "aggregate",
              "groupby": [
                "stack",
                "grpId"
              ],
              "fields": [
                "size"
              ],
              "ops": [
                "sum"
              ],
              "as": [
                "total"
              ]
            },
            {
              "type": "stack",
              "groupby": [
                "stack"
              ],
              "sort": {
                "field": "grpId",
                "order": "descending"
              },
              "field": "total"
            },
            {
              "type": "formula",
              "expr": "scale('y', datum.y0)",
              "as": "scaledY0"
            },
            {
              "type": "formula",
              "expr": "scale('y', datum.y1)",
              "as": "scaledY1"
            },
            {
              "type": "formula",
              "expr": "datum.stack == 'stk1'",
              "as": "rightLabel"
            },
            {
              "type": "formula",
              "expr": "datum.total/domain('y')[1]",
              "as": "percentage"
            }
          ]
        },
        {
          "name": "destinationNodes",
          "source": "nodes",
          "transform": [
            {
              "type": "filter",
              "expr": "datum.stack == 'stk2'"
            }
          ]
        },
        {
          "name": "edges",
          "source": "nodes",
          "transform": [
            {
              "type": "filter",
              "expr": "datum.stack == 'stk1'"
            },
            {
              "type": "lookup",
              "from": "destinationNodes",
              "key": "key",
              "fields": [
                "key"
              ],
              "as": [
                "target"
              ]
            },
            {
              "type": "linkpath",
              "orient": "horizontal",
              "shape": "diagonal",
              "sourceY": {
                "expr": "scale('y', datum.yc)"
              },
              "sourceX": {
                "expr": "scale('x', 'stk1') + bandwidth('x')"
              },
              "targetY": {
                "expr": "scale('y', datum.target.yc)"
              },
              "targetX": {
                "expr": "scale('x', 'stk2')"
              }
            },
            {
              "type": "formula",
              "expr": "range('y')[0]-scale('y', datum.size)",
              "as": "strokeWidth"
            },
            {
              "type": "formula",
              "expr": "datum.size/domain('y')[1]",
              "as": "percentage"
            }
          ]
        }
      ],
    "scales": [
    {
          "name": "x",
          "type": "band",
          "range": "width",
          "domain": [
            "stk1",
            "stk2"
          ],
          "paddingOuter": 0.05,
          "paddingInner": 0.95
        },
        {
          "name": "y",
          "type": "linear",
          "range": "height",
          "domain": {
            "data": "nodes",
            "field": "y1"
          }
        },
      {
        "name": "color",
        "type": "ordinal",
        "range": [
            "#98df8a",
            "#27acaa",
            "#ff810a",
            "#34c668",
            "#178be4",
            "#714ac3",
            "#27acaa",
            "#8ce8ad",
            "#ffbb78",
            "#c4c4cd",
            "#d62728"
          ],
        "domain": {"data": "rawData", "field": "stk1"}
      },
      {
        "name": "stackNames",
        "type": "ordinal",
        "range":  ["Organization",
            "Municipality"],
        "domain": ["stk1", "stk2"]
      }
    ],
      "axes": [
        {
          "orient": "bottom",
          "scale": "x",
          "domain" : false,
          "ticks" : false,
          "labelPadding" : 20,
          "encode": {
            "labels": {
              "update": {
                "text": {
                  "scale": "stackNames",
                  "field": "value",
                  "fontWeight" : "bold",
                  "fontSize" : 14
                }
              }
            }
          }
        },
        {
          "orient": "top",
          "scale": "x",
          "domain" : false,
          "ticks" : false,
          "labelPadding" : 20,
          "encode": {
            "labels": {
              "update": {
                "text": {
                  "scale": "stackNames",
                  "field": "value",
                  "fontWeight" : "bold",
                  "fontSize" : 14
                }
              }
            }
          }
        },
        {
          "orient": "left",
          "scale": "y",
          "labels" : false,
          "domain" : false,
          "ticks" : false
        }
      ],
    "marks": [
      {
        "type": "path",
        "name": "edgeMark",
        "from": {"data": "edges"},
        "clip": true,
        "encode": {
          "update": {
            "stroke": [
              {
                "test": "groupSelector && groupSelector.stack=='stk1'",
                "scale": "color",
                "field": "stk2"
              },
              {"scale": "color", "field": "stk1"}
            ],
            "strokeWidth": {"field": "strokeWidth"},
            "path": {"field": "path"},
            "strokeOpacity": {
              "signal": "!groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 0.9 : 0.3"
            },
            "zindex": {
              "signal": "!groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 1 : 0"
            },
            "tooltip": {
              "signal": "datum.stk1 + ' → ' + datum.stk2 + '    ' + format(datum.size, ',.0f') + '   (' + format(datum.percentage, '.1%') + ')'"
            }
          },
          "hover": {"strokeOpacity": {"value": 1}}
        }
      },
      {
        "type": "rect",
        "name": "groupMark",
        "from": {"data": "groups"},
        "encode": {
          "enter": {
            "fill": {"scale": "color", "field": "grpId"},
            "width": {"scale": "x", "band": 1}
          },
          "update": {
            "x": {"scale": "x", "field": "stack"},
            "y": {"field": "scaledY0"},
            "y2": {"field": "scaledY1"},
            "fillOpacity": {"value": 0.6},
            "tooltip": {
              "signal": "datum.grpId + '   ' + format(datum.total, ',.0f') + '   (' + format(datum.percentage, '.1%') + ')'"
            }
          },
          "hover": {"fillOpacity": {"value": 1}}
        }
      },
      {
        "type": "text",
        "from": {"data": "groups"},
        "interactive": false,
        "encode": {
          "update": {
            "x": {
              "signal": "scale('x', datum.stack) + (datum.rightLabel ? bandwidth('x') + 8 : -8)"
            },
            "yc": {"signal": "(datum.scaledY0 + datum.scaledY1)/2"},
            "align": {"signal": "datum.rightLabel ? 'left' : 'right'"},
            "baseline": {"value": "middle"},
            "fontWeight": {"value": "bold"},
            "text": {
              "signal": "abs(datum.scaledY0-datum.scaledY1) > 13 ? datum.grpId : ''"
            }
          }
        }
      },
      {
        "type": "group",
        "data": [
          {
            "name": "dataForShowAll",
            "values": [{}],
            "transform": [{"type": "filter", "expr": "groupSelector"}]
          }
        ],
        "encode": {
          "enter": {
            "xc": {"signal": "width/2"},
            "y": {"value": 30},
            "width": {"value": 80},
            "height": {"value": 30}
          }
        },
        "marks": [
          {
            "type": "group",
            "name": "groupReset",
            "from": {"data": "dataForShowAll"},
            "encode": {
              "enter": {
                "cornerRadius": {"value": 6},
                "fill": {"value": "#f5f5f5"},
                "stroke": {"value": "#c1c1c1"},
                "strokeWidth": {"value": 2},
                "height": {"field": {"group": "height"}},
                "width": {"field": {"group": "width"}}
              },
              "update": {"opacity": {"value": 1}},
              "hover": {"opacity": {"value": 0.7}}
            },
            "marks": [
              {
                "type": "text",
                "interactive": false,
                "encode": {
                  "enter": {
                    "xc": {"field": {"group": "width"}, "mult": 0.5},
                    "yc": {
                      "field": {"group": "height"},
                      "mult": 0.5,
                      "offset": 2
                    },
                    "align": {"value": "center"},
                    "baseline": {"value": "middle"},
                    "fontWeight": {"value": "bold"},
                    "text": {"value": "Show All"}
                  }
                }
              }
            ]
          }
        ]
      }
    ],
    "signals": [
      {
        "name": "groupHover",
        "value": {},
        "on": [
          {
            "events": "@groupMark:mouseover",
            "update": "{stk1:datum.stack=='stk1' && datum.grpId, stk2:datum.stack=='stk2' && datum.grpId}"
          },
          {"events": "mouseout", "update": "{}"}
        ]
      },
      {
        "name": "groupSelector",
        "value": false,
        "on": [
          {
            "events": "@groupMark:click!",
            "update": "{stack:datum.stack, stk1:datum.stack=='stk1' && datum.grpId, stk2:datum.stack=='stk2' && datum.grpId}"
          },
          {
            "events": [
              {"type": "click", "markname": "groupReset"},
              {"type": "dblclick"}
            ],
            "update": "false"
          }
        ]
      }
    ]
  }

  return (
    <>
     <Vega spec={spec} actions={false} />
    </>
  );
};

export default Sankychart;

Step 3. Create a new component for sortable list

Create a new JSON file, 'sankrychart.json'. Add the following code to this file.

[{
  "organizationReference.legalName": "Autolinee Caivano",
  "municipality": "Picerno",
  "sum_amount": 251830
},
{
  "organizationReference.legalName": "P&C Consorzio Stabile arl",
  "municipality": "Pietragalla",
  "sum_amount": 90856
},
{
  "organizationReference.legalName": "Pepice Nicola",
  "municipality": "San Fele",
  "sum_amount": 119040
},
{
  "organizationReference.legalName": "F.lli Martoccia",
  "municipality": "Pietragalla",
  "sum_amount": 213877
},
{
  "organizationReference.legalName": "Daraio Pasquale",
  "municipality": "Baragiano",
  "sum_amount": 56298
},
{
  "organizationReference.legalName": "Inno srls",
  "municipality": "Ruvo del Monte",
  "sum_amount": 6393
},
{
  "organizationReference.legalName": "3N Costruzioni",
  "municipality": "Pietrapertosa",
  "sum_amount": 83291
},
{
  "organizationReference.legalName": "Esteja Soc. Coop",
  "municipality": "Tolve",
  "sum_amount": 527485
},
{
  "organizationReference.legalName": "C.C.D. Costruzioni e manutenzioni srl",
  "municipality": "Baragiano",
  "sum_amount": 239217
},
{
  "organizationReference.legalName": "Costruzioni Messina Soc. Coop.",
  "municipality": "Balvano",
  "sum_amount": 70799
},
]

Step 4. Add the below code in App.js file

import './App.css';
import Sankychart from './sanky-Chart/sankychart';

function App() {
  return (
    <Sankychart/>
  );
}

export default App;

Step 5. Output

Now, run the project by using the 'npm start' command, and check the result.

implement a sankey chart using Vega in React

Summary

In this article, we learned how to create a sankey chart using Vega library in ReactJS application.