Skip to main content
Announcements
Have questions about Qlik Connect? Join us live on April 10th, at 11 AM ET: SIGN UP NOW
cancel
Showing results for 
Search instead for 
Did you mean: 
markus_myllymaki
Partner - Contributor II
Partner - Contributor II

Sankey/Ribbon/Alluvial style visualization in Fortune 500 app

Hi All

I was checking yesterday Qlik Sense Fortune 500 application and I liked the visual style very much.  Excellent visual design.

https://qlik.fortune.com/global500/

I'm a very curious to know how the Sankey/Ribbon style visualization is built (please see the attachment). Is this possible to build by using Qlik Sense (May 2021) native visualizations?  Or have they used picasso.js open source charting library etc?

In older Sense version there were Bar & Area chart but it's no longer supported. I'm just wondering if this is done by using basic Bar chart.

2 Replies
Ouadie
Employee
Employee

Hi Markus - Thanks for showing interest in the global 500 visualization. Our team usually jokes about what that chart should be called and the best thing we came up with was "Slokey", a combination of "slope" and "sankey", so it's funny to see that you used three names on your end to refer to it.

The visualization was indeed built using the Picasso.js charting library in combination with Nebula.js to access the Qlik Sense App. You can check out this blog post for more information about the process and a step-by-step tutorial on how to build a chart using those tools in tandem.

Going back to the chart itself, we couldn't use the supported Sankey chart because we wanted to show a 1-to-1 relationship between the same dimension (Sector) portraying the increase or decrease of our measure (profits/revenues).

The chart is made up of stacked bars for the 2020 and 2021 years + a custom Picasso component that links each particular sector from left to right. Please refer to this blog post if you would like to read more about how a Picasso custom component is built. 

In our case, the "links" custom component we built draws an SVG path as follows:

 

const d = `
          M${xStart},${y0Top}
          ${xMid},${y0Top}
          ${xEnd},${y1Top}
          ${xEnd},${y1Bottom}
          ${xMid},${y0Bottom}
          ${xStart},${y0Bottom}
          Z`;

 

Given that:
xStart refers to the x -coordinate of 2020
xEnd refers to the x -coordinate of 2021
xMid is the mid-point between 2020 and 2021
y0Top is the top point of the sector on the left
y0Bottom is the bottom point of the sector on the right
y1Top is the top point of the sector on the right
y1Bottom is the bottom point of the sector on the right

(In other words, the path starts drawing from the top point of the sector on the left → goes to the center → then to the top point of the sector on the right → to the bottom point of the sector on the right → back to the center → then to the bottom point of the sector on the left.)

Below are code snippets for reference:

Nebula.js configuration:

 

useEffect(async () => {
    if (!nebula) return;
    chartRef.current = await nebula.render({
      element: elementRef.current,
      type: 'sankey',
      fields: [
        '[Date of Issue]',
        '[Sector]',
        '=Sum({$<[Date of Issue]={2020, 2021}>} [Sector Profit])',
      ],
    });
  }, [nebula]);

 

Custom component "links":

 

picasso.component('links', {
    require: ['renderer', 'resolver'],
    defaultSettings: {},
    render({ data }) {
      const { items } = this.resolver.resolve({
        data,
        settings: this.settings.settings,
      });

      const layers = items.reduce((acc, curr) => {
        acc[curr.data.series.label] = acc[curr.data.series.label] || { 2020: null, 2021: null };
        acc[curr.data.series.label][curr.data.label] = curr;
        return acc;
      }, {});

      const layerComponents = Object.values(layers).reduce((layerComponentsArr, layer) => {
        const xStart = layer['2020'].major * this.rect.width;
        const xMid = ((layer['2021'].major + layer['2020'].major) / 2) * this.rect.width;
        const xEnd = layer['2021'].major * this.rect.width;
        const y0Top = layer['2020'].top * this.rect.height;
        const y0Bottom = layer['2020'].bottom * this.rect.height;
        const y1Top = layer['2021'].top * this.rect.height;
        const y1Bottom = layer['2021'].bottom * this.rect.height;
        const d = `
          M${xStart},${y0Top}
          ${xMid},${y0Top}
          ${xEnd},${y1Top}
          ${xEnd},${y1Bottom}
          ${xMid},${y0Bottom}
          ${xStart},${y0Bottom}
          Z`;

        return [
          ...layerComponentsArr,
          {
            type: 'path',
            d,
            fill: layer['2020'].color,
            // eslint-disable-next-line no-nested-ternary
            opacity: (layer['2020'].highlight === undefined) ? 0.6 : (layer['2020'].data.series.label === layer['2020'].highlight ? 1 : 0.2),
          },
        ];
      }, []);

      const components = [...layerComponents];
      return components;
    },
  });

 

Usage of the custom component:

 

{
    type: 'links',
    key: 'links',
    displayOrder: 1,
    data: {
    collection: 'stacked',
    },
    settings: {
    major: { scale: 'x2' },
    top: (d) => d.resources.scale('y')(d.datum.start.value),
    bottom: (d) => d.resources.scale('y')(d.datum.end.value),
    color: {
        scale: 'color',
        ref: 'series',
    },
    highlight,
    },
},

 

 

The full chart extension file is attached to put everything into context.

Please let me know if you have any additional questions!

markus_myllymaki
Partner - Contributor II
Partner - Contributor II
Author

Thanks for an excellent answer! We will most probably try this at home.