/* * TimeBasedChart.java - Copyright(c) 2013 Joe Pasqua * Provided under the MIT License. See the LICENSE file for details. * Created: Aug 25, 2013 */ package org.noroomattheinn.fxextensions; import com.google.common.collect.Range; import java.util.Date; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.geometry.Point2D; import javafx.scene.Cursor; import javafx.scene.chart.NumberAxis; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.layout.AnchorPane; import javafx.util.StringConverter; import javafx.util.converter.TimeStringConverter; /** * TimeBasedChart * Notes: * - This chartBackground internally works with time in second - not milliseconds. * - It is completely an implementation detail that the XAxis Tick Formatter * works. It assumes that it will be called with data in x axis order. It does, * but there is no guarantee of that. * - The minorTicksForX value is carefully chosen to make the number of ticks * compatible with the average label size on the axis. * * @author Joe Pasqua <joe at NoRoomAtTheInn dot org> */ public class TimeBasedChart { /*------------------------------------------------------------------------------ * * Constants and Enums * *----------------------------------------------------------------------------*/ private static final int minorTicksForX = 10; private static final int minorTicksForY = 5; private static final Range<Long> xRange = Range.closed(secondsFromMinutes(10), secondsFromDays(30)); private static final Range<Double> yRange = Range.closed(25.0, 500.0); /*------------------------------------------------------------------------------ * * Internal State * *----------------------------------------------------------------------------*/ private final Label readout; private NumberAxis xAxis, yAxis; private VTLineChart lineChart; private ChartUtils utils; /*============================================================================== * ------- ------- * ------- Public Interface To This Class ------- * ------- ------- *============================================================================*/ public TimeBasedChart(AnchorPane container, Label readout) { this.readout = readout; createChart(); } public VTLineChart getChart() { return lineChart; } public void addContextMenu(ContextMenu cm) { utils.enableContextMenu(cm); } public void centerTime(long timeInMillis) { long time = secondsFromMillis(timeInMillis); double lowerBound = xAxis.getLowerBound(); double upperBound = xAxis.getUpperBound(); double centerOffset = ((upperBound + lowerBound)/2) - lowerBound; double newLower = time - centerOffset; xAxis.setLowerBound(newLower); xAxis.setUpperBound(newLower + (upperBound - lowerBound)); yAxis.setAutoRanging(true); } /*------------------------------------------------------------------------------ * * PRIVATE - The guts of chartBackground creation * *----------------------------------------------------------------------------*/ private void createChart() { long nowInSeconds = secondsFromMillis(System.currentTimeMillis()); long nowInMinutes = minutesFromSeconds(nowInSeconds); xAxis = new NumberAxis( secondsFromMinutes(nowInMinutes - 23 * 60), secondsFromMinutes(nowInMinutes + 60), (double)secondsFromMinutes(24 * 60)/(double)minorTicksForX); xAxis.setAnimated(false); xAxis.setAutoRanging(false); xAxis.setTickLabelFormatter(new DateLabelGenerator()); yAxis = new NumberAxis(0.0, 250.0, (250.0-0)/minorTicksForY); yAxis.setAnimated(false); yAxis.setAutoRanging(true); yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis) { @Override public String toString(Number object) { return String.format("%3.1f", object); } }); lineChart = new VTLineChart(xAxis, yAxis); //lineChart.setMinorTicksForY(minorTicksForY); lineChart.setCreateSymbols(false); lineChart.setAlternativeRowFillVisible(false); lineChart.setAnimated(false); lineChart.setLegendVisible(false); lineChart.setCursor(Cursor.CROSSHAIR); AnchorPane.setTopAnchor(lineChart, 0.0); AnchorPane.setBottomAnchor(lineChart, 0.0); AnchorPane.setLeftAnchor(lineChart, 0.0); AnchorPane.setRightAnchor(lineChart, 0.0); utils = new ChartUtils(lineChart); utils.enableScrolling(); utils.enableZooming(xRange, yRange, minorTicksForX, minorTicksForX); utils.getValueProperty().addListener(new ChangeListener<Point2D>() { @Override public void changed(ObservableValue<? extends Point2D> ov, Point2D t, Point2D t1) { updateReadout(t1); } }); } 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); } } /*------------------------------------------------------------------------------ * * Private Utility Methods * *----------------------------------------------------------------------------*/ private static final TimeStringConverter timeFormatter = new TimeStringConverter("MM/dd HH:mm"); private void updateReadout(Point2D point) { if (readout == null) return; long time = millisFromSeconds( xAxis.getValueForDisplay(point.getX()).longValue()); double value = yAxis.getValueForDisplay(point.getY()).doubleValue(); readout.setText( String.format("[%s: %3.1f]", timeFormatter.toString(new Date(time)), value)); } private static long secondsFromMillis(long timeInMillis) { return timeInMillis/1000; } private static long millisFromSeconds(long timeInMillis) { return timeInMillis*1000; } private static long minutesFromSeconds(long timeInSeconds) { return timeInSeconds / 60; } private static long secondsFromMinutes(long timeInMinutes) { return timeInMinutes * 60; } private static long secondsFromHours(long timeInHours) { return secondsFromMinutes(timeInHours * 60); } private static long secondsFromDays(long timeInDays) { return secondsFromHours(timeInDays * 24); } }