# Create a cohort chart

{% hint style="success" %}
This is the official documentation of the `forestadmin-agent-django` and `forestadmin-agent-flask` Python agents.
{% endhint %}

![](https://2921382565-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HgnlEINLUAEQC1KgN48%2Fuploads%2Fgit-blob-ba20374523cbd66f3a737ba727de1a049c94fb8a%2Fsmart-chart-cohort.png?alt=media)

This is another example to help you build a Cohort Chart.

In the snippet, notice how we import the [D3js](https://d3js.org) library. Of course, you can choose to use any other library of your choice.

The resulting chart can be resized to fit your use.

{% tabs %}
{% tab title="Agent" %}

{% endtab %}

{% tab title="Component" %}

```js
import { action } from '@ember/object';
import { loadExternalJavascript } from 'client/utils/smart-view-utils';
import { inject as service } from '@ember/service';
import Component from '@glimmer/component';

export default class extends Component {
  @service lianaServerFetch;

  constructor(...args) {
    super(...args);
    this.load();
  }

  async load() {
    // Load charting library
    await loadExternalJavascript('https://d3js.org/d3.v6.min.js');

    // Load data from agent
    const response = await this.lianaServerFetch.fetch('/forest/_charts/cohort', {});
    const options = await response.json();
    const rows = this._buildRows(options.data);

    // Render chart
    this.renderChart(options.title, options.head, rows);
  }

  @action
  async renderChart(title, head, rows) {
    // Retrieve container
    const container = d3.select('#demo').append('div').attr('class', 'box');

    // Header
    container
      .append('div')
      .attr('class', 'box-header with-border')
      .append('p')
      .attr('class', 'box-title')
      .text(title || 'Retention Graph');

    const body = container.append('div').attr('class', 'box-body');
    const table = body
      .append('table')
      .attr('class', 'table table-bordered text-center');

    table
      .append('thead')
      .append('tr')
      .attr('class', 'retention-thead')
      .selectAll('td')
      .data(head)
      .enter()
      .append('td')
      .attr('class', (d, i) => (i == 0 ? 'retention-date' : 'days'))
      .text(d => d);

    table
      .append('tbody')
      .selectAll('tr')
      .data(rows)
      .enter()
      .append('tr')
      .selectAll('td')
      .data(row => row)
      .enter()
      .append('td')
      .attr('class', (d, i) => (i == 0 ? 'retention-date' : 'days'))
      .attr('style', (d, i) =>
        i > 1 ? `background-color :${this._shadeColor('#00c4b4', d)}` : null,
      )
      .append('div')
      .attr('data-toggle', 'tooltip')
      .text((d, i) => d + (i > 1 ? '%' : ''));
  }

  _buildRows(data) {
    const rows = [];
    Object.entries(data).forEach(([date, days]) => {
      const percentDays = [date];

      for (let i = 0; i < days.length; i++)
        percentDays.push(
          i > 0 ? Math.round((days[i] / days[0]) * 100 * 100) / 100 : days[i],
        );

      rows.push(percentDays);
    });

    return rows;
  }

  _shadeColor(color, percent) {
    color = this._isValidHex(color) ? color : '#3f83a3'; // handle null color
    percent = 1.0 - Math.ceil(percent / 10) / 10;

    const f = parseInt(color.slice(1), 16);
    const t = percent < 0 ? 0 : 255;
    const p = percent < 0 ? percent * -1 : percent;
    const R = f >> 16;
    const G = (f >> 8) & 0x00ff;
    const B = f & 0x0000ff;

    const rgb =
      0x1000000 +
      (Math.round((t - R) * p) + R) * 0x10000 +
      (Math.round((t - G) * p) + G) * 0x100 +
      (Math.round((t - B) * p) + B);

    return `#${rgb.toString(16).slice(1)}`;
  }

  _isValidHex(color) {
    return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(color);
  }
}
```

{% endtab %}

{% tab title="Template" %}

```handlebars
<div class='c-smart-chart'>
  <div id='demo'></div>
</div>
```

{% endtab %}

{% tab title="Styles" %}

```css
.c-smart-chart {
  display: flex;
  white-space: normal;
  bottom: 0;
  left: 0;
  right: 0;
  top: 0;
  background-color: var(--color-beta-surface);
}

.box {
  position: relative;
  border-radius: 3px;
  background: #ffffff;
  width: 100%;
}

.box-body {
  max-height: 500px;
  overflow: auto;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  border-bottom-right-radius: 3px;
  border-bottom-left-radius: 3px;
}

.box-header {
  color: #444;
  display: block;
  padding: 10px;
  position: relative;
}

.box-header .box-title {
  display: inline-block;
  font-size: 18px;
  margin: 0;
  line-height: 1;
}

.box-title {
  display: inline-block;
  font-size: 18px;
  margin: 0;
  line-height: 1;
}

.retention-thead,
.retention-date {
  background-color: #cfcfcf;
  font-weight: 700;
  padding: 8px;
}

.days {
  cursor: pointer;
  padding: 8px;
  text-align: center;
}
```

{% endtab %}
{% endtabs %}
