Skip to content

Commit

Permalink
feat: add difficulty pyramid (#928)
Browse files Browse the repository at this point in the history
  • Loading branch information
vnugent authored Jul 19, 2023
1 parent d88f8cd commit 1f5d605
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 14 deletions.
17 changes: 17 additions & 0 deletions src/components/logbook/ChartsSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { TickType } from '../../js/types'
import DifficultyPyramid from './DifficultyPyramid'
import OverviewChart from './OverviewChart'

export interface ChartsSectionProps {
tickList: TickType[]
}
const ChartsSection: React.FC<ChartsSectionProps> = ({ tickList }) => {
return (
<section className='flex flex-col gap-6'>
<OverviewChart tickList={tickList} />
<DifficultyPyramid tickList={tickList} />
</section>
)
}

export default ChartsSection
91 changes: 91 additions & 0 deletions src/components/logbook/DifficultyPyramid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, CartesianGrid } from 'recharts'
import { getScale } from '@openbeta/sandbag'

import { TickType } from '../../js/types'
import { maxSorted } from 'simple-statistics'
/**
* Assume grades are YDS or Vscale for now since we don't store
* grade context with ticks, nor do we have a way to get score
* without knowing the grade system
*/
export const ydsScale = getScale('yds')
export const vScale = getScale('vscale')

interface DifficultyPyramidProps {
tickList: TickType[]
}

const DifficultyPyramid: React.FC<DifficultyPyramidProps> = ({ tickList }) => {
const gradeHistogram = new Map<number, number>()

if (tickList == null || tickList.length < 1) return null

tickList.forEach((tick) => {
const score = getScoreUSAForRouteAndBoulder(tick.grade)
if (score > 0) {
const count = gradeHistogram.get(score)
if (count == null) {
gradeHistogram.set(score, 0)
} else {
gradeHistogram.set(score, count + 1)
}
}
})

const sortedKeys = Array.from(gradeHistogram.keys()).sort((a, b) => a - b)
const sortedValues = Array.from(gradeHistogram.values()).sort((a, b) => a - b)
const yOffset = maxSorted(sortedValues)

const chartData = sortedKeys.map(key => {
const value = gradeHistogram.get(key) ?? 0
return ({
x: key,
xBottom: key,
hackRange: [value + yOffset, -value + yOffset]
})
})
return (
<div className='w-full'>
<h3 className='ml-16 py-4'>
Difficulty Pyramid
</h3>
<ResponsiveContainer height={300}>
<AreaChart data={chartData} margin={{ right: 80 }}>
<CartesianGrid stroke='#f5f5f5' />

<XAxis
orientation='bottom'
dataKey='xBottom'
tick={{ fontSize: '10' }}
tickFormatter={(value) => {
if (value == null) return ''
const yds = ydsScale?.getGrade(parseInt(value)) ?? ''
const vscale = vScale?.getGrade(parseInt(value)) ?? ''
return `${yds}/${vscale}`
}}
/>

<YAxis
// tickCount={8}
tickFormatter={(value) => {
const actual = parseInt(value) - yOffset
return `${actual > 0 ? actual : ''}`
}}
/>

<Area type='basis' stroke='none' dataKey='hackRange' fillOpacity={1} fill='rgb(6 182 212)' />
</AreaChart>
</ResponsiveContainer>
</div>
)
}

export default DifficultyPyramid

const getScoreUSAForRouteAndBoulder = (grade: string): number => {
let score = ydsScale?.getScore(grade)[0] as number ?? -1
if (score < 0) {
score = vScale?.getScore(grade)[0] as number ?? -1
}
return score
}
13 changes: 5 additions & 8 deletions src/components/logbook/OverviewChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import {
} from 'recharts'
import { groupBy } from 'underscore'
import { lastDayOfMonth, format } from 'date-fns'
import { getScale } from '@openbeta/sandbag'
import { linearRegression, linearRegressionLine, minSorted, maxSorted, medianSorted } from 'simple-statistics'

import { TickType } from '../../js/types'
import { ydsScale, vScale } from './DifficultyPyramid'

export interface OverviewChartProps {
tickList: TickType[]
Expand All @@ -17,13 +17,7 @@ export interface OverviewChartProps {
* Proof of concept chart showing climbs aggregated by a time interval
*/
const OverviewChart: React.FC<OverviewChartProps> = ({ tickList }) => {
/**
* Assume grades are YDS or Vscale for now since we don't store
* grade context with ticks, nor do we have a way to get score
* without knowing the grade system
*/
const ydsScale = getScale('yds')
const vScale = getScale('vscale')
if (tickList == null || tickList.length < 1) return null

const agg = groupBy(tickList, getYearMonthFromDate)

Expand Down Expand Up @@ -72,6 +66,9 @@ const OverviewChart: React.FC<OverviewChartProps> = ({ tickList }) => {

return (
<div className='w-full'>
<h3 className='ml-16 py-4'>
Climb History
</h3>
<ResponsiveContainer height={350}>
<ComposedChart
data={chartData2}
Expand Down
11 changes: 5 additions & 6 deletions src/pages/u2/[...slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import dynamic from 'next/dynamic'
import Link from 'next/link'
import { getTicksByUser } from '../../js/graphql/api'
import { TickType } from '../../js/types'
import { OverviewChartProps } from '../../components/logbook/OverviewChart'
import ImportFromMtnProj from '../../components/users/ImportFromMtnProj'
import Layout from '../../components/layout'
import { ChartsSectionProps } from '../../components/logbook/ChartsSection'

interface TicksIndexPageProps {
username: string
Expand All @@ -27,9 +27,8 @@ const Index: NextPage<TicksIndexPageProps> = ({ username, ticks }) => {
contentContainerClass='content-default with-standard-y-margin'
showFilterBar={false}
>
<section className='w-full pt-6'>
<DynamicOverviewChart tickList={ticks} />
</section>
<ChartsSection tickList={ticks} />

<section className='max-w-lg mx-auto w-full px-4 py-8'>
<h2>{username}</h2>
<div className='py-4 flex items-center gap-6'>
Expand Down Expand Up @@ -88,8 +87,8 @@ export const getStaticProps: GetStaticProps<TicksIndexPageProps, {slug: string[]
}
}

const DynamicOverviewChart = dynamic<OverviewChartProps>(
const ChartsSection = dynamic<ChartsSectionProps>(
async () =>
await import('../../components/logbook/OverviewChart').then(
await import('../../components/logbook/ChartsSection').then(
module => module.default), { ssr: false }
)

1 comment on commit 1f5d605

@vercel
Copy link

@vercel vercel bot commented on 1f5d605 Jul 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.