/* * VampireLossResults.java - Copyright(c) 2014 Joe Pasqua * Provided under the MIT License. See the LICENSE file for details. * Created: Apr 05, 2014 */ package org.noroomattheinn.visibletesla; import java.net.URL; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.control.Tooltip; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; import javafx.stage.Stage; import javafx.util.StringConverter; import javafx.util.converter.TimeStringConverter; import org.noroomattheinn.fxextensions.VTDialog; import org.noroomattheinn.visibletesla.data.RestCycle; /** * VampireLossResults: Display statistics about vampire loss. * * @author Joe Pasqua <joe at NoRoomAtTheInn dot org> */ public class VampireLossResults extends VTDialog.Controller { /*------------------------------------------------------------------------------ * * Internal State * *----------------------------------------------------------------------------*/ private String units; /*------------------------------------------------------------------------------ * * Internal State - UI Components * *----------------------------------------------------------------------------*/ @FXML private LineChart<Number, Number> chart; @FXML private LineChart<Number, Number> sequenceChart; /*============================================================================== * ------- ------- * ------- Public Interface To This Class ------- * ------- ------- *============================================================================*/ static void show(Stage stage, List<RestCycle> restPeriods, String units, double average) { VampireLossResults vlr = VTDialog.<VampireLossResults>load( VampireLossResults.class.getResource("VampireLossResults.fxml"), "Vampire Loss", stage); vlr.buildCharts(restPeriods, units, average); vlr.show(); } /*------------------------------------------------------------------------------ * * Main Chart Building Methods * *----------------------------------------------------------------------------*/ private void buildCharts(List<RestCycle> restPeriods, String units, double average) { this.units = units; if (restPeriods == null || restPeriods.isEmpty()) return; // Hack to make tooltip styles works. No one knows why. URL url = getClass().getClassLoader().getResource("org/noroomattheinn/styles/tooltip.css"); dialogStage.getScene().getStylesheets().add(url.toExternalForm()); // ----- Set up time-based chart chart.setTitle("Vampire Loss Data"); chart.setLegendVisible(false); Node chartBackground = chart.lookup(".chart-plot-background"); chartBackground.setStyle("-fx-background-color: white;"); NumberAxis xAxis = (NumberAxis)chart.getXAxis(); xAxis.setAutoRanging(false); xAxis.setLowerBound(0.0); xAxis.setUpperBound(24.0); xAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(xAxis) { @Override public String toString(Number hr) { int adjusted = (hr.intValue() + 24) % 24; return String.format("%2d", adjusted); } }); List<RestCycle> splitCycles = new ArrayList<>(2); for (RestCycle r : restPeriods) { r.splitIntoDays(splitCycles); for (RestCycle splitRest: splitCycles) { XYChart.Series<Number,Number> series = new XYChart.Series<>(); ObservableList<XYChart.Data<Number, Number>> data = series.getData(); addPeriod(data, splitRest); chart.getData().add(series); series.getNode().setStyle("-fx-opacity: 0.25; -fx-stroke-width: 3px;"); } splitCycles.clear(); } final double overallAverage = average; XYChart.Series<Number,Number> avg = new XYChart.Series<>(); avg.setName("Average"); final String tip = String.format("Average Loss: %3.2f %s/hr", overallAverage, units); final XYChart.Data<Number,Number> p1 = new XYChart.Data<Number,Number>(0.2, overallAverage); p1.setExtraValue(tip); avg.getData().add(p1); addTooltip(p1); final XYChart.Data<Number,Number> p2 = new XYChart.Data<Number,Number>(23.8, overallAverage); p2.setExtraValue(tip); avg.getData().add(p2); addTooltip(p2); chart.getData().add(avg); avg.getNode().setStyle("-fx-opacity: 0.5; -fx-stroke-width: 10px;"); // ----- Set up the Scatter Chart // Scale down to seconds from ms. Using ms seems to cause difficulty // for the Chart facility sequenceChart.setTitle("Vampire Loss Data"); sequenceChart.setLegendVisible(false); chartBackground = sequenceChart.lookup(".chart-plot-background"); chartBackground.setStyle("-fx-background-color: white;"); xAxis = (NumberAxis)sequenceChart.getXAxis(); xAxis.setAutoRanging(false); long s = restPeriods.get(0).startTime/1000L; long e = restPeriods.get(restPeriods.size()-1).endTime/1000L; xAxis.setLowerBound(s); xAxis.setUpperBound(e); xAxis.setTickUnit(24*60*60); // A day worth of seconds xAxis.setTickLabelFormatter(new DateLabelGenerator()); XYChart.Series<Number,Number> series = new XYChart.Series<>(); series.setName("Vampire Loss"); ObservableList<XYChart.Data<Number, Number>> data = series.getData(); for (RestCycle r : restPeriods) { final XYChart.Data<Number,Number> dataPoint = new XYChart.Data<Number,Number>(r.startTime/1000L, r.avgLoss()); addTooltip(dataPoint); dataPoint.setExtraValue(r); dataPoint.setNode(getMarker(nHours(r))); data.add(dataPoint); } sequenceChart.getData().add(series); series.getNode().setStyle("-fx-opacity: 0.0; -fx-stroke-width: 0px;"); avg = new XYChart.Series<>(); avg.setName("Average"); XYChart.Data<Number,Number> a1 = new XYChart.Data<Number,Number>(s, overallAverage); a1.setExtraValue(tip); avg.getData().add(a1); addTooltip(a1); XYChart.Data<Number,Number> a2 = new XYChart.Data<Number,Number>(e, overallAverage); a2.setExtraValue(tip); avg.getData().add(a2); addTooltip(a2); sequenceChart.getData().add(avg); avg.getNode().setStyle("-fx-opacity: 0.5; -fx-stroke-width: 10px;"); } /*------------------------------------------------------------------------------ * * Private Utility Methods * *----------------------------------------------------------------------------*/ private double nHours(RestCycle r) { double diff = r.endTime - r.startTime; double seconds = diff/1000; double minutes = seconds/60; double hours = minutes/60; return hours; } private Node getMarker(double hrs) { double size = ((Math.log(hrs/3.0)/Math.log(2.0))+1)*3; if (size < 3) size = 3; Circle c = new Circle(size); c.setFill(Color.web("#0000ff", 0.5)); c.setStroke(Color.web("#0000ff")); c.setStrokeWidth(1.0); return c; } private void addPeriod(ObservableList<XYChart.Data<Number, Number>> data, RestCycle r) { addPoint(data, r, r.startTime); addPoint(data, r, r.endTime); } private void addPoint(ObservableList<XYChart.Data<Number, Number>> data, RestCycle r, long timestamp) { Calendar c = Calendar.getInstance(); c.setTimeInMillis(timestamp); double time = c.get(Calendar.HOUR_OF_DAY) + ((double)c.get(Calendar.MINUTE))/60; time = time % 24; final XYChart.Data<Number,Number> dataPoint = new XYChart.Data<Number,Number>(time, r.avgLoss()); dataPoint.setExtraValue(r); data.add(dataPoint); addTooltip(dataPoint); } private void addTooltip(final XYChart.Data<Number,Number> dataPoint) { dataPoint.nodeProperty().addListener(new ChangeListener<Node>() { @Override public void changed(ObservableValue<? extends Node> observable, Node oldValue, Node newValue) { if (newValue != null) { String tip = (dataPoint.getExtraValue() instanceof String) ? (String)dataPoint.getExtraValue() : genTooltip((RestCycle)dataPoint.getExtraValue()); Tooltip.install(newValue, new Tooltip(tip)); dataPoint.nodeProperty().removeListener(this); } } }); } private double hours(long millis) {return ((double)(millis))/(60 * 60 * 1000); } private String genTooltip(RestCycle rest) { double period = hours(rest.endTime - rest.startTime); double loss = rest.startRange - rest.endRange; String date = String.format("%1$tm/%1$td %1$tH:%1$tM", new Date(rest.startTime)); return String.format( "Date: %s\n" + "Elapsed (HH:MM): %02d:%02d\n" + "Loss: %3.2f %s\n" + "Loss/hr: %3.2f", date, (int)period, (int)((period%1)*60), loss, units, loss/period ); } static class DateLabelGenerator extends StringConverter<Number> { TimeStringConverter hmConverter = new TimeStringConverter("HH:mm"); TimeStringConverter mdConverter = new TimeStringConverter("MM/dd"); String lastMD = ""; @Override public String toString(Number t) { Date d = new Date(t.longValue()*(1000)); String hourAndMinute = hmConverter.toString(d); String monthAndDay = mdConverter.toString(d); if (lastMD.equals(monthAndDay)) return hourAndMinute; lastMD = monthAndDay; return hourAndMinute + "\n" + monthAndDay; } @Override public Number fromString(String string) { return Long.valueOf(string); } } }