Skip to content

Commit

Permalink
feat: add tick stats (#930)
Browse files Browse the repository at this point in the history
  • Loading branch information
vnugent authored Jul 20, 2023
1 parent a9e8066 commit 5b3de76
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 34 deletions.
6 changes: 4 additions & 2 deletions src/components/logbook/ChartsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { TickType } from '../../js/types'
import DifficultyPyramid from './DifficultyPyramid'
import OverviewChart from './OverviewChart'

import Stats from './Stats'
export interface ChartsSectionProps {
tickList: TickType[]
}
const ChartsSection: React.FC<ChartsSectionProps> = ({ tickList }) => {
const sortedList = tickList.sort((a, b) => a.dateClimbed - b.dateClimbed)
return (
<section className='flex flex-col gap-6'>
<OverviewChart tickList={tickList} />
<Stats tickList={sortedList} />
<OverviewChart tickList={sortedList} />
<DifficultyPyramid tickList={tickList} />
</section>
)
Expand Down
17 changes: 9 additions & 8 deletions src/components/logbook/DifficultyPyramid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,17 @@ const DifficultyPyramid: React.FC<DifficultyPyramidProps> = ({ tickList }) => {
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}`
}}
tickFormatter={tickFormatScoreToYdsVscale}
/>

<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)' />
<Area type='step' stroke='none' dataKey='hackRange' fillOpacity={1} fill='rgb(6 182 212)' />
</AreaChart>
</ResponsiveContainer>
</div>
Expand All @@ -89,3 +83,10 @@ const getScoreUSAForRouteAndBoulder = (grade: string): number => {
}
return score
}

export const tickFormatScoreToYdsVscale = (value: string): string => {
if (value == null) return ''
const yds = ydsScale?.getGrade(parseInt(value)) ?? ''
const vscale = vScale?.getGrade(parseInt(value)) ?? ''
return `${yds}/${vscale}`
}
19 changes: 9 additions & 10 deletions src/components/logbook/OverviewChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { lastDayOfMonth, format } from 'date-fns'
import { linearRegression, linearRegressionLine, minSorted, maxSorted, medianSorted } from 'simple-statistics'

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

export interface OverviewChartProps {
tickList: TickType[]
Expand All @@ -23,7 +23,7 @@ const OverviewChart: React.FC<OverviewChartProps> = ({ tickList }) => {

const xyRegressionData: number[][] = []

const chartData: ChartDataPayloadProps[] = Object.entries(agg).reverse().map(value => {
const chartData: ChartDataPayloadProps[] = Object.entries(agg).map(value => {
const x = parseInt(value[0])
const gradeScores = value[1].reduce<number[]>((acc, curr) => {
let score = ydsScale?.getScore(curr.grade)?.[0] as number ?? -1
Expand Down Expand Up @@ -73,12 +73,11 @@ const OverviewChart: React.FC<OverviewChartProps> = ({ tickList }) => {
<ComposedChart
data={chartData2}
syncId='overviewChart'
margin={{ left: 0, right: 0 }}
>
<CartesianGrid stroke='#f5f5f5' />
<YAxis
yAxisId='score' stroke='rgb(15 23 42)' tickFormatter={(value) => {
return parseInt(value) <= 0 ? ' ' : value
}}
yAxisId='score' stroke='rgb(15 23 42)' tick={{ fontSize: '10' }} tickFormatter={tickFormatScoreToYdsVscale}
/>

<Line
Expand Down Expand Up @@ -162,8 +161,8 @@ const CustomizeMedianDot: React.FC<LineProps & { payload?: ChartDataPayloadProps
strokeLinecap='round'
/>
<line
x1={cx as number - 5} y1={cy} x2={cx as number + 5} y2={cy}
stroke='rgb(190 24 93)'
x1={cx as number - 6} y1={cy} x2={cx as number + 6} y2={cy}
stroke='rgb(15 23 42)'
strokeWidth={2}
/>
</>
Expand All @@ -175,9 +174,9 @@ const CustomTooltip: React.FC<any> = ({ active, payload, label }) => {
return (
<div className='bg-info p-4 rounded-btn'>
<div>Total climbs: <span className='font-semibold'>{payload[4].value}</span></div>
<div>Median: {payload[0].value}</div>
<div>Low: {payload[1].value}</div>
<div>High: {payload[2].value}</div>
<div>Median: {tickFormatScoreToYdsVscale(payload[0].value)}</div>
<div>Low: {tickFormatScoreToYdsVscale(payload[1].value)}</div>
<div>High: {tickFormatScoreToYdsVscale(payload[2].value)}</div>
</div>
)
}
Expand Down
70 changes: 70 additions & 0 deletions src/components/logbook/Stats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { groupBy } from 'underscore'
import { formatDistanceToNowStrict, format, endOfDay, differenceInCalendarDays } from 'date-fns'
import { TickType } from '../../js/types'

const Stats: React.FC<{ tickList: TickType[]}> = ({ tickList }) => {
const sortedList = tickList
const total = tickList.length
const totalTime = formatDistanceToNowStrict(sortedList[0].dateClimbed)

const dayMap = groupBy(sortedList, getEndOfDay)
const climbingDays = Object.keys(dayMap).length

const longestStreak = calculateLongestStreak(sortedList)
return (
<div className='stats stats-vertical lg:stats-horizontal shadow my-12 mx-4 lg:mx-16'>

<div className='stat place-items-center'>
<div className='stat-title'>Total</div>
<div className='stat-value'>{total}</div>
<div className='stat-desc'>sends</div>

</div>

<div className='stat place-items-center'>
<div className='stat-title'>Time</div>
<div className='stat-value'>{totalTime}</div>
<div className='stat-desc'>since {format(sortedList[0].dateClimbed, 'MMMM dd, yyyy')} </div>
</div>

<div className='stat place-items-center'>
<div className='stat-title'>Climbing days</div>
<div className='stat-value'>{climbingDays}</div>
<div className='stat-desc'>&nbsp;</div>
</div>

<div className='stat place-items-center'>
<div className='stat-title'>Longest streak</div>
<div className='stat-value'>{longestStreak}</div>
<div className='stat-desc'>consecutive days</div>
</div>
</div>
)
}

export default Stats

const getEndOfDay = (entry: TickType): number => endOfDay(entry.dateClimbed).getTime()

export const calculateLongestStreak = (sortedList: TickType[]): number | null => {
const streakSet = new Set<number>()
let longestStreak: Date[] = []
for (let i = 0; i < sortedList.length; i++) {
const today = new Date(sortedList[i].dateClimbed)
if (i === sortedList.length - 1) {
longestStreak.push(today)
break
}
const nextDay = sortedList[i + 1].dateClimbed
if (differenceInCalendarDays(nextDay, today) === 1) {
longestStreak.push(today)
} else {
if (longestStreak.length > 0) {
streakSet.add(longestStreak.length + 1)
}
longestStreak = []
}
}
const list = Array.from(streakSet.keys()).sort((a, b) => b - a)
return list.length === 0 ? null : list[0]
}
35 changes: 21 additions & 14 deletions src/pages/u2/[...slug].tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'

import { useRouter } from 'next/router'
import { NextPage, GetStaticProps } from 'next'
import dynamic from 'next/dynamic'
import Link from 'next/link'
Expand All @@ -22,26 +22,33 @@ interface TicksIndexPageProps {
* - Incrementally adopt nested layout https://nextjs.org/blog/layouts-rfc
*/
const Index: NextPage<TicksIndexPageProps> = ({ username, ticks }) => {
const { isFallback } = useRouter()

return (
<Layout
contentContainerClass='content-default with-standard-y-margin'
showFilterBar={false}
>
<ChartsSection tickList={ticks} />
{isFallback
? <div className='h-screen'>Loading...</div>
: (
<>
<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'>
<ImportFromMtnProj isButton username={username} />
<a className='btn btn-xs md:btn-sm btn-outline' href={`/u/${username}`}>Classic Profile</a>
</div>
<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'>
<ImportFromMtnProj isButton username={username} />
<a className='btn btn-xs md:btn-sm btn-outline' href={`/u/${username}`}>Classic Profile</a>
</div>

<h3 className='py-4'>Log book</h3>
<div>
{ticks?.map(Tick)}
{ticks?.length === 0 && <div>No ticks</div>}
</div>
</section>
<h3 className='py-4'>Log book</h3>
<div>
{ticks?.map(Tick)}
{ticks?.length === 0 && <div>No ticks</div>}
</div>
</section>
</>)}
</Layout>
)
}
Expand Down

1 comment on commit 5b3de76

@vercel
Copy link

@vercel vercel bot commented on 5b3de76 Jul 20, 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.