forked from jordanlambrecht/tracker-tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTorrentSeedTimeDistribution.tsx
More file actions
164 lines (147 loc) · 5.72 KB
/
Copy pathTorrentSeedTimeDistribution.tsx
File metadata and controls
164 lines (147 loc) · 5.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// src/components/charts/TorrentSeedTimeDistribution.tsx
"use client"
import type { EChartsOption } from "echarts"
import { useMemo } from "react"
import { getSeedTimeBuckets, SEEDING_STATES } from "@/lib/fleet"
import { formatCount } from "@/lib/formatters"
import { ChartECharts } from "./lib/ChartECharts"
import { ChartEmptyState } from "./lib/ChartEmptyState"
import { buildBucketedBarOption } from "./lib/chart-helpers"
import { CHART_THEME, chartAxisLabel, chartDot, chartGrid, chartTooltip } from "./lib/theme"
// Pre-aggregated props (fleet dashboard path)
interface PreAggregatedProps {
buckets: { label: string; count: number; color: string; max: number }[]
seedTimeHours?: number | null
height?: number
}
// Raw torrents props (per-tracker page path)
interface RawTorrentsProps {
torrents: { state: string; seedingTime: number }[]
seedTimeHours?: number | null
accentColor?: string
height?: number
}
type TorrentSeedTimeDistributionProps = PreAggregatedProps | RawTorrentsProps
function computeMarkLine(
buckets: { max: number }[],
seedTimeHours: number | null
): { thresholdIdx: number; label: string; color: string } | undefined {
if (seedTimeHours == null || seedTimeHours <= 0) return undefined
const thresholdSeconds = seedTimeHours * 3600
const idx = buckets.findIndex((b, i) => {
const prevMax = i === 0 ? 0 : (buckets[i - 1]?.max ?? 0)
return thresholdSeconds >= prevMax && thresholdSeconds < b.max
})
if (idx === -1) return undefined
const label = seedTimeHours % 24 === 0 ? `Min: ${seedTimeHours / 24}d` : `Min: ${seedTimeHours}h`
return { thresholdIdx: idx, label, color: CHART_THEME.warn }
}
function buildSeedTimeBucketedOption(
buckets: { label: string; count: number; color: string; max: number }[],
markLine?: { thresholdIdx: number; label: string; color: string }
): EChartsOption {
const total = buckets.reduce((sum, b) => sum + b.count, 0)
const data = buckets.map((b) => ({
value: b.count,
itemStyle: { color: b.color, borderRadius: [4, 4, 0, 0] },
}))
const seriesEntry: Record<string, unknown> = {
type: "bar",
data,
barMaxWidth: 48,
emphasis: { itemStyle: { shadowBlur: 10, shadowColor: "rgba(0,0,0,0.3)" } },
}
if (markLine) {
seriesEntry.markLine = {
silent: true,
symbol: "none",
data: [{ xAxis: markLine.thresholdIdx }],
lineStyle: { color: markLine.color, type: "dashed", width: 1 },
label: {
show: true,
formatter: markLine.label,
position: "end",
color: markLine.color,
fontSize: CHART_THEME.fontSizeCompact,
},
}
}
return {
backgroundColor: "transparent",
grid: chartGrid({ top: 24, right: 16, bottom: 40, left: 48 }),
tooltip: chartTooltip("item", {
formatter: (params: unknown) => {
const p = params as { value: number; color: string; dataIndex: number }
const bucket = buckets[p.dataIndex]
if (!bucket) return ""
const pct = total > 0 ? ((bucket.count / total) * 100).toFixed(1) : "0.0"
return (
chartDot(p.color) +
`<span style="color:${CHART_THEME.textPrimary};font-weight:600;">Seed Time ${bucket.label}</span><br/>` +
`<span style="color:${CHART_THEME.textSecondary};">${formatCount(bucket.count)} torrents</span>` +
`<span style="color:${CHART_THEME.textTertiary};"> · ${pct}%</span>`
)
},
}),
xAxis: {
type: "category",
data: buckets.map((b) => b.label),
axisLine: { lineStyle: { color: CHART_THEME.gridLine } },
axisTick: { show: false },
axisLabel: chartAxisLabel(),
},
yAxis: {
type: "value",
axisLine: { show: false },
axisTick: { show: false },
axisLabel: chartAxisLabel(),
splitLine: { lineStyle: { color: CHART_THEME.gridLine } },
},
series: [seriesEntry],
}
}
function TorrentSeedTimeDistribution(props: TorrentSeedTimeDistributionProps) {
const { seedTimeHours = null, height = 200 } = props
const isFleet = "buckets" in props
const fleetBuckets = isFleet ? (props as PreAggregatedProps).buckets : null
const rawTorrents = !isFleet ? (props as RawTorrentsProps).torrents : []
const accentColor = !isFleet
? ((props as RawTorrentsProps).accentColor ?? CHART_THEME.accent)
: CHART_THEME.accent
const fleetOption = useMemo<EChartsOption | null>(() => {
if (!fleetBuckets || fleetBuckets.length === 0 || fleetBuckets.every((b) => b.count === 0)) {
return null
}
const markLine = computeMarkLine(fleetBuckets, seedTimeHours)
return buildSeedTimeBucketedOption(fleetBuckets, markLine)
}, [fleetBuckets, seedTimeHours])
const perTrackerOption = useMemo<EChartsOption | null>(() => {
if (isFleet) return null
const seeding = rawTorrents.filter((t) => SEEDING_STATES.has(t.state))
if (seeding.length === 0) return null
const buckets = getSeedTimeBuckets(accentColor)
const markLine = computeMarkLine(buckets, seedTimeHours)
return buildBucketedBarOption({
buckets,
torrents: seeding,
getThreshold: (b) => b.max,
getValue: (t) => t.seedingTime,
getLabel: (b) => b.label,
getColor: (b) => b.color,
labelPrefix: "Seed Time",
markLine,
})
}, [isFleet, rawTorrents, accentColor, seedTimeHours])
if (isFleet) {
if (!fleetOption) {
return <ChartEmptyState height={height} message="No seeding torrents found" />
}
return <ChartECharts option={fleetOption} style={{ height, width: "100%" }} />
}
if (!perTrackerOption) {
return <ChartEmptyState height={height} message="No seeding torrents found" />
}
return <ChartECharts option={perTrackerOption} style={{ height, width: "100%" }} />
}
export type { TorrentSeedTimeDistributionProps }
export { TorrentSeedTimeDistribution }