lift-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev f9f4e37d6945
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 23e397d8a5d5bf7756362640
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/Chart.tsx
// A small line chart for one measure over time (an exercise's best 1RM, body weight), with a
// crosshair and a readout for the point under the finger or pointer.
import { useEffect, useMemo, useRef, useState } from "react";

export interface Point { x: number; y: number }

const H = 200;
const PAD = { l: 46, r: 18, t: 16, b: 28 };

function niceStep(range: number) {
  const raw = range / 3;
  const mag = 10 ** Math.floor(Math.log10(raw || 1));
  return [1, 2, 2.5, 5, 10].map((m) => m * mag).find((s) => s >= raw) ?? raw;
}

export function LineChart({ points, format, label }: { points: Point[]; format: (y: number) => string; label: string }) {
  const box = useRef<HTMLDivElement>(null);
  const [w, setW] = useState(600);
  const [hover, setHover] = useState<number | null>(null);

  useEffect(() => {
    const el = box.current;
    if (!el) return;
    const ro = new ResizeObserver(() => setW(Math.max(240, el.clientWidth)));
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  const geo = useMemo(() => {
    const xs = points.map((p) => p.x), ys = points.map((p) => p.y);
    const x0 = Math.min(...xs), x1 = Math.max(...xs);
    let y0 = Math.min(...ys), y1 = Math.max(...ys);
    if (y1 - y0 < 1e-9) { y0 -= 1; y1 += 1; }
    const step = niceStep(y1 - y0);
    y0 = Math.floor(y0 / step) * step;
    y1 = Math.ceil(y1 / step) * step;
    const ticks: number[] = [];
    for (let v = y0; v <= y1 + step / 2; v += step) ticks.push(v);
    const sx = (x: number) => (x1 === x0 ? (PAD.l + w - PAD.r) / 2 : PAD.l + ((x - x0) / (x1 - x0)) * (w - PAD.l - PAD.r));
    const sy = (y: number) => PAD.t + (1 - (y - y0) / (y1 - y0)) * (H - PAD.t - PAD.b);
    return { sx, sy, ticks, x0, x1 };
  }, [points, w]);

  if (!points.length) return null;
  const pts = points.map((p) => ({ ...p, px: geo.sx(p.x), py: geo.sy(p.y) }));
  const path = pts.map((p, i) => `${i ? "L" : "M"}${p.px.toFixed(1)},${p.py.toFixed(1)}`).join(" ");
  const last = pts[pts.length - 1];
  const shown = hover !== null ? pts[hover] : null;
  const dateText = (x: number) => new Date(x).toLocaleDateString(undefined, { day: "numeric", month: "short" });

  const onMove = (e: React.PointerEvent<SVGSVGElement>) => {
    const r = e.currentTarget.getBoundingClientRect();
    const x = ((e.clientX - r.left) / r.width) * w;
    let best = 0;
    for (let i = 1; i < pts.length; i++) if (Math.abs(pts[i].px - x) < Math.abs(pts[best].px - x)) best = i;
    setHover(best);
  };

  return (
    <div className="chart" ref={box}>
      <svg width={w} height={H} viewBox={`0 0 ${w} ${H}`} role="img"
        aria-label={`${label}: ${format(points[0].y)} on ${dateText(points[0].x)}, ${format(last.y)} on ${dateText(last.x)}, ${points.length} entries`}
        onPointerMove={onMove} onPointerDown={onMove} onPointerLeave={() => setHover(null)}>
        {geo.ticks.map((t) => (
          <g key={t}>
            <line x1={PAD.l} x2={w - PAD.r} y1={geo.sy(t)} y2={geo.sy(t)} className="chart-grid" />
            <text x={PAD.l - 8} y={geo.sy(t) + 4} textAnchor="end" className="chart-axis">{format(t)}</text>
          </g>
        ))}
        <text x={pts[0].px} y={H - 8} textAnchor={pts.length > 1 ? "start" : "middle"} className="chart-axis">{dateText(points[0].x)}</text>
        {pts.length > 1 && <text x={last.px} y={H - 8} textAnchor="end" className="chart-axis">{dateText(last.x)}</text>}
        <path d={path} className="chart-line" />
        {pts.map((p, i) => <circle key={i} cx={p.px} cy={p.py} r={pts.length > 40 ? 2.5 : 4} className="chart-dot" />)}
        {shown && <line x1={shown.px} x2={shown.px} y1={PAD.t} y2={H - PAD.b} className="chart-cross" />}
        {shown && <circle cx={shown.px} cy={shown.py} r={6} className="chart-focus" />}
      </svg>
      <p className="chart-readout" aria-live="polite">
        {shown ? <><strong>{format(shown.y)}</strong> on {new Date(shown.x).toLocaleDateString(undefined, { weekday: "short", day: "numeric", month: "short", year: "numeric" })}</>
          : <>Latest <strong>{format(last.y)}</strong> on {dateText(last.x)}</>}
      </p>
    </div>
  );
}

Change history

Point OpenApps store links to openapps.free
f9f4e37d69 · 17 hours ago
Add favicon.ico, local share image and openapi.json for links and guides
2796c0063c · 17 hours ago
Bind agent address in policy constants
cf80a3ca66 · 19 hours ago
Add OpenApp contribution strip and OpenApps source links
5e4cc2b9f3 · 19 hours ago
Bounded source sync
ca4a3d3631 · 1 day ago
Bounded source sync
7351063f4d · 1 day ago
Bounded source sync
0ce3238c46 · 1 day ago