# @nebula.js/sn-funnel-chart

The funnel chart lets you add a sequential chart showing the connected stages of a process. Each stage decreases and should contain a subset of the previous stage. The decrease is gradual, giving the chart an ever narrower funnel.

## Requirements

Requires `@nebula.js/stardust` version `1.2.0` or later.

## Installing

If you use npm: `npm install @nebula.js/sn-funnel-chart`. You can also load through the script tag directly from [https://unpkg.com](https://unpkg.com/@nebula.js/sn-funnel-chart).

## Usage

The example below shows the number of candidates during sequential stages of a recruitment process. The chart is plotted in `width` mode, where the quantity at each stage is proportional to the upper edge of the segment.

```js

import { embed } from '@nebula.js/stardust';
import funnel from '@nebula.js/sn-funnel-chart';

// 'app' is an enigma app model
const nuked = embed(app, {
  types: [{ // register funnel chart
    name: 'funnel-chart',
    load: () => Promise.resolve(funnel);
  }]
});

// Rendering a simple funnel chart
nuked.render({
  element: document.querySelector('.funnel'),
  type: 'funnel-chart',
  fields: ['Hiring Stage', '=Sum(NumberOfCandidates)'],
  properties: {
    title: 'Recruitment Process',
  },
});
```

Funnel chart can represent anything that is decreasing in size,
showing a process that starts at 100% and ends with a lower percentage.
The funnel chart is the opposite to a pyramid graph, which has
increasing stages instead of decreasing stages.

The chart requires one dimension and one measure.
Unlike bar charts, funnel chart segments are centered
to create a funnel shape.

## More examples

### Area mode

You can configure the funnel chart so that the area of each item is
proportional to the measure.

```js
// Rendering a funnel chart in area mode
nuked.render({
  element: document.querySelector('.funnel'),
  type: 'funnel-chart',
  fields: ['Hiring Stage', '=Sum(NumberOfCandidates)'],
  properties: {
    title: 'Recruitment Process',
    funnel: {
      mode: 'AREA',
    },
  },
});
```

### Height mode

The height of each item is proportional to the measure.

```js
// Rendering a funnel chart in height mode
nuked.render({
  element: document.querySelector('.funnel'),
  type: 'funnel-chart',
  fields: ['Hiring Stage', '=Sum(NumberOfCandidates)'],
  properties: {
    title: 'Recruitment Process',
    funnel: {
      mode: 'HEIGHT',
    },
  },
});
```

### Ordering mode

Only order matters, the items have the same height, ordered from top to bottom.

```js
// Rendering a funnel chart in order mode
nuked.render({
  element: document.querySelector('.funnel'),
  type: 'funnel-chart',
  fields: ['Hiring Stage', '=Sum(NumberOfCandidates)'],
  properties: {
    title: 'Recruitment Process',
    funnel: {
      mode: 'ORDER',
    },
  },
});
```

### More configurations

You can also remove the dimension title, show the actual number of
candidates instead of the percentage, or color the stages by measure.

```js
// Rendering a funnel chart with customized look
nuked.render({
  element: document.querySelector('.funnel'),
  type: 'funnel-chart',

  // Defines the fields in `properties`
  properties: {
    title: 'Recruitment Process',

    // Hide dimension title
    showDimensionTitle: false,

    qHyperCubeDef: {
      qDimensions: [
        {
          qDef: { qFieldDefs: ['Hiring Stage'] },

          // Color the dimension by `MedianRequestedSalary`
          qAttributeExpressions: [
            {
              qExpression: 'Avg(MedianRequestedSalary)',
              id: 'colorByAlternative',
            },
          ],
        },
      ],
      qMeasures: [
        {
          qDef: { qDef: 'Sum(NumberOfCandidates)' },
        },
      ],
      qInterColumnSortOrder: [1, 0],
      qInitialDataFetch: [
        {
          qLeft: 0,
          qTop: 0,
          qWidth: 2,
          qHeight: 5000,
        },
      ],
    },

    // Show actual number of candidates instead of percentage
    dataPoint: {
      auto: false,
      labelMode: 'value',
    },

    // Configure color
    color: {
      auto: false,
      byMeasureDef: {
        key: 'Avg(MedianRequestedSalary)',
        type: 'expression',
      },
      measureScheme: 'dg',
      mode: 'byMeasure',
      reverseScheme: true,
    },
  },
});
```

## Funnel chart plugins

A plugin can be passed into a funnel chart to add or modify its capability
or visual appearance.
A plugin needs to be defined before it can be rendered together with the chart.

```js
// Step 1: define the plugin

// Modifying the look of the dimension title
const dimensionTitlePlugin = {
  info: {
    name: 'dimension-title-plugin',
    type: 'component-definition',
  },
  fn: ({ keys, layout }) => {
    const componentDefinition = {
      type: 'text',

      // Provide the same name as the exisiting component to override it
      key: keys.COMPONENT.DIMENSION_TITLE,
      text: 'The percentage of candidates remain after each hiring stage',
      layout: {
        dock: 'bottom',
      },
    };
    return componentDefinition;
  },
};

// Step 2: passing the plugin definition into the render function

// Render a funnel chart with plugins
nuked.render({
  element: document.querySelector('#object'),
  type: 'sn-funnel-chart',
  plugins: [dimensionTitlePlugin],
  fields: ['Hiring Stage', '=Sum(NumberOfCandidates)'],
  properties: {
    title: 'Recruitment Process',
  },
});
```

The plugin definition is an object, with two properties `info` and `fn`.
The `fn` returns a `picasso.js` component. To build this component,
some important chart internals are passed into the argument object of `fn`.

```js
// Structure of the argument object of fn
const pluginArgs = {
  layout,
  keys: {
    SCALE: { FILL },
    COMPONENT: { FUNNEL, FUNNEL_LABELS, DIMENSION_TITLE },
  },
};
```

With plugins, you can either add new components or modify existing components
of the funnel chart.

### Modify existing components

As an example, the positions and the appearance of the funnel labels
can be modified by plugins.

To overide an existing component, `fn` should returns a `picasso.js` component
that has the same `key` as the existing component
(`keys.COMPONENT.FUNNEL_LABELS` in
this example)

```js
// Modifying the look of the funnel labels
const funnelLabelsPlugin = {
  info: {
    name: 'funnel-labels-plugin',
    type: 'component-definition',
  },
  fn: ({ keys, layout }) => {
    const componentDefinition = {
      type: 'labels',

      // Provide the same name as the exisiting component to override it
      key: keys.COMPONENT.FUNNEL_LABELS,
      layout: { displayOrder: 2 },
      settings: {
        sources: [
          {
            component: keys.COMPONENT.FUNNEL,
            selector: '.labelLeft',
            strategy: {
              type: 'rows',
              settings: { align: 0, fill: 'gray', fontSize: '15px' },
            },
          },
          {
            component: keys.COMPONENT.FUNNEL,
            selector: '.labelCenter',
            strategy: {
              type: 'rows',
              settings: { align: 0.5, fill: 'darkred', fontSize: '15px' },
            },
          },
        ],
      },
    };
    return componentDefinition;
  },
};
```

### Add new components

The new component can be a standard Picasso component
or a custom Picasso component. The code for adding a new component is
similar to that for modifying an existing component, the only difference
is that the `key` should be different from that of
any of the existing components.

### Plugins disclaimer

- The plugins API is still experimental.
- We can not guarantee our charts to be compatible with all different settings, especially when modifying existing components.
