From a65858af84e18c271e7c92443972d08e83988a77 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Wed, 12 Aug 2026 11:17:00 +0200 Subject: [PATCH] =?UTF-8?q?Statistikk:=20line=C3=A6r=20regresjon=20i=20ste?= =?UTF-8?q?det=20for=20glidende=20snitt,=20trend=20p=C3=A5=20alle=20variab?= =?UTF-8?q?ler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brukeren påpekte (med rette) at det glidende 3-runde-snittet i "Utvikling" bare gjentok stolpediagrammet med én runde forsinkelse -- for få punkter til at det smoothet noe reelt. Foreslo tre alternativer via AskUserQuestion (lineær regresjon / eksponentielt glidende snitt / kumulativt løpende snitt); brukeren valgte lineær regresjon. Lineær regresjon (minste kvadraters metode) gir en rett linje -- stigningstallet leses direkte som "endring per runde" og vises nå i bildeteksten (f.eks. "+7 pp per runde"). Visuelt en helt annen form enn stolpene (rett strek vs. hakkete stolper), som løser selve klagen. Bruker ba samtidig om trend på ALLE variabler, ikke bare snitt til par. Lagt til stolpe+trend for: Fairwaytreff, Greentreff (GIR), Putt/18, Én-putt, Chip/runde, Bunkerslag/runde, Straffeslag/runde, Anywayslag/ runde -- hver plassert i det eksisterende kortet for akkurat den metrikken. Scrambling/sand save er unntaket (brukervalg via AskUserQuestion): for få kvalifiserte hull per runde (0-2) til at et per-runde-tall er meningsfullt, vises i stedet som én kumulativ linje ("raten din til og med hver runde"), ingen stolper. Backend: RoundStatsSummary sitt gamle round_series-felt (kun snitt til par) erstattet av et nytt round_series på RoundStatsWindowSummary-nivå, med per-runde-tall for alle variabler pluss kumulativ scrambling/sand save. Fanget og rettet en enhets-bug selv under scratch-verifisering: scrambling/sand save ble først bygget som 0-1-brøker (kopiert fra en boolsk 1.0/0.0-sjekk) i stedet for samme 0-100-skala som resten av API-et -- rettet før commit. Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester. Full scratch-stack med åtte syntetiske runder (bevisst støyende fremgang, for å faktisk se at regresjonslinjen skiller seg fra stolpene) browser- verifisert lys+mørk: regresjonslinje bekreftet rett og visuelt distinkt fra stolpene på alle variabler, riktig fortegn/farge på stigningstall, ingen dobbel enhet i bildetekstene, kumulative linjer for scrambling/ sand save konvergerer som forventet. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 --- app/routers/rounds.py | 111 ++++++- frontend/components/rounds-stats-summary.tsx | 332 ++++++++++++++++--- 2 files changed, 383 insertions(+), 60 deletions(-) diff --git a/app/routers/rounds.py b/app/routers/rounds.py index 63c4c93..a20c8fc 100644 --- a/app/routers/rounds.py +++ b/app/routers/rounds.py @@ -1660,18 +1660,30 @@ async def list_rounds(user: CurrentUser = Depends(get_current_user)) -> list[Rou class RoundStatsSeriesPoint(BaseModel): played_at: date score_to_par: float + # Per-runde-tall for stolpe+trendlinje-grafene (bruker-instruks + # 2026-08-12: "trender på alle variablene"). Samme filtrering/formler + # som de tilsvarende pooled-tallene i RoundStatsSummary, bare regnet + # PER runde i stedet for poolet over alle runder i vinduet -- None der + # runden ikke har noe kvalifisert hull for akkurat den metrikken. + putts_per_18: float | None + fairway_hit_pct: float | None + gir_pct: float | None + one_putt_pct: float | None + chip_per_round: float | None + bunker_per_round: float | None + penalty_per_round: float | None + anyway_per_round: float | None + # Scrambling/sand save er KUMULATIVE (poolet over alle runder til og + # med denne), ikke per-runde -- en enkelt runde har ofte 0-2 + # kvalifiserte hull (bom green / i bunker), for lite til et eget + # per-runde-tall (brukervalg 2026-08-12, se AskUserQuestion-svar). + scrambling_cumulative_pct: float | None + sand_save_cumulative_pct: float | None class RoundStatsSummary(BaseModel): rounds_completed: int avg_score_to_par: float | None - # Kronologisk (eldste først) snitt-til-par per fullført runde i - # perioden -- til stolpegraf-med-trendlinje på frontend (bruker- - # instruks 2026-08-12). Kun for `current`-vinduet (se - # get_rounds_stats_summary); `previous` trenger ikke egen serie siden - # grafen bare viser inneværende periodes utvikling, ikke en - # sammenligning mot forrige periode slik de andre tallene på siden gjør. - round_series: list[RoundStatsSeriesPoint] avg_putts_per_18: float | None putts_tracked_rounds: int fairway_hit_pct: float | None @@ -1714,6 +1726,11 @@ class RoundStatsWindowSummary(BaseModel): window: str current: RoundStatsSummary previous: RoundStatsSummary + # Kronologisk (eldste først) per-runde-serie for stolpe+trendlinje- + # grafene (brukerinstruks 2026-08-12). Kun for `current`-vinduet -- + # grafene viser inneværende periodes utvikling, ikke en sammenligning + # mot forrige periode slik de andre tallene på siden gjør. + round_series: list[RoundStatsSeriesPoint] StatsWindow = Literal["last_round", "last_5", "last_10", "month", "year", "last_year", "all"] @@ -1758,6 +1775,77 @@ def _resolve_stats_window(window: StatsWindow, ordered_rounds: list[tuple[str, d return current, previous +def _build_round_series( + round_ids: set[str], + by_round: dict[str, list], + stat_level_by_round: dict[str, str], + played_at_by_round: dict[str, date], +) -> list[RoundStatsSeriesPoint]: + """Kronologisk (eldste først) per-runde-serie for stolpe+trendlinje- + grafene. Samme filtre/formler som de pooled tallene i + `_summarize_rounds`, bare regnet PER runde -- unntatt scrambling/sand + save, som er bevisst KUMULATIVE (se `RoundStatsSeriesPoint`).""" + + def avg(values: list[float]) -> float | None: + return sum(values) / len(values) if values else None + + def is_gir(h) -> bool: + return h["score"] - h["putts"] <= h["par"] - 2 + + ordered = sorted(round_ids, key=lambda rid: played_at_by_round[rid]) + points: list[RoundStatsSeriesPoint] = [] + cum_scrambling: list[float] = [] + cum_sand_save: list[float] = [] + + for rid in ordered: + holes = by_round.get(rid, []) + played = [h for h in holes if h["played"] and h["score"] is not None] + if not played: + continue + + putts_per_18 = None + if stat_level_by_round.get(rid) != "strokes_only": + putts_per_18 = float(sum(h["putts"] if h["putts"] is not None else 2 for h in holes)) + + fairway_tracked = [h for h in played if h["par"] != 3 and h["tee_shot_result"] is not None] + fairway_hit_pct = avg([100.0 if h["tee_shot_result"] == "fairway" else 0.0 for h in fairway_tracked]) + + gir_eligible = [h for h in played if h["putts"] is not None] + gir_pct = avg([100.0 if is_gir(h) else 0.0 for h in gir_eligible]) + one_putt_pct = avg([100.0 if h["putts"] <= 1 else 0.0 for h in gir_eligible]) + + def summed(field: str) -> float | None: + eligible = [h[field] for h in played if h[field] is not None] + return float(sum(eligible)) if eligible else None + + # Utvider den kumulative poolen med DENNE rundens hull FØR prosenten + # regnes ut, slik at tallet ved runde N er "raten din til og med + # runde N" -- se RoundStatsSeriesPoint for hvorfor disse to alene + # er kumulative og ikke per-runde. + gir_missed_this_round = [h for h in gir_eligible if not is_gir(h)] + cum_scrambling.extend([100.0 if (h["score"] - h["par"]) <= 0 else 0.0 for h in gir_missed_this_round]) + sand_this_round = [h for h in gir_missed_this_round if h["bunker_shot_count"] is not None and h["bunker_shot_count"] >= 1] + cum_sand_save.extend([100.0 if (h["score"] - h["par"]) <= 0 else 0.0 for h in sand_this_round]) + + points.append( + RoundStatsSeriesPoint( + played_at=played_at_by_round[rid], + score_to_par=float(sum(h["score"] - h["par"] for h in played)), + putts_per_18=putts_per_18, + fairway_hit_pct=fairway_hit_pct, + gir_pct=gir_pct, + one_putt_pct=one_putt_pct, + chip_per_round=summed("chip_count"), + bunker_per_round=summed("bunker_shot_count"), + penalty_per_round=summed("penalty_strokes"), + anyway_per_round=summed("anyway_strokes"), + scrambling_cumulative_pct=avg(cum_scrambling) if cum_scrambling else None, + sand_save_cumulative_pct=avg(cum_sand_save) if cum_sand_save else None, + ) + ) + return points + + def _summarize_rounds( round_ids: set[str], by_round: dict[str, list], @@ -1773,18 +1861,13 @@ def _summarize_rounds( to_par_values: list[float] = [] putt_totals: list[float] = [] - round_series: list[RoundStatsSeriesPoint] = [] for round_id in round_ids: holes = by_round.get(round_id, []) played = [h for h in holes if h["played"] and h["score"] is not None] if played: - round_to_par = float(sum(h["score"] - h["par"] for h in played)) - to_par_values.append(round_to_par) - round_series.append(RoundStatsSeriesPoint(played_at=played_at_by_round[round_id], score_to_par=round_to_par)) + to_par_values.append(float(sum(h["score"] - h["par"] for h in played))) if stat_level_by_round.get(round_id) != "strokes_only": putt_totals.append(sum(h["putts"] if h["putts"] is not None else 2 for h in holes)) - # Eldste først -- stolpegrafen på frontend viser utvikling venstre->høyre. - round_series.sort(key=lambda pt: pt.played_at) all_played = [h for rid in round_ids for h in by_round.get(rid, []) if h["played"] and h["score"] is not None] @@ -1856,7 +1939,6 @@ def _summarize_rounds( return RoundStatsSummary( rounds_completed=len(round_ids), avg_score_to_par=avg(to_par_values), - round_series=round_series, avg_putts_per_18=avg(putt_totals), putts_tracked_rounds=len(putt_totals), fairway_hit_pct=fairway_hit_pct, @@ -1949,6 +2031,7 @@ async def get_rounds_stats_summary( window=window, current=_summarize_rounds(current_ids, by_round, stat_level_by_round, played_at_by_round), previous=_summarize_rounds(previous_ids, by_round, stat_level_by_round, played_at_by_round), + round_series=_build_round_series(current_ids, by_round, stat_level_by_round, played_at_by_round), ) diff --git a/frontend/components/rounds-stats-summary.tsx b/frontend/components/rounds-stats-summary.tsx index ea00eb1..0ba527b 100644 --- a/frontend/components/rounds-stats-summary.tsx +++ b/frontend/components/rounds-stats-summary.tsx @@ -23,12 +23,24 @@ import { useRouter } from "next/navigation" import Link from "next/link" import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, BarChart3, Flag, TrendingDown, TrendingUp, Waves, Wind } from "lucide-react" -type ApiRoundSeriesPoint = { played_at: string; score_to_par: number } +type ApiRoundSeriesPoint = { + played_at: string + score_to_par: number + putts_per_18: number | null + fairway_hit_pct: number | null + gir_pct: number | null + one_putt_pct: number | null + chip_per_round: number | null + bunker_per_round: number | null + penalty_per_round: number | null + anyway_per_round: number | null + scrambling_cumulative_pct: number | null + sand_save_cumulative_pct: number | null +} type ApiStatsSummary = { rounds_completed: number avg_score_to_par: number | null - round_series: ApiRoundSeriesPoint[] avg_putts_per_18: number | null putts_tracked_rounds: number fairway_hit_pct: number | null @@ -66,6 +78,7 @@ type ApiWindowSummary = { window: string current: ApiStatsSummary previous: ApiStatsSummary + round_series: ApiRoundSeriesPoint[] } type WindowKey = "last_round" | "last_5" | "last_10" | "month" | "year" | "last_year" | "all" @@ -89,6 +102,17 @@ function pct(n: number): string { return `${Math.round(n)} %` } +// Enhetsløs versjon av `pct` -- til bruk som `formatSlope` i BarTrendChart +// der `slopeSuffix` allerede bærer enheten ("pp per runde"); `pct` sin +// egen "%" ville da kollidert med det. +function roundedNoUnit(n: number): string { + return String(Math.round(n)) +} + +function oneDecimal(n: number): string { + return n.toFixed(1).replace(".", ",") +} + // Prosentpoeng-differanse mellom to andeler (ikke "N % endring", men "N pp"). function signedPoints(n: number): string { const rounded = Math.round(n) @@ -382,30 +406,49 @@ function FairwayBar({ ) } -// --- Utvikling over tid: stolpegraf + trendlinje ----------------------- +// --- Utvikling over tid: stolpegraf + trendlinje, per variabel --------- // Brukerinstruks 2026-08-12: stolpe per runde (eldste til venstre) + -// trendlinje over. Tidligere bevisst UTSATT (se CHANGELOG 2026-07-28: "for -// lite rundehistorikk til å vise noe meningsfullt") -- bygges nå som -// brukeren har nok fullførte runder til at en trend faktisk sier noe. +// trendlinje, for ALLE variabler -- ikke bare snitt til par. Tidligere +// bevisst UTSATT (se CHANGELOG 2026-07-28: "for lite rundehistorikk til +// å vise noe meningsfullt"), bygget da brukeren fikk nok runder. // -// Fargevalg følger samme diverging-par som `Delta` allerede bruker på -// resten av siden (primary = under/på par, brand-orange = over par) -- -// ikke en ny palett, samme etablerte semantikk. Trendlinjen er et -// glidende snitt (ikke rå lineær regresjon), tegnet i en nøytral, -// dempet farge og stiplet, slik at den tydelig leses som UTLEDET, ikke -// som en tredje rådataserie. +// Trendlinjen er LINEÆR REGRESJON (minste kvadraters metode), ikke et +// glidende snitt -- brukeren påpekte (med rette) at et glidende snitt +// over så få runder bare gjentar stolpene med én runde forsinkelse. En +// rett regresjonslinje er visuelt og matematisk en helt annen form (en +// retning, ikke en kopi av dataene), og stigningstallet kan leses rett +// ut som "endring per runde" (se AskUserQuestion-svar 2026-08-12). +// +// Fargevalg: KUN snitt til par har en meningsfull 0-linje (par) å være +// diverging rundt (primary = på/under par, brand-orange = over par, +// samme par som `Delta` allerede bruker). De andre variablene er ren +// magnitude (prosent eller antall, alltid >= 0) og får én gjennomgående +// farge -- ingen kunstig "godt/dårlig"-deling der en slik deling ikke +// finnes i selve tallet. const TREND_BAR_WIDTH = 20 const TREND_BAR_GAP = 10 const TREND_CHART_HEIGHT = 160 const TREND_HALF_HEIGHT = TREND_CHART_HEIGHT / 2 - 14 // plass til dato-etiketter under +const TREND_MAGNITUDE_HEIGHT = TREND_CHART_HEIGHT - 34 // baseline nederst, samme topp-marg -function movingAverage(values: number[], window: number): number[] { - return values.map((_, i) => { - const start = Math.max(0, i - window + 1) - const slice = values.slice(start, i + 1) - return slice.reduce((sum, v) => sum + v, 0) / slice.length +// Lineær regresjon (minste kvadraters metode) -- returnerer den rette +// linjens y-verdi ved hver indeks. Rendret som polyline blir dette +// bokstavelig talt en rett strek, ikke en linje som følger dataene. +function linearRegression(values: number[]): number[] { + const n = values.length + if (n === 0) return [] + const xMean = (n - 1) / 2 + const yMean = values.reduce((a, b) => a + b, 0) / n + let num = 0 + let den = 0 + values.forEach((y, x) => { + num += (x - xMean) * (y - yMean) + den += (x - xMean) ** 2 }) + const slope = den === 0 ? 0 : num / den + const intercept = yMean - slope * xMean + return values.map((_, x) => intercept + slope * x) } function formatShortDate(iso: string): string { @@ -431,63 +474,97 @@ function barPath(x: number, valueY: number, baselineY: number, width: number, ra return `M ${x} ${top} H ${x + width} V ${bottom - radius} A ${radius} ${radius} 0 0 1 ${x + width - radius} ${bottom} H ${x + radius} A ${radius} ${radius} 0 0 1 ${x} ${bottom - radius} Z` } -function ScoreTrendChart({ series }: { series: ApiRoundSeriesPoint[] }) { - if (series.length < 2) return null +type TrendPoint = { played_at: string; value: number } + +function BarTrendChart({ + points, + mode, + formatValue, + formatSlope = formatValue, + goodDirection, + slopeSuffix, +}: { + points: TrendPoint[] + // "diverging": symmetrisk 0-linje midt i grafen, farge følger fortegn + // (kun snitt til par). "magnitude": baseline nederst, alltid >= 0, én farge. + mode: "diverging" | "magnitude" + // Brukt på hver stolpes verdi-etikett/tooltip -- kan trygt ha egen enhet + // (f.eks. "%") siden den står alene der. + formatValue: (v: number) => string + // Brukt KUN på stigningstallet i bildeteksten -- egen formatterer der + // formatValue sin enhet ville kollidert med slopeSuffix (f.eks. "52 %" + // + "pp per runde" ville blitt "52 % pp per runde"). Faller tilbake til + // formatValue når det ikke kolliderer (f.eks. `signed`, som ikke har + // noen enhet innebygd). + formatSlope?: (v: number) => string + goodDirection: "up" | "down" + // Enhet på stigningstallet i bildeteksten, f.eks. "per runde" eller "pp per runde". + slopeSuffix: string +}) { + if (points.length < 2) return null + + const values = points.map((p) => p.value) + const trend = linearRegression(values) + const slopePerRound = points.length > 1 ? trend[trend.length - 1] - trend[0] : 0 + const avgSlopePerRound = slopePerRound / (points.length - 1) + const improving = goodDirection === "up" ? avgSlopePerRound > 0 : avgSlopePerRound < 0 - const values = series.map((p) => p.score_to_par) - const trend = movingAverage(values, 3) - const maxAbs = Math.max(...values.map((v) => Math.abs(v)), 1) - const scale = TREND_HALF_HEIGHT / maxAbs const baselineY = TREND_CHART_HEIGHT - 20 - const width = series.length * (TREND_BAR_WIDTH + TREND_BAR_GAP) + TREND_BAR_GAP + let valueToY: (v: number) => number + if (mode === "diverging") { + const maxAbs = Math.max(...values.map((v) => Math.abs(v)), 1) + const scale = TREND_HALF_HEIGHT / maxAbs + valueToY = (v) => baselineY - v * scale + } else { + const maxVal = Math.max(...values, 1) + const scale = TREND_MAGNITUDE_HEIGHT / maxVal + valueToY = (v) => baselineY - v * scale + } + const width = points.length * (TREND_BAR_WIDTH + TREND_BAR_GAP) + TREND_BAR_GAP const barCenterX = (i: number) => TREND_BAR_GAP + i * (TREND_BAR_WIDTH + TREND_BAR_GAP) + TREND_BAR_WIDTH / 2 - const valueToY = (v: number) => baselineY - v * scale - - const trendPoints = trend.map((v, i) => `${barCenterX(i)},${valueToY(v)}`).join(" ") + const trendLinePoints = trend.map((v, i) => `${barCenterX(i)},${valueToY(v)}`).join(" ") return ( - +
= 0 ? "+" : "−"}${formatSlope(Math.abs(avgSlopePerRound))} ${slopeSuffix}.`} viewBox={`0 0 ${width} ${TREND_CHART_HEIGHT}`} width={width} height={TREND_CHART_HEIGHT} className="block" > - {/* 0-linjen (par) -- hårfin, ubrutt, tilbaketrukket, som resten av rutenettet skal være. */} - {series.map((point, i) => { + {points.map((point, i) => { const cx = barCenterX(i) - const overPar = point.score_to_par > 0 - const isLast = i === series.length - 1 + const overPar = mode === "diverging" && point.value > 0 + const isLast = i === points.length - 1 return ( - {formatShortDate(point.played_at)}: {signed(point.score_to_par)} + {formatShortDate(point.played_at)}: {formatValue(point.value)} {/* Direkte etikett kun på siste (nyeste) stolpe -- dataviz-prinsipp - "merk endepunktet, ikke hvert punkt". Resten leses via 0-linjen, - stolpenes relative høyde, og trendlinjens retning. */} + "merk endepunktet, ikke hvert punkt". */} {isLast && ( - {signed(point.score_to_par)} + {formatValue(point.value)} )} - {(i === 0 || i === series.length - 1 || i === Math.floor(series.length / 2)) && ( + {(i === 0 || i === points.length - 1 || i === Math.floor(points.length / 2)) && ( {formatShortDate(point.played_at)} @@ -496,19 +573,95 @@ function ScoreTrendChart({ series }: { series: ApiRoundSeriesPoint[] }) { ) })} - +
-
- +
+ + + {avgSlopePerRound >= 0 ? "+" : "−"} + {formatSlope(Math.abs(avgSlopePerRound))} {slopeSuffix}
+
+ ) +} + +function ScoreTrendChart({ series }: { series: ApiRoundSeriesPoint[] }) { + const points = series.map((p) => ({ played_at: p.played_at, value: p.score_to_par })) + if (points.length < 2) return null + return ( + + ) } +// Generisk per-variabel-trend -- filtrerer bort runder uten data for +// akkurat DENNE metrikken (f.eks. en runde uten puttsporing), i stedet +// for å late som verdien var 0. +function seriesFor(series: ApiRoundSeriesPoint[], field: keyof ApiRoundSeriesPoint): TrendPoint[] { + return series + .filter((p) => p[field] !== null) + .map((p) => ({ played_at: p.played_at, value: p[field] as number })) +} + +// Kumulativ (ikke per-runde) linje for Scrambling/Sand save -- se +// AskUserQuestion-svar 2026-08-12: for få kvalifiserte hull per runde +// til at et per-runde-tall er meningsfullt, så her vises kun raten +// "til og med denne runden" som en enkel linje, ingen stolper. +function CumulativeTrendLine({ points, label }: { points: TrendPoint[]; label: string }) { + if (points.length < 2) return null + const values = points.map((p) => p.value) + const maxVal = Math.max(...values, 1) + const baselineY = TREND_CHART_HEIGHT - 20 + const scale = TREND_MAGNITUDE_HEIGHT / maxVal + const valueToY = (v: number) => baselineY - v * scale + const width = Math.max(points.length * 26, 160) + const stepX = (i: number) => (points.length > 1 ? (i / (points.length - 1)) * (width - 24) + 12 : width / 2) + const linePoints = points.map((p, i) => `${stepX(i)},${valueToY(p.value)}`).join(" ") + + return ( +
+
+ + + + {points.map((p, i) => ( + + + {formatShortDate(p.played_at)}: {Math.round(p.value)} % + + + ))} + + {Math.round(points[0].value)} % + + + {Math.round(points[points.length - 1].value)} % + + +
+

Kumulativt -- raten din til og med hver runde, ikke per-runde-tall.

+
+ ) +} + // --- Til par: med vs. uten en gitt hendelse på hullet ----------------------- // Brukerens eksplisitte instruks 2026-07-28: se snitt til par NÅR noe // inntraff mot NÅR det ikke gjorde det (greentreff, fairwaytreff, bunker, @@ -645,6 +798,17 @@ export function RoundsStatsSummary() { (() => { const s = data.current const p = data.previous + const series = data.round_series + const fairwayTrend = seriesFor(series, "fairway_hit_pct") + const girTrend = seriesFor(series, "gir_pct") + const puttsTrend = seriesFor(series, "putts_per_18") + const onePuttTrend = seriesFor(series, "one_putt_pct") + const chipTrend = seriesFor(series, "chip_per_round") + const bunkerTrend = seriesFor(series, "bunker_per_round") + const penaltyTrend = seriesFor(series, "penalty_per_round") + const anywayTrend = seriesFor(series, "anyway_per_round") + const scramblingCumulative = seriesFor(series, "scrambling_cumulative_pct") + const sandSaveCumulative = seriesFor(series, "sand_save_cumulative_pct") return ( <>

@@ -677,7 +841,7 @@ export function RoundsStatsSummary() { /> - + {/* Rekkefølge 2026-08-12 (brukerinstruks): Utslag først, deretter Greentreff (en beregnet andel), så Innspill (en retnings- @@ -691,6 +855,19 @@ export function RoundsStatsSummary() { Fairwaytreff

+ {fairwayTrend.length >= 2 && ( +
+

Fairwaytreff over tid

+ +
+ )}
@@ -699,6 +876,12 @@ export function RoundsStatsSummary() { Greentreff + {girTrend.length >= 2 && ( +
+

Greentreff over tid

+ +
+ )}
@@ -730,6 +913,18 @@ export function RoundsStatsSummary() { Én-putt + {puttsTrend.length >= 2 && ( +
+

Putt / 18 hull over tid

+ +
+ )} + {onePuttTrend.length >= 2 && ( +
+

Én-putt over tid

+ +
+ )}
@@ -750,6 +945,29 @@ export function RoundsStatsSummary() { />
+ {(chipTrend.length >= 2 || bunkerTrend.length >= 2 || penaltyTrend.length >= 2) && ( + + {chipTrend.length >= 2 && ( +
+

Chip / runde

+ +
+ )} + {bunkerTrend.length >= 2 && ( +
+

Bunkerslag / runde

+ +
+ )} + {penaltyTrend.length >= 2 && ( +
+

Straffeslag / runde

+ +
+ )} +
+ )} + {s.avg_anyway_per_round !== null && (
)} + {anywayTrend.length >= 2 && ( + + + + )} +
@@ -787,6 +1011,22 @@ export function RoundsStatsSummary() {
+ {(scramblingCumulative.length >= 2 || sandSaveCumulative.length >= 2) && ( +
+ {scramblingCumulative.length >= 2 && ( +
+

Scrambling over tid

+ +
+ )} + {sandSaveCumulative.length >= 2 && ( +
+

Sand save over tid

+ +
+ )} +
+ )}