Files
Compass/web/components/og/graph.tsx
2026-02-23 14:48:03 +01:00

153 lines
4.5 KiB
TypeScript

import {Point} from 'common/edge/og'
import {scaleLinear, scaleTime} from 'd3-scale'
import {area, curveLinear, curveStepBefore, line} from 'd3-shape'
export function ProfitLossGraph(props: {
data: Point[]
height: number
/** scaled width / height */
aspectRatio?: number
}) {
const {data, height, aspectRatio = 1} = props
const w = height * aspectRatio
const h = height
const visibleRange = [data[0].x, data[data.length - 1].x]
const minY = Math.min(...data.map((p) => p.y))
const maxY = Math.max(...data.map((p) => p.y))
const curve = curveStepBefore
const xScale = scaleTime(visibleRange, [0, w])
const yScale = scaleLinear([minY, maxY], [h, 0])
const px = (p: Point) => xScale(p.x)
const py0 = yScale(0)
const py1 = (p: Point) => yScale(p.y)
// const clipId = ':rnm:'
const gradientId = ':rnc:'
const da = area(px, py0, py1).curve(curve)(data)!
const dl = line(px, py1).curve(curve)(data)!
const color = (p: Point) => (p.y >= 0 ? '#14b8a6' : '#FFA799')
const stops = computeColorStops(data, color, px)
return (
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
<defs>
<linearGradient gradientUnits="userSpaceOnUse" id={gradientId}>
{stops.map((s, i) => (
<stop key={i} offset={`${s.x / w}`} stopColor={s.color} />
))}
</linearGradient>
</defs>
<g>
<path d={da} fill={`url(#${gradientId})`} opacity={0.2} />
<path d={dl} stroke={`url(#${gradientId})`} strokeWidth={4} fill="none" />
</g>
</svg>
)
}
const computeColorStops = (data: Point[], pc: (p: Point) => string, px: (p: Point) => number) => {
const segments: {x: number; color: string}[] = []
let currOffset = px(data[0])
let currColor = pc(data[0])
for (const p of data) {
const c = pc(p)
if (c !== currColor) {
segments.push({x: currOffset, color: currColor})
currOffset = px(p)
currColor = c
}
}
segments.push({x: currOffset, color: currColor})
const stops: {x: number; color: string}[] = []
stops.push({x: segments[0].x, color: segments[0].color})
for (const s of segments.slice(1)) {
stops.push({x: s.x, color: stops[stops.length - 1].color})
stops.push({x: s.x, color: s.color})
}
return stops
}
export function Sparkline(props: {
data: Point[]
height: number
/** width / height */
aspectRatio?: number
min: number
max: number
color?: string
className?: string
}) {
const {data, height: h, aspectRatio = 1, min, max, color, className} = props
const w = h * aspectRatio
const visibleRange = [data[0].x, data[data.length - 1].x]
const curve = data.length > 50 ? curveLinear : curveStepBefore
const xScale = scaleTime(visibleRange, [0, w - 4])
const yScale = scaleLinear([min, max], [h, 0])
const px = (p: Point) => xScale(p.x)
const py1 = (p: Point) => yScale(p.y)
const dl = line(px, py1).curve(curve)(data)!
return (
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} className={className}>
<path d={dl} stroke={color} strokeWidth={4} fill="none" strokeLinejoin="round" />
</svg>
)
}
export function ProbGraph(props: {
data: Point[]
height: number
/** scaled width / height */
aspectRatio?: number
color?: string
}) {
const {data, height, color = '#14b866', aspectRatio = 1} = props
const w = height * aspectRatio
const h = height
const visibleRange = [data[0].x, data[data.length - 1].x]
const minY = Math.min(...data.map((p) => p.y))
const maxY = Math.max(...data.map((p) => p.y))
const curve = curveLinear
const fillStretchFactor = 5
const xScale = scaleTime(visibleRange, [0, w])
const yScale = scaleLinear([minY, maxY + 0.01], [h / 3, 0])
const px = (p: Point) => xScale(p.x)
const adjustedPy0 = yScale(minY) * fillStretchFactor
const py1 = (p: Point) => yScale(p.y)
// const clipId = ':rnm:'
const gradientId = ':rnc:'
const da = area(px, adjustedPy0, py1).curve(curve)(data)!
const dl = line(px, py1).curve(curve)(data)!
return (
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
<defs>
<linearGradient
gradientUnits="userSpaceOnUse"
id={gradientId}
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop offset="0%" stopColor={color} stopOpacity="1" />
<stop offset="100%" stopColor={color} stopOpacity="0.25" />
</linearGradient>
</defs>
<g>
<path d={da} fill={`url(#${gradientId})`} opacity={0.1} />
<path d={dl} stroke={color} strokeWidth={3} fill="none" strokeLinejoin="round" />
</g>
</svg>
)
}