Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tick stats #930

Merged
merged 1 commit into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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