# @gramex/radar

A radial beeswarm or dot plot.

## Example

Given this [100 most popular movies on IMDb](docs/imdb.csv ":ignore"):

[![Table of 100 movies with name, genre, year, rating, and votes on IMDb](https://code.gramener.com/cto/gramex-radar/-/raw/main/docs/imdb.png)](docs/imdb.csv ":ignore")

... we can render the following radar:

[![IMDb radar visual - playground](https://code.gramener.com/cto/gramex-radar/-/raw/main/docs/playground.png)](docs/playground.html ":include height=420px")

## Installation

Install via `npm`:

```bash
npm install @gramex/radar@1
```

Use locally as an ES module:

```html
<script type="module">
  import { radar } from "./node_modules/@gramex/radar/dist/radar.js";
</script>
```

Use locally as a script:

```html
<script src="./node_modules/@gramex/radar/dist/radar.min.js"></script>
<script>
  gramex.radar(...)
</script>
```

Use via CDN as an ES Module:

```html
<script type="module">
  import { radar } from "https://cdn.jsdelivr.net/npm/@gramex/radar@1";
</script>
```

Use via CDN as a script:

```html
<script src="https://cdn.jsdelivr.net/npm/@gramex/radar@1/dist/radar.min.js"></script>
<script>
  gramex.radar(...)
</script>
```

## Data is a list of levels and spokes

The data is a list of rows containing an ordered "level" category (going outwards) and an unordered "spoke" category . Each row is an object with keys for each column. For example:

```js
const data = [
  { level: "low", spoke: "blue" },
  { level: "low", spoke: "orange" },
  { level: "low", spoke: "green" },
  { level: "medium", spoke: "blue" },
  { level: "medium", spoke: "orange" },
  { level: "medium", spoke: "green" },
  { level: "high", spoke: "blue" },
  { level: "high", spoke: "orange" },
  { level: "high", spoke: "green" },
];
```

[![Example](https://code.gramener.com/cto/gramex-radar/-/raw/main/docs/data.png)](docs/data.html ":include height=420px")

[See how to use radar](docs/data.html ":include :type=code")

Calling `radar(el, { data })` will render the radar. You need to style the output.

## Specify level and spoke columns

Use `level` and `spoke` parameters to customize the columns used for levels and spokes. For example, this
[employees data](docs/employees.csv ":ignore") has:

| team        | band   |
| ----------- | ------ |
| Analytics   | Band 1 |
| Consulting  | Band 2 |
| Design      | Band 3 |
| Engineering | Band 4 |
| ...         | ...    |

We use the `level` and `spoke` parameters to specify the columns:

```js
  const graph = radar("#radar", {
    data,
    level: (d) => d.band.replace(/Band /, ""),
    spoke: (d) => d.team,
    ...
  });
```

The radar automatically adjusts the

[![Example](https://code.gramener.com/cto/gramex-radar/-/raw/main/docs/employees.png)](docs/employees.html ":include height=420px")

[See how to specify level and spoke columns](docs/employees.html ":include :type=code")

## Re-order levels and spokes

Radar auto-extracts levels and spokes from data. Use `levels` and `spokes` to:

- Show only specific levels and spokes (even if they don't exist in the data)
- Re-order levels and spokes

For example:

```js
  const graph = radar("#radar", {
    data,
    // Show only the top 3 levels, in descending order
    levels: ["6", "5", "4"],
    // Show these levels in order, even though Legal doesn't exist
    spokes: ["Design", "Consulting", "Engineering", "Legal"],
    ...
  });
```

You can specificy `levels` and `spokes` dynamically with a function `Map(key, count) => [...keys]`. For example:

```js
  const graph = radar("#radar", {
    data,
    // Sort levels in reverse (e.g. "6", "5", "4", ... "1")
    levels: (counter) => [...counter.keys()].sort(d3.descending),
    // Sort spokes in descending order of count, i.e. largest first
    spokes: (counter) => d3.sort([...counter.keys()], d => -counter.get(d)),
    ...
  });
```

[![Example](https://code.gramener.com/cto/gramex-radar/-/raw/main/docs/reorder.png)](docs/reorder.html ":include height=420px")

[See how to customize levels and spokes](docs/reorder.html ":include :type=code")

## Resize levels and spokes

Radar distributes levels and spokes evenly within the container circle. Use `levelScale` and `spokeScale` to:

- Create spacing between levels or spokes
- Give more space to specific levels or spokes

For example:

```js
  // Start and end radius in SVG units (pixels)
  const levelRange = [[0, 60], [66, 120], [125, 140], [145, 160], [165, 180], [185, 200]];
  // Start and end angle in degrees
  const spokeRange = [[180, 210], [210, 240], [240, 255], [255, 310], [310, 330], [330, 360]]
  const graph = radar("#radar", {
    data,
    levelScale: (level, index) => levelRange[index],
    spokeScale: (spoke, index) => spokeRange[index],
    ...
  });
```

You can also specify the center of the radar using `cx` and `cy`. It defaults to `width / 2` and `height / 2`.

```js
  const graph = radar("#radar", {
    data,
    cx: 100,
    cy: 100,
    ...
  });
```

Use `graph.r` to get the maximum non-overlapping radius of the nodes.

[![Example](https://code.gramener.com/cto/gramex-radar/-/raw/main/docs/resize.png)](docs/resize.html ":include height=220px")

[See how to resize levels and spokes](docs/resize.html ":include :type=code")

## Style the radar

The returned object has the following D3 selections:

- `cells`: Arc paths for each level, spoke segment. (Used to style the background of the radar). The data includes:
  - `level`: The level
  - `spoke`: The spoke
  - `levelIndex`: The index of the level (0, 1, 2, ...)
  - `spokeIndex`: The index of the spoke (0, 1, 2, ...)
  - `r0`: The inner radius of the spoke
  - `r1`: The outer radius of the spoke
  - `a0`: The start angle of the spoke in radians
  - `a1`: The end angle of the spoke in radians
  - `n`: Number of data points in this cell
  - `data`: All data elements for this cell (i.e. level and spoke combination). Absent if n == 0
- `nodes`: Circles for each data element. The data includes:
  - `x`: The x-coordinate of the node
  - `y`: The y-coordinate of the node
- `levelLabels`: Text labels for each level. The data includes:
  - `level`: The level name / label
  - `levelIndex`: The index of the level (0, 1, 2, ...)
  - `r0`: The inner radius of the spoke
  - `r1`: The outer radius of the spoke
- `spokeLabels`: Text labels for each spoke. The data includes:
  - `spoke`: The spoke name / label
  - `spokeIndex`: The index of the spoke (0, 1, 2, ...)
  - `a0`: The start angle of the spoke in radians
  - `a1`: The end angle of the spoke in radians

You can style these using D3 selections. For example:

```js
// Color empty background cells pink, and filled cells white
graph.cells.attr("fill", (d) => (d.n ? "white" : "pink"));
// Color nodes with see-through black. Give them a bit more than the maximum non-overlapping size
graph.nodes.attr("fill", "rgba(0,0,0,0.2)").attr("r", graph.r * 1.5);
// Make labels bigger and red
graph.levelLabels.attr("font-size", 12).attr("fill", "red");
graph.spokeLabels.attr("font-size", 12).attr("fill", "red");
```

[![Example](https://code.gramener.com/cto/gramex-radar/-/raw/main/docs/style.png)](docs/style.html ":include height=420px")

[See how to style the radar](docs/style.html ":include :type=code")

## Add tooltips

You can use [Bootstrap tooltips](https://getbootstrap.com/docs/5.3/components/tooltips/).

1. Add a `data-bs-toggle="tooltip" title="..."` attribute to each feature using `update`
2. Call `new bootstrap.Tooltip(element, {selector: '[data-bs-toggle="tooltip"]'})` to initialize tooltips

[![Example](https://code.gramener.com/cto/gramex-radar/-/raw/main/docs/imdb.png)](docs/imdb.html ":include height=420px")

[See how to add tooltips](docs/imdb.html ":include :type=code")

## Bring your own D3

If you already have D3 loaded, or want to use a specific version / instance of D3, pass it to `radar(el, { d3 })`:

## API

[See API documentation](docs/api.md ":include :type=markdown")

## Release notes

- 1.0.0: 24 Mar 2024. Initial release

## Authors

- Anand S <s.anand@gramener.com>

## License

[MIT](https://spdx.org/licenses/MIT.html)
