# @nebula.js/sn-bullet-chart

The bullet chart displays a gauge with extended options.
Bullet charts can be used to visualize and compare performance of
a measure to a target value and to a qualitative scale,
such as poor, average, and good.

## Requirements

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

## Installing

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

## Usage

In the example below, the sales in different quarters are compared
using a bullet chart.

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

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

// Rendering a simple bullet chart
nuked.render({
  element: document.querySelector('.bullet'),
  type: 'bullet-chart',
  fields: ['Quarter', '=Sum(Sales)'],
  properties: {
    title: 'Sales by Quarters',
  },
});
```

You can create a bullet chart with one dimension and one measure,
or no dimension and multiple measures.

| Dimensions | Measures | Results                                                                                  |
| ---------- | -------- | ---------------------------------------------------------------------------------------- |
| 1          | 1        | A bullet chart with columns corresponding to different values in the dimension           |
| 0          | n        | A bullet chart with columns corresponding to the measures, aggreated over the dimension. |

## More examples

### Horizontal bullet chart with common range

Sometime it is easier for the eyes to perceive the information from a
bullet chart if it is horizontal and its measure has a common range.

```js
// Render a bullet chart horizontally, and with common range
nuked.render({
  element: document.querySelector('.bullet'),
  type: 'bullet-chart',
  fields: ['Quarter', '=Sum(Sales)'],
  properties: {
    title: 'Sales by Quarters',
    orientation: 'horizontal',
    measureAxis: {
      commonRange: true,
      dock: 'near',
    },
  },
});
```

### Adding target

By adding targets, you can compare not only the sales between
different quarters but also the sale of each quarter to its sale target.

```js
// Rendering a bullet chart with targets
nuked.render({
  element: document.querySelector('.bullet'),
  type: 'bullet-chart',
  fields: ['Quarter'],

  // Define `qMeasures` in `properties` instead of in `fields`
  properties: {
    title: 'Sales by Quarters',
    qHyperCubeDef: {
      qMeasures: [
        {
          qDef: {
            qDef: 'Sum(Sales)',
            target: 'Sum([Sale targets])',
          },
          qSortBy: {
            qSortByNumeric: -1,
          },
          qAttributeExpressions: [
            {
              qExpression: 'Sum([Sale targets])',
              id: 'bullet-target',
            },
          ],
        },
      ],
      qInitialDataFetch: [
        {
          qLeft: 0,
          qTop: 0,
          qWidth: 15,
          qHeight: 500,
        },
      ],
    },

    // Horizontal, with common range
    orientation: 'horizontal',
    measureAxis: {
      commonRange: true,
      dock: 'near',
    },
  },
});
```

### Add color segments

You can also add color segments to the chart to show poor/normal/good
performance. Here two limits
are added, splitting the range into three segments: red (lower than
90% of the target), yellow (within 90-110% of the target),
and green (higher than 110% of the target).

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

  // Define all `fields` in `properties`
  properties: {
    title: 'Sales by Quarters',

    qHyperCubeDef: {
      qDimensions: [
        {
          qDef: {
            qFieldDefs: ['Quarter'],
            qSortCriterias: [{ qSortByAscii: 1 }],
          },
        },
      ],
      qMeasures: [
        {
          qDef: {
            qDef: 'Sum(Sales)',
            target: 'Sum([Sale targets])',
            conditionalColoring: {
              segments: {
                limits: [
                  {
                    value: {
                      qValueExpression: {
                        qExpr: 'Sum([Sale targets])*0.9',
                      },
                    },
                  },
                  {
                    value: {
                      qValueExpression: {
                        qExpr: 'Sum([Sale targets])*1.1',
                      },
                    },
                  },
                ],
                paletteColors: [
                  {
                    color: '#7c4345',
                  },
                  {
                    color: '#e0db4d',
                  },
                  {
                    color: '#53ad55',
                  },
                ],
              },
            },
          },
          qSortBy: {
            qSortByNumeric: -1,
          },
          qAttributeExpressions: [
            {
              id: 'bullet-target',
              qExpression: 'Sum([Sale targets])',
            },

            {
              id: 'bullet-segment',
              qExpression: 'Sum([Sale targets])*0.9',
            },
            {
              id: 'bullet-segment',
              qExpression: 'Sum([Sale targets])*1.1',
            },
          ],
        },
      ],
      qInitialDataFetch: [
        {
          qLeft: 0,
          qTop: 0,
          qWidth: 15,
          qHeight: 500,
        },
      ],
      qInterColumnSortOrder: [0, 1],
    },

    // Horizontal, with common range
    orientation: 'horizontal',
    measureAxis: {
      commonRange: true,
      dock: 'near',
    },
  },
});
```

### Multiple measures, no dimension

The bullet chart can also be defined with no dimension and multiple measures.
Each bar represents corresponding measure aggregate over the dimension.

```js
// Rendering a bullet chart with three measures and no dimension
nuked.render({
  element: document.querySelector('.bullet'),
  type: 'bullet-chart',
  fields: ['=Sum(Coffee)', '=Sum(Tea)', '=Sum(Sales)'],
  properties: {
    title: 'Sales of Coffe, Tea, and Total',
  },
});
```

## Bullet chart plugins

A plugin can be passed into a bullet 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 and the position of the major axis
const majorAxisPlugin = {
  info: {
    name: 'major-axis-plugin',
    type: 'component-definition',
  },
  fn: ({ keys, layout }) => {
    const componentDefinition = {
      type: 'axis',

      // Provide the same name as the exisiting component to override it
      key: keys.COMPONENT.MAJOR_AXIS,
      settings: {
        labels: {
          fontFamily: 'Tahoma, san-serif',
          fontSize: '15px',
          fill: 'darkred',
        },
      },
    };
    return componentDefinition;
  },
};

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

// Rendering a bullet chart with plugins
    nuked.render({
      element: document.getElementById('object'),
      type: 'sn-bullet-chart',
      plugins: [majorAxisPlugin],
      properties,
    });
});
```

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: {
      MAIN: {
        MAJOR: KEYS.SCALE.MAIN.MAJOR,
        MINOR: KEYS.SCALE.MAIN.MINOR,
      },
    },
    COMPONENT: {
      BAR: KEYS.COMPONENT.BAR,
      MAJOR_AXIS: KEYS.COMPONENT.MAJOR_AXIS,
      MAJOR_AXIS_TITLE: KEYS.COMPONENT.MAJOR_AXIS_TITLE,
      BULLET_AXIS: KEYS.COMPONENT.BULLET_AXIS,
    },
    COLLECTION: {
      MAIN,
    },
  },
};
```

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

### Add new components

The new component can be a standard Picasso component
or a custom Picasso component. Here we demo a standard
reference line component.

```js
// Adding reference line at the mean of the targets
const meanReferenceLinePlugin = {
  info: {
    name: 'mean-reference-line-plugin',
    type: 'component-definition',
  },
  fn: ({ keys, layout }) => {
    const targets = layout.qHyperCube.qDataPages[0].qMatrix.map((item) => item[1].qAttrExps.qValues[0].qNum);
    const averageOfTargets = targets.reduce((accumulator, currentValue) => accumulator + currentValue) / targets.length;
    const componentDefinition = {
      key: 'mean-reference-line',
      type: 'ref-line',
      layout: { displayOrder: 2 },
      lines: {
        x: [
          {
            line: {
              stroke: 'darkgray',
              strokeWidth: 5,
            },
            scale: keys.SCALE.MINOR,
            value: averageOfTargets,
          },
        ],
      },
    };
    return componentDefinition;
  },
};
```

### Modify existing components

As an example, the positions and the appearance of the axes can be
modified completely 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.BULLET_AXIS` in
this example)

```js
// Modifying the look and the position of the bullet axis
const bulletAxisPlugin = {
  info: {
    name: 'bullet-axis-plugin',
    type: 'component-definition',
  },
  fn: ({ keys, layout }) => {
    const componentDefinition = {
      type: 'box-axis',

      // Provide the same name as the exisiting component to override it
      key: keys.COMPONENT.BULLET_AXIS,
      settings: {
        labels: {
          fontFamily: 'Tahoma, san-serif',
          fontSize: '15px',
          fill: 'darkblue',
        },
        line: { stroke: 'gray' },
      },
    };
    return componentDefinition;
  },
};
```

### 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.
