Create a cohort chart

This is the official documentation of the forestadmin-agent-django and forestadmin-agent-flask Python agents.

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

In the snippet, notice how we import the D3js ↗ library. Of course, you can choose to use any other library of your choice.

The resulting chart can be resized to fit your use.

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);
  }
}

Last updated