kodadot/nft-gallery

View on GitHub
utils/chart.ts

Summary

Maintainability
A
0 mins
Test Coverage
import type { Point } from 'chart.js'

type ChartData = [Date, number][]

export type CollectionChartData = {
  date: Date
  value: bigint | number
  average?: bigint | number
  count: number
}

export type MappingFunction<T> = (item: CollectionChartData) => T
export const defaultMapper = (item: CollectionChartData) => item.value
export const mapToAverage = (item: CollectionChartData) =>
  item.average ?? BigInt(0)
export const mapToCount = (item: CollectionChartData) => item.count
export const getLabel = (item: CollectionChartData) => item.date

type MedianDetails = {
  median: number
  medianIndex: null | number
}
type RenderedChartData<T = bigint | number> = { x: Date, y: T }[]

interface HSpread {
  min: number
  max: number
  iqr: number
  q1: number
  q3: number
  sorted: number[]
}

const getMedianDetails = (dataset: number[]): MedianDetails => {
  const length = dataset.length
  const medianIndex = null
  if (length % 2 === 0) {
    return {
      median: (dataset[length / 2] + dataset[length / 2 - 1]) / 2,
      medianIndex,
    }
  }
  else {
    const median = dataset[Math.floor(length / 2)]
    return { median, medianIndex: Math.floor(length / 2) }
  }
}

export const getChartPoints = (data: ChartData = []): Point[] =>
  data.map(item => ({
    x: item[0].getTime(),
    y: item[1],
  }))

export const getChartDataByTimeRange = (data: ChartData, timeRange: number) => {
  if (!data) {
    return
  }
  if (timeRange === 0) {
    return data
  }
  else {
    const now = new Date()
    now.setDate(now.getDate() - timeRange)
    return data.filter(item => item[0] >= now)
  }
}

export const getCollectionChartData = (
  data: CollectionChartData[] = [],
  mapper = defaultMapper,
): RenderedChartData =>
  data.map(item => ({
    x: item.date,
    y: mapper(item),
    count: item.count,
  }))

export const getCollectionMedian = (data: CollectionChartData[] = []) => {
  const dataset = data
    .map(item => item.value)
    .map(Number)
    .sort((a, b) => b - a)
  const { median } = getMedianDetails(dataset)
  return median
}

export const getMedianPoint = (data: ChartData = []): number => {
  const dataset = data.map(item => item[1]).sort((a, b) => b - a)
  const { median } = getMedianDetails(dataset)
  return median
}

export const getMovingAverage = (data: RenderedChartData = []): number[] => {
  const dataset = data.map(({ y }) => y) as number[]
  const movingAverageArray: number[] = []
  const average = 3

  for (let i = 0; i < dataset.length - 2; i++) {
    const datapoints = dataset.slice(i, average + i)
    const movingAverage = datapoints.reduce((total, num) => total + num, 0) / 3
    movingAverageArray.push(movingAverage)
  }

  return movingAverageArray
}

// source: https://stackoverflow.com/a/64452666
// https://mathworld.wolfram.com/Outlier.html
export const getHSpread = (data): HSpread => {
  const values = data.concat().sort((a, b) => a - b)

  const quartile = (q) => {
    const sorted = values
    const pos = (sorted.length - 1) * q
    const base = Math.floor(pos)
    const rest = pos - base

    if (sorted[base + 1] !== undefined) {
      return sorted[base] + rest * (sorted[base + 1] - sorted[base])
    }

    return sorted[base]
  }

  const q1 = quartile(0.25)
  const q3 = quartile(0.75)
  const iqr = q3 - q1
  const max = q3 + iqr * 1.5
  const min = q1 - iqr * 1.5

  return { min, max, q1, q3, iqr, sorted: values }
}

export const filterOutliers = (data) => {
  const { min, max, sorted } = getHSpread(data)

  // Then filter anything beyond or beneath these values.
  const filteredValues = sorted.filter((x) => {
    return x <= max && x >= min
  })

  // Then return
  return filteredValues
}