Newer
Older
dmpopidor / app / javascript / views / usage / index.js
import moment from 'moment/moment';
import Chart from 'chart.js';

$(() => {
  const usageFormSelector = '.usage_index';
  const apiToken = $(usageFormSelector).find('input[name="api_token"]').val();
  // Builds an object whose keys are the topic fro the select options and value its the value
  // associated to the attribute data-url of each option
  const topicToURL = $(`${usageFormSelector} select[name="topic"]`).find('option').map((i, el) => {
    const topic = $(el);
    return { [topic.val()]: topic.attr('data-url') };
  }).get() // An array of objects { topic: URL }
    .reduce((acc, value) => $.extend(acc, value), {});
  const rangeDatesUpToLastYearFromNow = () => {
    const getLastMonth = () => moment().subtract(1, 'month').clone();
    const rangeDates = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1].reduce((acc, v, i) => {
      const id = getLastMonth().subtract(i, 'month').format('MMM-YY');
      acc[id] = {
        start_date: getLastMonth().startOf('month').subtract(i, 'month').format('YYYY-MM-DD'),
        end_date: getLastMonth().endOf('month').subtract(i, 'month').format('YYYY-MM-DD'),
        id,
      };
      return acc;
    }, {});

    return rangeDates;
  };
  const createChart = ({ selector, data, appendTolabel = '' } = {}) => {
    new Chart($(selector), { // eslint-disable-line no-new
      type: 'bar',
      data: {
        labels: Object.keys(data),
        datasets: [{
          data: Object.keys(data).map(k => data[k]),
          backgroundColor: '#4F5253',
          // TODO parameterised according to roadmap main colour instance
        }],
      },
      options: {
        legend: {
          display: false,
        },
        tooltips: {
          callbacks: {
            label: tooltipItem => `${tooltipItem.yLabel} ${appendTolabel}`,
          },
        },
        scales: {
          yAxes: [{
            ticks: { min: 0, suggestedMax: 50 },
          }],
        },
      },
    });
  };
  /*
    Submit event associated to the filter by dates form
  */
  $(usageFormSelector).on('submit', (e) => {
    e.preventDefault();
    const target = $(e.target);
    const topic = target.find('select[name="topic"]').val();
    const orgId = target.find('select[name="org_id"]').val() || target.find('input[name="org_id"]').val();
    $('[data-topics]').hide(); // Hides data-topics container
    $('[data-topic]').hide(); // Hides any data-topic specific
    const ajaxSettings = ({ totals = false } = {}) => ({
      headers: { Authorization: `Token token="${apiToken}"` },
      url: topicToURL[topic],
      data: totals ? { topic, org_id: orgId } : target.serialize(),
    });
    // Awaits until both AJAX request responds.
    // Note, the success handler is only executed if both AJAX requests return success
    $.when($.ajax(ajaxSettings()), $.ajax(ajaxSettings({ totals: true }))).then(
      (dataRangeSuccessCb, dataTotalsSuccessCb) => {
        let dataRange = null;
        let dataTotals = null;
        if (dataRangeSuccessCb[0]) { // data is the first argument of the successCb ranges
          const dataKeys = Object.keys(dataRangeSuccessCb[0]);
          // We assume the dataRange is the first key of the object responded
          dataRange = dataKeys.length > 0 ? dataRangeSuccessCb[0][dataKeys[0]] : null;
        }
        if (dataTotalsSuccessCb[0]) { // data is the first argument of the successCb for totals
          const dataKeys = Object.keys(dataTotalsSuccessCb[0]);
          // We assume the dataTotals is the first key of the object responded
          dataTotals = dataKeys.length > 0 ? dataTotalsSuccessCb[0][dataKeys[0]] : null;
        }
        const dataTopics = $('[data-topics]');
        const views = $(`[data-topic="${topic}"]`);
        dataRange !== null ? dataTopics.find('[data-range]').html(dataRange) : undefined; // eslint-disable-line no-unused-expressions
        dataTotals !== null ? dataTopics.find('[data-totals]').html(dataTotals) : undefined; // eslint-disable-line no-unused-expressions
        views.show();
        dataTopics.show();
      },
    ); // TODO request error handling
  });
  /*
    Click event associated to each Export button
  */
  $('button.stat[data-url]').on('click', (e) => {
    const rangeDates = rangeDatesUpToLastYearFromNow();
    $.ajax({
      headers: { Authorization: `Token token="${apiToken}"` },
      url: $(e.currentTarget).attr('data-url'),
      data: { range_dates: rangeDates },
    }).then((data, statusText, jqXHR) => {
      /* eslint-env browser */
      const blob = new Blob([data], { type: 'text/csv' });
      // Attemps to match the filename from the Content-Disposition header produced by the API
      const match = /filename="([^"]*)"/.exec(jqXHR.getResponseHeader('Content-Disposition'));
      const link = $('<a />', {
        href: URL.createObjectURL(blob),
        download: match ? match[1] : 'export.csv',
      });
      $('body').append(link);
      link[0].click();
      link.remove();
    });
  });
  const yearlySuccesHandler = ({ data, selector } = {}) => {
    const keys = Object.keys(data); // Keys are Month-Year strings and values might be [0...N]
    if (keys.find(k => data[k] > 0)) {
      createChart({ selector, data });
    } else {
      $(selector).prev().show();
    }
  };
  // Sends an AJAX request to our two current endpoints that generate yearly data
  // (e.g. users_joined_api_v0_statistics_path, created_plans_api_v0_statistics_path )
  // and draws a barChart when success response is found
  const initialise = () => {
    // Only fire AJAX requests if topicToURL object has keys, i.e. topics mapping to URLs
    if (Object.keys(topicToURL).length > 0) {
      const rangeDates = rangeDatesUpToLastYearFromNow();
      $.ajax({
        headers: { Authorization: `Token token="${apiToken}"` },
        url: topicToURL.users,
        data: { range_dates: rangeDates },
      }).then((data) => {
        yearlySuccesHandler({ data, selector: '#yearly_users' });
      }); // TODO request error handling
      $.ajax({
        headers: { Authorization: `Token token="${apiToken}"` },
        url: topicToURL.plans,
        data: { range_dates: rangeDates },
      }).then((data) => {
        yearlySuccesHandler({ data, selector: '#yearly_plans' });
      }); // TODO request error handling
    }
  };
  initialise();
});

$(() => {
  const jQuerySelectorSelect = $('select[name=monthly_plans_by_template]');
  let drawnChart = null;
  const randomRgb = () => {
    const { round, random } = Math;
    const max = 255;
    const f = () => round(random() * max);
    return `rgb(${f()},${f()},${f()})`;
  };
  const yAxisLabel = date => moment(date).format('MMM-YY');

  const drawHorizontalBar = (canvasSelector, data, aspectRatio = 1) => {
    const chart = new Chart(canvasSelector, { // eslint-disable-line no-new
      type: 'horizontalBar',
      data,
      options: {
        responsive: true,
        maintainAspectRatio: true,
        aspectRatio,
        scales: {
          xAxes: [{
            position: 'top',
            ticks: { beginAtZero: true, stepSize: 10 },
          }],
        },
      },
    });
    return chart;
  };

  const buildData = (data) => {
    const labels = data.map(current => yAxisLabel(current.date));

    const datasetsMap = data.reduce((acc, statCreatedPlan) => {
      statCreatedPlan.by_template.forEach((template) => {
        if (!acc[template.name]) {
          acc[template.name] = { label: template.name, data: [], backgroundColor: randomRgb() };
        }
        acc[template.name].data.push({ x: template.count, y: yAxisLabel(statCreatedPlan.date) });
      });
      return acc;
    }, {});

    const datasets = Object.keys(datasetsMap).map(key => datasetsMap[key]);

    return { labels, datasets };
  };

  const fetch = (lastDayOfMonth, aspectRatio = 1) => {
    const baseUrl = $('select[name="monthly_plans_by_template"]').attr('data-url');
    $.ajax({
      url: `${baseUrl}?start_date=${lastDayOfMonth}`,
    }).then((data) => {
      const chartData = buildData(data);
      const canvasSelector = '#monthly_plans_by_template_canvas';
      if (drawnChart) {
        drawnChart.destroy();
      }
      drawnChart = drawHorizontalBar($(canvasSelector), chartData, aspectRatio);
    });
  };

  // Set Aspect Rate (width of X-axis/height of Y-axis) based on
  // choice of selectedLastDayOfMonth in Time picker string value.  Note aspect
  const getAspectRatio = (selectedLastDayOfMonth) => {
    let aspectRatio;
    try {
      const now = new Date();
      const dateOfSelectedMonth = new Date(selectedLastDayOfMonth);
      const diff = new Date(now.getTime() - dateOfSelectedMonth.getTime());
      const diffInMonths = diff.getUTCMonth();

      switch (diffInMonths) {
      case 0:
      case 1:
      case 2:
      case 3:
        aspectRatio = 4;
        break;
      case 4:
      case 5:
        aspectRatio = 3;
        break;
      case 7:
      case 8:
      case 9:
        aspectRatio = 2;
        break;
      default:
        aspectRatio = 1;
      }
    } catch (e) {
      aspectRatio = 1;
    }

    return aspectRatio;
  };

  const handler = () => {
    const selectedMonth = jQuerySelectorSelect.val();

    if (selectedMonth) {
      const aspectRatio = getAspectRatio(selectedMonth);
      fetch(selectedMonth, aspectRatio);
    }
  };

  jQuerySelectorSelect.on('change', (e) => {
    e.preventDefault();
    handler();
  });

  handler();
});