Skip to content
Open
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
66 changes: 66 additions & 0 deletions app/components/ChartTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/
import { format } from 'date-fns'
import type { ReactNode } from 'react'
import { match } from 'ts-pattern'

const longDateTime = (ts: number) => format(new Date(ts), 'MMM d, yyyy HH:mm:ss zz')

type ChartTooltipProps = {
timestamp: number
left: number
top: number
offset: [LeftRight, TopBottom]
children: ReactNode
}

/** Offset the box into the quadrant away from the point so it never overflows an edge */
export type LeftRight = 'left' | 'right'
export type TopBottom = 'top' | 'bottom'

const TOOLTIP_GAP = 12
function tooltipTransform(leftRight: LeftRight, topBottom: TopBottom): string {
const tx = match(leftRight)
.with('left', () => `calc(-100% - ${TOOLTIP_GAP}px)`)
.with('right', () => `${TOOLTIP_GAP}px`)
.exhaustive()
const ty = match(topBottom)
.with('top', () => `calc(-100% - ${TOOLTIP_GAP}px)`)
.with('bottom', () => `${TOOLTIP_GAP}px`)
.exhaustive()
return `translate(${tx}, ${ty})`
}

export function ChartTooltip({
timestamp,
left,
top,
offset,
children,
}: ChartTooltipProps) {
return (
<div
className="pointer-events-none absolute z-10 w-max"
style={{
left: left,
top: top,
transform: tooltipTransform(...offset),
}}
>
<div
role="tooltip"
className="text-sans-md text-secondary bg-raise shadow-tooltip rounded-md outline-0"
>
<div className="border-secondary border-b px-3 py-2 pr-6">
{longDateTime(timestamp)}
</div>
<div className="px-3 py-2">{children}</div>
</div>
</div>
)
}
78 changes: 78 additions & 0 deletions app/components/FramedChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/
import { useMemo, type ReactNode, type RefObject } from 'react'
import type uPlot from 'uplot'
import UplotReact from 'uplot-react'

import { useElementSize } from '~/hooks/use-element-size'

// The intended left padding (px-5) is taken from the container and given to
// uPlot instead, so the plot sits flush left while x-tick labels can bleed into
// the gutter without clipping.
const CHART_LEFT_PAD = 20

export type UPlotOptions = Omit<uPlot.Options, 'width' | 'height' | 'padding'>

type Props = {
title: string
height: number
chartOptions: UPlotOptions
data: uPlot.AlignedData
uRef: RefObject<uPlot | null>
children?: ReactNode
legend?: ReactNode
}

export function FramedChart({
title,
height,
chartOptions,
data,
uRef,
children,
legend,
}: Props) {
const [size, sizeRef] = useElementSize()

// Width/height changes cause a cheaper "update" path for uplot, instead of
// "create", so it gets its own layer of memoization
const options = useMemo(
() =>
({
...chartOptions,
padding: [null, null, null, CHART_LEFT_PAD],
width: size?.width ?? 0,
height,
}) satisfies uPlot.Options,
[chartOptions, size?.width, height]
)

return (
<figure
aria-label={title}
className="m-0 pt-8 pb-5 pl-0"
style={{ paddingRight: CHART_LEFT_PAD }}
>
{/* The actual chart is absolutely positioned so its fixed pixel width
doesn't influence layout and block future resizing. That in turn makes
its container need an explicit height */}
<div ref={sizeRef} className="relative" style={{ height }}>
{size && (
<UplotReact
className="absolute top-0 left-0"
options={options}
data={data}
onCreate={(u) => (uRef.current = u)}
/>
)}
{children}
</div>
{legend}
</figure>
)
}
Loading
Loading