rubyforgood/casa

View on GitHub
app/javascript/src/display_app_metric.js

Summary

Maintainability
B
4 hrs
Test Coverage
import { Chart, registerables } from 'chart.js'
import 'chartjs-adapter-luxon'

const { Notifier } = require('./notifier')

Chart.register(...registerables)

const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']

$(() => { // JQuery's callback for the DOM loading
  const caseContactCreationTimesBubbleChart = document.getElementById('caseContactCreationTimeBubbleChart')
  const monthLineChart = document.getElementById('monthLineChart')
  const uniqueUsersMonthLineChart = document.getElementById('uniqueUsersMonthLineChart')

  const notificationsElement = $('#notifications')
  const pageNotifier = notificationsElement.length ? new Notifier(notificationsElement) : null

  if (caseContactCreationTimesBubbleChart) {
    fetchDataAndCreateChart('/health/case_contacts_creation_times_in_last_week', caseContactCreationTimesBubbleChart, function (data) {
      const timestamps = data.timestamps
      const graphData = formatTimestampsAsBubbleChartData(timestamps)
      createChart(caseContactCreationTimesBubbleChart, graphData)
    })
  }

  if (monthLineChart) {
    fetchDataAndCreateChart('/health/monthly_line_graph_data', monthLineChart, function (data) {
      console.log(data)
      createLineChartForCaseContacts(monthLineChart, data)
    })
  }

  if (uniqueUsersMonthLineChart) {
    fetchDataAndCreateChart('/health/monthly_unique_users_graph_data', uniqueUsersMonthLineChart, function (data) {
      console.log(data)
      createLineChartForUniqueUsersMonthly(uniqueUsersMonthLineChart, data)
    })
  }

  function fetchDataAndCreateChart (url, chartElement, successCallback) {
    $.ajax({
      type: 'GET',
      url,
      success: successCallback,
      error: handleAjaxError
    })
  }

  function handleAjaxError (xhr, status, error) {
    console.error('Failed to fetch data for case contact entry times chart display')
    console.error(error)
    pageNotifier?.notify('Failed to display metric chart. Check the console for error details.', 'error')
  }
})

function formatTimestampsAsBubbleChartData (timestamps) {
  const bubbleDataAsObject = {}

  for (const timestamp of timestamps) {
    const contactCreationTime = new Date(timestamp * 1000)
    const day = contactCreationTime.getDay()
    const hour = contactCreationTime.getHours()

    // Group case contacts with the same hour and day creation time into the same data point

    let dayData

    if (!(day in bubbleDataAsObject)) {
      dayData = {}
      bubbleDataAsObject[day] = dayData
    } else {
      dayData = bubbleDataAsObject[day]
    }

    if (!(hour in dayData)) {
      dayData[hour] = 1
    } else {
      dayData[hour]++
    }
  }

  // Flatten data points

  const bubbleDataAsArray = []

  for (const day in bubbleDataAsObject) {
    const hours = bubbleDataAsObject[day]

    for (const hour in hours) {
      bubbleDataAsArray.push({
        x: hour,
        y: day,
        r: Math.sqrt(hours[hour]) * 4
      })
    }
  }

  return bubbleDataAsArray
}

function createChart (chartElement, dataset) {
  const ctx = chartElement.getContext('2d')

  return new Chart(ctx, {
    type: 'bubble',
    data: {
      datasets: [
        {
          label: 'Case Contact Creation Times',
          data: dataset,
          backgroundColor: 'rgba(255, 99, 132, 0.2)',
          borderColor: 'rgba(255, 99, 132, 1)'
        }
      ]
    },
    options: {
      scales: {
        x: {
          min: 0,
          max: 23,
          ticks: {
            beginAtZero: true,
            stepSize: 1
          }
        },
        y: {
          min: 0,
          max: 6,
          ticks: {
            beginAtZero: true,
            callback: getYTickCallback,
            stepSize: 1
          }
        }
      },
      plugins: {
        legend: {
          display: false
        },
        title: {
          display: true,
          font: {
            size: 18
          },
          text: 'Case Contact Creation Times in the Past Week'
        },
        tooltip: {
          callbacks: {
            label: getTooltipLabelCallback
          }
        }
      }
    }
  })
}

function getYTickCallback (value) {
  return days[value]
}

function getTooltipLabelCallback (context) {
  const bubbleData = context.dataset.data[context.dataIndex]
  const caseContactCountSqrt = bubbleData.r / 4
  return `${Math.round(caseContactCountSqrt * caseContactCountSqrt)} case contacts created on ${days[bubbleData.y]} at ${bubbleData.x}:00`
}

function createLineChartForCaseContacts (chartElement, dataset) {
  const datasetLabels = ['Total Case Contacts', 'Total Case Contacts with Notes', 'Total Case Contact Users']
  return createLineChart(chartElement, dataset, 'Case Contact Creation', datasetLabels)
}

function createLineChartForUniqueUsersMonthly (chartElement, dataset) {
  const datasetLabels = ['Total Volunteers', 'Total Supervisors', 'Total Admins']
  return createLineChart(chartElement, dataset, 'Monthly Active Users', datasetLabels)
}

function createLineChart (chartElement, dataset, chartTitle, datasetLabels) {
  const ctx = chartElement.getContext('2d')
  const allMonths = extractChartData(dataset, 0)
  const datasets = []
  const colors = ['#308af3', '#48ba16', '#FF0000']

  for (let i = 1; i < dataset[0].length; i++) {
    const data = extractChartData(dataset, i)
    const label = datasetLabels[i - 1]
    const color = colors[i - 1]
    datasets.push(createLineChartDataset(label, data, color, color))
  }

  return new Chart(ctx, {
    type: 'line',
    data: {
      labels: allMonths,
      datasets
    },
    options: createChartOptions(chartTitle)
  })
}

function extractChartData (dataset, index) {
  return dataset.map(data => data[index])
}

function createLineChartDataset (label, data, borderColor, pointBackgroundColor) {
  return {
    label,
    data,
    fill: false,
    borderColor,
    pointBackgroundColor,
    pointBorderWidth: 2,
    pointHoverBackgroundColor: '#fff',
    pointHoverBorderWidth: 2,
    lineTension: 0.05
  }
}

function createChartOptions (label) {
  return {
    legend: { display: true },
    plugins: {
      legend: { display: true, position: 'bottom' },
      title: {
        display: true,
        font: { size: 18 },
        text: label
      },
      tooltips: {
        callbacks: {
          label: function (tooltipItem, data) {
            let label = data.datasets[tooltipItem.datasetIndex].label || ''
            if (label) {
              label += ': '
            }
            label += Math.round(tooltipItem.yLabel * 100) / 100
            return label
          }
        }
      }
    }
  }
}