☕ PondPlotter.java (Java 22) GraphiC Foreign Function & Memory (FFM)
package examples;

import com.sciend.graphic.Color;
import com.sciend.graphic.Figure;
import com.sciend.graphic.Symbol;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

/**
 * PondPlotter - GraphiC Live Pond Temperature Plotter (Java 22 Edition)
 *
 * Plot specifications:
 *   - Y-axis: Temperature (°F) from 30 to 100.
 *   - X-axis: Time of day (0 to 24 hours).
 *   - Curves: T(t) for each of the past 30 days, cycling colors.
 *   - Current Point: Drawn in bold red with a circle marker and labeled reading.
 *   - Caption: "Pond Temperature (F)"
 *   - Subhead: "for past 30 days"
 *   - Live Timestamp: Date and time of last reading on the right above the X-axis.
 *   - Data Source: Reads from real rolling data in pond_data.csv.
 *
 * ==========================================================================
 * COMPILE AND RUN INSTRUCTIONS
 * ==========================================================================
 * Working Directory: /Users/jamesarome/Documents/Gravty/GraphiC
 *
 * 1. COMPILE:
 *    javac --enable-preview --release 22 -cp java/target/classes -d java/bin java/examples/PondPlotter.java
 *
 * 2. RUN (re-render existing data):
 *    GPC_NONINTERACTIVE=1 java --enable-preview --enable-native-access=ALL-UNNAMED -cp java/target/classes:java/bin examples.PondPlotter
 *
 * 3. RUN AND RECORD A NEW READING (e.g. 75.0 F):
 *    GPC_NONINTERACTIVE=1 java --enable-preview --enable-native-access=ALL-UNNAMED -cp java/target/classes:java/bin examples.PondPlotter 75.0
 *
 * 4. UPLOAD PLOT TO SERVER:
 *    scp pond_30days.svg user@yourserver.com:/path/to/web/pond/pond_30days.svg
 * ==========================================================================
 */
public class PondPlotter {

    // Palette of distinct GraphiC colors to cycle across historical days
    private static final Color[] PALETTE = {
        Color.DRK_GRAY,
        Color.BLUE,
        Color.LGT_BLUE,
        Color.CYAN,
        Color.LGT_CYAN,
        Color.GREEN,
        Color.LGT_GREEN,
        Color.BROWN,
        Color.MAGENTA,
        Color.LGT_MAGENTA,
        Color.YELLOW,
        Color.LGT_RED,
        Color.RED
    };

    private static final DateTimeFormatter ISO_FMT = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
    private static final DateTimeFormatter DISPLAY_FMT = DateTimeFormatter.ofPattern("MMM d, yyyy  h:mm a z");

    /**
     * Single temperature reading record.
     */
    public static class Reading {
        public final ZonedDateTime time;
        public final LocalDate date;
        public final float hour;
        public final float temp;

        public Reading(ZonedDateTime time, float temp) {
            this.time = time;
            this.date = time.toLocalDate();
            this.hour = time.getHour() + (time.getMinute() / 60.0f) + (time.getSecond() / 3600.0f);
            this.temp = temp;
        }

        public String toCsvLine() {
            return String.format(Locale.US, "%s,%.4f,%.2f", time.format(ISO_FMT), hour, temp);
        }

        public static Reading fromCsvLine(String line) {
            String[] parts = line.split(",");
            if (parts.length < 3) return null;
            try {
                ZonedDateTime t = ZonedDateTime.parse(parts[0].trim(), ISO_FMT).withZoneSameInstant(ZoneId.systemDefault());
                float temp = Float.parseFloat(parts[2].trim());
                return new Reading(t, temp);
            } catch (Exception e) {
                return null;
            }
        }
    }

    public static void main(String[] args) throws Exception {
        Path dataFile = Path.of("pond_data.csv");

        // If a numeric argument was provided, record that new reading first
        if (args.length > 0) {
            try {
                float newTemp = Float.parseFloat(args[0]);
                recordReading(dataFile, newTemp);
                System.out.printf("[Data] Recorded new temperature reading: %.1f °F%n", newTemp);
            } catch (NumberFormatException e) {
                // If argument is a file path instead of a number
                dataFile = Path.of(args[0]);
            }
        }

        // Ensure data file exists (create with CSV header if missing)
        if (!Files.exists(dataFile) || Files.size(dataFile) == 0) {
            try (BufferedWriter writer = Files.newBufferedWriter(dataFile,
                    StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
                writer.write("timestamp,hour,temp");
                writer.newLine();
            }
        }

        // Load real historical readings from CSV
        List<Reading> allReadings = loadReadings(dataFile);
        if (allReadings.isEmpty()) {
            System.err.println("[Error] No readings found in " + dataFile);
            return;
        }

        // Group readings by calendar day (TreeMap keeps dates in chronological order)
        Map<LocalDate, List<Reading>> daysMap = new TreeMap<>();
        for (Reading r : allReadings) {
            daysMap.computeIfAbsent(r.date, k -> new ArrayList<>()).add(r);
        }

        // Keep at most the last 30 calendar days
        List<LocalDate> sortedDates = new ArrayList<>(daysMap.keySet());
        if (sortedDates.size() > 30) {
            sortedDates = sortedDates.subList(sortedDates.size() - 30, sortedDates.size());
        }

        int numDays = sortedDates.size();
        LocalDate todayDate = sortedDates.get(numDays - 1);
        List<Reading> todayReadings = daysMap.get(todayDate);
        todayReadings.sort(Comparator.comparingDouble(r -> r.hour));

        Reading latestReading = todayReadings.get(todayReadings.size() - 1);
        float currentHour = latestReading.hour;
        float currentTemp = latestReading.temp;

        // Render with GraphiC library
        try (Figure fig = new Figure(8.5f, 6.5f, Color.WHITE, "pond_30days.tkf")) {

            // Main Title Heading
            fig.title("Pond Temperature (F)");

            // Axis names (shift Y-axis label further left: axis 2 = GPC_Y_AXIS, factor = 2.4f)
            fig.xlabel("Time of Day (Hours)");
            fig.setNameSpace(2, 2.4f);
            fig.ylabel("Temperature (F)");

            // Setup axes: X: 0 to 24 (step 4), Y: 30 to 100 (step 10)
            fig.setupAxes(
                0.0f, 4.0f, 24.0f,
                30.0f, 10.0f, 100.0f,
                "%2.0f", "%3.0f", Color.BLACK
            );
            fig.box(Color.BLACK);

            // Subhead lower down, comfortably below the main title
            String subhead = (numDays >= 30) ? "for past 30 days"
                : (numDays > 1 ? String.format(Locale.US, "for past %d days", numDays) : "Today's Readings");
            fig.putlabel(subhead, 9.8f, 100.8f, 0.13f, 0, 1);

            // Plot previous historical days cycling through palette
            for (int d = 0; d < numDays - 1; d++) {
                LocalDate date = sortedDates.get(d);
                List<Reading> dayList = daysMap.get(date);
                dayList.sort(Comparator.comparingDouble(r -> r.hour));

                float[] x = new float[dayList.size()];
                float[] y = new float[dayList.size()];
                for (int i = 0; i < dayList.size(); i++) {
                    x[i] = dayList.get(i).hour;
                    y[i] = dayList.get(i).temp;
                }

                Color c = PALETTE[d % PALETTE.length];
                fig.tcurve(1);
                fig.plot(x, y, c, 0);
            }

            // Plot today's curve up to latest reading in bold RED
            float[] todayX = new float[todayReadings.size()];
            float[] todayY = new float[todayReadings.size()];
            for (int i = 0; i < todayReadings.size(); i++) {
                todayX[i] = todayReadings.get(i).hour;
                todayY[i] = todayReadings.get(i).temp;
            }
            fig.tcurve(6);
            fig.plot(todayX, todayY, Color.RED, 0);
            fig.tcurve(0);

            // Current point: Draw circle marker and numeric label
            fig.tcurve(2);
            fig.scatter(new float[]{currentHour}, new float[]{currentTemp}, Color.RED, Symbol.CIRCLE);
            fig.tcurve(0);
            String label = String.format(Locale.US, "  %.1f", currentTemp);
            fig.putlabel(label, currentHour, currentTemp + 1.0f, 0.13f, 0, 1);

            // Time and date of last reading right above the X-axis
            String lastReadingStr = "Last reading: " + latestReading.time.format(DISPLAY_FMT)
                + String.format(Locale.US, " (%.1f F)", currentTemp);
            fig.color(Color.BLACK);
            fig.putlabel(lastReadingStr, 11.5f, 32.0f, 0.10f, 0, 1);

            // Save SVG and HTML outputs
            Path svgOut = Path.of("pond_30days.svg");
            Path htmlOut = Path.of("pond_30days.html");
            fig.save(svgOut);
            fig.saveHtml(htmlOut);

            System.out.println("[GraphiC] Successfully rendered 30-day pond plot from real data!");
            System.out.println("  -> Dates plotted: " + numDays + " days (" + sortedDates.get(0) + " to " + todayDate + ")");
            System.out.printf("  -> Latest point:  %.2f hrs, %.1f °F (%s)%n", currentHour, currentTemp, latestReading.time.format(DISPLAY_FMT));
            System.out.println("  -> SVG Output:    " + svgOut.toAbsolutePath());
            System.out.println("  -> HTML Output:   " + htmlOut.toAbsolutePath());
        }
    }

    /**
     * Appends a new reading to pond_data.csv.
     */
    public static void recordReading(Path csvFile, float temp) throws IOException {
        ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault());
        Reading r = new Reading(now, temp);
        try (BufferedWriter writer = Files.newBufferedWriter(csvFile,
                StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
            writer.write(r.toCsvLine());
            writer.newLine();
        }
    }

    /**
     * Loads readings from pond_data.csv.
     */
    public static List<Reading> loadReadings(Path csvFile) throws IOException {
        List<Reading> list = new ArrayList<>();
        try (BufferedReader reader = Files.newBufferedReader(csvFile)) {
            String line;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (line.isEmpty() || line.startsWith("#") || line.startsWith("timestamp")) {
                    continue;
                }
                Reading r = Reading.fromCsvLine(line);
                if (r != null) {
                    list.add(r);
                }
            }
        }
        return list;
    }
}