0.1.6Updated a month ago
import { useEffect, useRef, useState } from "preact/hooks";
import type { JSX } from "preact";
import LineChart from "./line_chart.tsx";
import { customRangeEnd, customRangeStart, rangeMode, rangeValue } from "@islands/charts/state/summary.ts";

type ChartProps = {
  name: string;
  description?: string;
  type?: "line";
  endpoint: string;
};

const ChartPlugins: Record<string, (props: Chart.Plugin.Props) => JSX.Element> = {
  line: LineChart,
};

export default function CardChart({ name, description, type = "line", endpoint }: ChartProps) {
  const [data, setData] = useState<Chart.DataPoint[] | null>(null);
  const [loading, setLoading] = useState(true);

  const [width, setWidth] = useState(500);
  const height = 160;

  const [hoverX, setHoverX] = useState<number | null>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  // Update rendered width
  useEffect(() => {
    const updateWidth = () => {
      if (containerRef.current) {
        const rect = containerRef.current.getBoundingClientRect();
        setWidth(rect.width);
      }
    };

    updateWidth();
    globalThis.addEventListener("resize", updateWidth);
    return () => globalThis.removeEventListener("resize", updateWidth);
  }, []);

  // Load chart data
  useEffect(() => {
    const load = async () => {
      try {
        let qs = '';

        switch(rangeMode.value) {
          case "custom": {
            qs = new URLSearchParams({
              start: customRangeStart.value.toISOString(),
              end: customRangeEnd.value.toISOString(),
            }).toString();
            break;
          }
          case "minutes":
          case "hours":
          case "days":
          case "months":
          case "weeks":
            qs = new URLSearchParams({
              [rangeMode.value]: rangeValue.value.toString()
            }).toString();
            break;
        }

        const res = await fetch(`${endpoint}?${qs}`);
        const json = await res.json();

        const points = json.data.map((d: any) => ({
          timestamp: new Date(d.timestamp),
          value: d.value,
          label: d.label,
        })) as Chart.DataPoint[];

        setData(points);
      } catch (err) {
        console.error("Chart load failed:", err);
        setData([]);
      } finally {
        setLoading(false);
      }
    };

    load();
  }, [endpoint, rangeMode.value, rangeValue.value, customRangeStart.value, customRangeEnd.value]);

  // Nearest point from hoverX
  let nearest: Chart.DataPoint | null = null;
  let hoverY: number | null = null;

  if (hoverX !== null && data && data.length > 0) {
    const step = (width - 36) / (data.length - 1);
    const index = Math.round((hoverX - 36) / step);
    nearest = data[Math.max(0, Math.min(index, data.length - 1))];
    if (nearest) {
      const values = data.map(d => d.value);
      const min = Math.min(...values);
      const max = Math.max(...values);
      const range = max - min || 1;
      hoverY = height - ((nearest.value - min) / range) * height;
    }
  }

  // Pointer handlers
  const handlePointer = (clientX: number) => {
    if (!containerRef.current) return;
    const bounds = containerRef.current.getBoundingClientRect();
    const x = clientX - bounds.left;
    setHoverX(x);
  };

  const eventHandlers = {
    onMouseMove: (e: MouseEvent | TouchEvent) => {
      if(e instanceof MouseEvent) {
        handlePointer(e.clientX)
      } else if(globalThis.TouchEvent && e instanceof TouchEvent) {
        handlePointer(e.touches[e.touches.length-1].clientX);
      }
    },
    onMouseLeave: () => {
      setHoverX(null);
    },
    onTouchStart: (e: TouchEvent) => {
      handlePointer(e.touches[0].clientX);
    },
    onTouchMove: (e: TouchEvent) => {
      handlePointer(e.touches[0].clientX);
    },
    onTouchEnd: () => {
      
    }
    
  };

  const Plugin = ChartPlugins[type];

  return (
    <div class="bg-base-100 rounded-xl shadow p-4 space-y-4 w-128 max-w-full">
      <div class="flex flex-col">
        <span class="font-semibold text-base-content text-sm">{name}</span>
        {description && (
          <span class="text-xs text-base-content/60">{description}</span>
        )}
      </div>

      <div ref={containerRef} class="h-40 w-full relative touch-none">
        {loading ? (
          <div class="skeleton h-full w-full rounded" />
        ) : data && data.length > 0 && Plugin ? (
          <Plugin
            data={data}
            width={width}
            height={height}
            hoverX={hoverX}
            hoverY={hoverY}
            nearest={nearest}
            eventHandlers={{
              onMouseLeave: eventHandlers.onMouseLeave,
              onMouseMove: eventHandlers.onMouseMove,
              onTouchStart: eventHandlers.onTouchStart,
              onTouchEnd: eventHandlers.onTouchEnd,
              onTouchMove: eventHandlers.onTouchMove,
            }}
          />
        ) : (
          <div class="text-xs italic text-base-content/40 h-full flex items-center justify-center">
            No data available
          </div>
        )}
      </div>
    </div>
  );
}