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;
}
}