eu.hansolo.tilesfx.skins.TimelineTileSkin Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of tilesfx Show documentation
Show all versions of tilesfx Show documentation
TilesFX is a JavaFX library containing tiles for dashboards
The newest version!
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2016-2021 Gerrit Grunwald.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.hansolo.tilesfx.skins;
import eu.hansolo.tilesfx.Section;
import eu.hansolo.tilesfx.Tile;
import eu.hansolo.tilesfx.chart.ChartData;
import eu.hansolo.tilesfx.events.TileEvt;
import eu.hansolo.tilesfx.fonts.Fonts;
import eu.hansolo.tilesfx.tools.DoubleExponentialSmoothingForLinearSeries;
import eu.hansolo.tilesfx.tools.DoubleExponentialSmoothingForLinearSeries.Model;
import eu.hansolo.tilesfx.tools.Helper;
import eu.hansolo.tilesfx.tools.MovingAverage;
import eu.hansolo.tilesfx.tools.NiceScale;
import eu.hansolo.tilesfx.tools.TimeData;
import eu.hansolo.toolbox.Statistics;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.collections.ListChangeListener;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Group;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static eu.hansolo.tilesfx.tools.Helper.clamp;
import static eu.hansolo.tilesfx.tools.Helper.enableNode;
/**
* User: hansolo
* Date: 13.09.19
* Time: 03:12
*/
public class TimelineTileSkin extends TileSkin {
private static final int SEC_MONTH = 2_592_000;
private static final int SEC_DAY = 86_400;
private static final int SEC_HOUR = 3_600;
private static final int SEC_MINUTE = 60;
private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("MM");
private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("dd");
private static final DateTimeFormatter HOUR_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
private static final DateTimeFormatter MINUTE_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
private static final DateTimeFormatter SECOND_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
private DateTimeFormatter DTF = DateTimeFormatter.ofPattern("dd.YY HH:mm");
private DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
private Text titleText;
private Text valueText;
private Text upperUnitText;
private Line fractionLine;
private Text unitText;
private VBox unitFlow;
private HBox valueUnitFlow;
private Text averageText;
private Text averageText2;
private Text minText;
private Text maxText;
private Text highText;
private Text lowText;
private Text text;
private Text timeSpanText;
private Rectangle graphBounds;
private Map dots;
private Path path;
private Group dotGroup;
private Rectangle stdDeviationArea;
private Line thresholdLine;
private Line lowerThresholdLine;
private Line averageLine;
private Group sectionGroup;
private Map sections;
private Map percentageInSections;
private Group percentageInSectionGroup;
private LinearGradient gradient;
private double low;
private double high;
private double stdDeviation;
private int noOfDatapoints;
private int maxNoOfDatapoints;
private List dataList;
private List reducedDataList;
private Duration timePeriod;
private MovingAverage movingAverage;
private InvalidationListener periodListener;
private NiceScale niceScaleY;
private List horizontalTickLines;
private double horizontalLineOffset;
private List verticalTickLines;
private double tickLabelFontSize;
private List tickLabelsX;
private List tickLabelsY;
private Color tickLineColor;
private Color tickLabelColor;
private Text trendText;
private Instant lastUpdate;
private double dotRadius;
private EventHandler mouseListener;
private Tooltip dotTooltip;
// ******************** Constructors **************************************
public TimelineTileSkin(final Tile TILE) {
super(TILE);
}
// ******************** Initialization ************************************
@Override protected void initGraphics() {
super.initGraphics();
dotTooltip = new Tooltip("");
dotTooltip.setAutoHide(true);
dotTooltip.setHideDelay(javafx.util.Duration.seconds(0));
dotTooltip.setShowDuration(javafx.util.Duration.seconds(5));
periodListener = o -> handleEvents("PERIOD");
mouseListener = e -> handleMouseEvents(e);
timeFormatter = DateTimeFormatter.ofPattern("HH:mm", tile.getLocale());
timePeriod = tile.getTimePeriod();
if (tile.isAutoScale()) { tile.calcAutoScale(); }
niceScaleY = new NiceScale(minValue, tile.getMaxValue());
niceScaleY.setMaxTicks(5);
tickLineColor = Color.color(tile.getChartGridColor().getRed(), tile.getChartGridColor().getGreen(), tile.getChartGridColor().getBlue(), 0.5);
tickLabelColor = tile.getTickLabelColor();
horizontalTickLines = new ArrayList<>(5);
verticalTickLines = new ArrayList<>(16);
tickLabelsX = new ArrayList<>(16);
tickLabelsY = new ArrayList<>(5);
int noOfVerticalLines = getNoOfVerticalLines(Instant.now(), timePeriod);
for (long i = 0 ; i < noOfVerticalLines ; i++) {
Line vLine = new Line(0, 0, 0, 0);
vLine.getStrokeDashArray().addAll(1.0, 2.0);
vLine.setStroke(Color.TRANSPARENT);
vLine.setMouseTransparent(true);
verticalTickLines.add(vLine);
Text tickLabelX = new Text("");
tickLabelX.setTextOrigin(VPos.BOTTOM);
tickLabelX.setMouseTransparent(true);
tickLabelsX.add(tickLabelX);
}
for (int i = 0 ; i < 5 ; i++) {
Line hLine = new Line(0, 0, 0, 0);
hLine.getStrokeDashArray().addAll(1.0, 2.0);
hLine.setStroke(Color.TRANSPARENT);
hLine.setMouseTransparent(true);
horizontalTickLines.add(hLine);
Text tickLabelY = new Text("");
tickLabelY.setFill(Color.TRANSPARENT);
tickLabelY.setMouseTransparent(true);
tickLabelsY.add(tickLabelY);
}
low = maxValue;
high = minValue;
stdDeviation = 0;
movingAverage = tile.getMovingAverage();
dataList = new ArrayList<>();
reducedDataList = new ArrayList<>();
dotRadius = 3;
noOfDatapoints = calcNumberOfDatapointsForPeriod(timePeriod);
maxNoOfDatapoints = calcNumberOfDatapointsForPeriod(tile.getMaxTimePeriod());
graphBounds = new Rectangle(PREFERRED_WIDTH * 0.05, PREFERRED_HEIGHT * 0.5, PREFERRED_WIDTH * 0.9, PREFERRED_HEIGHT * 0.45);
tile.setAveragingPeriod(noOfDatapoints);
titleText = new Text(tile.getTitle());
titleText.setFill(tile.getTitleColor());
Helper.enableNode(titleText, !tile.getTitle().isEmpty());
valueText = new Text(String.format(locale, formatString, tile.getValue()));
valueText.setFill(tile.getValueColor());
Helper.enableNode(valueText, tile.isValueVisible());
upperUnitText = new Text("");
upperUnitText.setFill(tile.getUnitColor());
Helper.enableNode(upperUnitText, !tile.getUnit().isEmpty());
fractionLine = new Line();
unitText = new Text(tile.getUnit());
unitText.setFill(tile.getUnitColor());
Helper.enableNode(unitText, !tile.getUnit().isEmpty());
unitFlow = new VBox(upperUnitText, unitText);
unitFlow.setAlignment(Pos.CENTER_RIGHT);
valueUnitFlow = new HBox(valueText, unitFlow);
valueUnitFlow.setAlignment(Pos.BOTTOM_RIGHT);
valueUnitFlow.setMouseTransparent(true);
averageText = new Text(String.format(locale, "\u2300 " + formatString, tile.getAverage()));
averageText.setFill(Tile.FOREGROUND);
Helper.enableNode(averageText, tile.isAverageVisible());
averageText2 = new Text(String.format(locale, "\u2300 " + formatString, tile.getAverage()));
averageText2.setFill(Tile.FOREGROUND);
Helper.enableNode(averageText2, tile.isAverageVisible());
minText = new Text();
minText.setTextOrigin(VPos.TOP);
minText.setFill(tile.getValueColor());
maxText = new Text();
maxText.setTextOrigin(VPos.BOTTOM);
maxText.setFill(tile.getValueColor());
highText = new Text();
highText.setTextOrigin(VPos.BOTTOM);
highText.setFill(tile.getValueColor());
lowText = new Text();
lowText.setTextOrigin(VPos.TOP);
lowText.setFill(tile.getValueColor());
text = new Text(tile.getText());
text.setTextOrigin(VPos.TOP);
text.setFill(tile.getTextColor());
timeSpanText = new Text("");
timeSpanText.setTextOrigin(VPos.TOP);
timeSpanText.setFill(tile.getTextColor());
Helper.enableNode(timeSpanText, !tile.isTextVisible());
stdDeviationArea = new Rectangle();
Helper.enableNode(stdDeviationArea, tile.isAverageVisible());
thresholdLine = new Line();
thresholdLine.setStroke(tile.getThresholdColor());
thresholdLine.getStrokeDashArray().addAll(PREFERRED_WIDTH * 0.005, PREFERRED_WIDTH * 0.005);
Helper.enableNode(thresholdLine, tile.isThresholdVisible());
lowerThresholdLine = new Line();
lowerThresholdLine.setStroke(tile.getLowerThresholdColor());
lowerThresholdLine.getStrokeDashArray().addAll(PREFERRED_WIDTH * 0.005, PREFERRED_WIDTH * 0.005);
Helper.enableNode(lowerThresholdLine, tile.isLowerThresholdVisible());
averageLine = new Line();
averageLine.setStroke(Tile.FOREGROUND);
averageLine.getStrokeDashArray().addAll(PREFERRED_WIDTH * 0.005, PREFERRED_WIDTH * 0.005);
Helper.enableNode(averageLine, tile.isAverageVisible());
sections = new HashMap<>();
tile.getSections().forEach(section -> {
Rectangle sectionRect = new Rectangle();
sectionRect.setMouseTransparent(true);
sections.put(section, sectionRect);
});
sectionGroup = new Group();
sectionGroup.getChildren().addAll(sections.values());
Helper.enableNode(sectionGroup, tile.getSectionsVisible());
percentageInSections = new HashMap<>();
tile.getSections().forEach(section -> {
Label sectionLabel = new Label();
sectionLabel.setAlignment(Pos.CENTER_RIGHT);
sectionLabel.setTextFill(tile.getTextColor());
percentageInSections.put(section, sectionLabel);
});
percentageInSectionGroup = new Group();
percentageInSectionGroup.getChildren().setAll(percentageInSections.values());
Helper.enableNode(percentageInSectionGroup, tile.getSectionsVisible());
trendText = new Text("");
trendText.setTextOrigin(VPos.TOP);
trendText.setFill(tile.getTextColor());
lastUpdate = Instant.now();
path = new Path();
path.setMouseTransparent(true);
path.setStrokeLineJoin(StrokeLineJoin.ROUND);
path.setStrokeLineCap(StrokeLineCap.ROUND);
dots = new LinkedHashMap<>(noOfDatapoints);
dotGroup = new Group();
if (tile.getDataPointsVisible()) {
dotGroup.getChildren().setAll(dots.values());
dotGroup.getChildren().add(path);
} else {
dotGroup.getChildren().setAll(path);
}
getPane().getChildren().addAll(titleText, valueUnitFlow, fractionLine, sectionGroup, stdDeviationArea, thresholdLine, lowerThresholdLine, dotGroup, percentageInSectionGroup, averageLine, averageText, averageText2, minText, maxText, highText, lowText, trendText, timeSpanText, text);
getPane().getChildren().addAll(verticalTickLines);
getPane().getChildren().addAll(horizontalTickLines);
getPane().getChildren().addAll(tickLabelsX);
getPane().getChildren().addAll(tickLabelsY);
TimerTask timerTask = new TimerTask() {
@Override public void run() {
Platform.runLater(() -> checkForOutdated());
}
};
Timer timer = new Timer("Timer");
timer.scheduleAtFixedRate(timerTask, 1000, 500);
}
@Override protected void registerListeners() {
super.registerListeners();
tile.timePeriodProperty().addListener(periodListener);
tile.getChartData().addListener((ListChangeListener) c -> {
while(c.next()) {
if (c.wasAdded()) {
c.getAddedSubList().forEach(chartData -> addData(chartData));
}
}
Set dataSet = tile.getChartData().stream().filter(data -> !dataList.contains(data)).collect(Collectors.toSet());
Platform.runLater(() -> tile.removeChartData(new ArrayList<>(dataSet)));
});
tile.getSections().addListener((ListChangeListener) c -> {
while(c.next()) {
if (c.wasAdded()) {
c.getAddedSubList().forEach(section -> {
Rectangle sectionRect = new Rectangle();
sectionRect.setMouseTransparent(true);
sections.put(section, sectionRect);
});
} else if (c.wasRemoved()) {
c.getRemoved().forEach(section -> sections.remove(section));
}
}
sectionGroup.getChildren().setAll(sections.values());
resize();
});
}
// ******************** Methods *******************************************
@Override protected void handleEvents(final String EVENT_TYPE) {
super.handleEvents(EVENT_TYPE);
if(tile.isAnimated()) { tile.setAnimated(false); }
if (TileEvt.VISIBILITY.getName().equals(EVENT_TYPE)) {
Helper.enableNode(titleText, !tile.getTitle().isEmpty());
Helper.enableNode(text, tile.isTextVisible());
Helper.enableNode(valueText, tile.isValueVisible());
Helper.enableNode(unitFlow, !tile.getUnit().isEmpty());
Helper.enableNode(timeSpanText, !tile.isTextVisible());
Helper.enableNode(averageLine, tile.isAverageVisible());
Helper.enableNode(averageText, tile.isAverageVisible());
Helper.enableNode(averageText2, tile.isAverageVisible());
Helper.enableNode(stdDeviationArea, tile.isAverageVisible());
Helper.enableNode(thresholdLine, tile.isThresholdVisible());
Helper.enableNode(lowerThresholdLine, tile.isLowerThresholdVisible());
Helper.enableNode(sectionGroup, tile.getSectionsVisible());
Helper.enableNode(percentageInSectionGroup, tile.getSectionsVisible());
Helper.enableNode(trendText, tile.isTrendVisible());
redraw();
} /*else if (TileEvt.VALUE.getName().equals(EVENT_TYPE)) {
double value = clamp(minValue, maxValue, tile.getValue());
tile.getChartData().add(new ChartData("", value, Instant.now()));
} else if (TileEvt.SECTION.getName().equals(EVENT_TYPE)) {
percentageInSections.clear();
tile.getSections().forEach(section -> {
Label sectionLabel = new Label();
sectionLabel.setAlignment(Pos.CENTER_RIGHT);
sectionLabel.setTextFill(tile.getTextColor());
percentageInSections.put(section, sectionLabel);
});
percentageInSectionGroup.getChildren().setAll(percentageInSections.values());
} else if (TileEvt.TIME_PERIOD.getName().equals(EVENT_TYPE)) {
timePeriod = tile.getTimePeriod();
noOfDatapoints = calcNumberOfDatapointsForPeriod(timePeriod);
maxNoOfDatapoints = calcNumberOfDatapointsForPeriod(tile.getMaxTimePeriod());
timeSpanText.setText(createTimeSpanText());
tile.setAveragingPeriod(noOfDatapoints);
// Add initial values
dots.values().forEach(dot -> {
dot.removeEventHandler(MouseEvent.MOUSE_ENTERED, mouseListener);
dot.removeEventHandler(MouseEvent.MOUSE_EXITED, mouseListener);
});
dots.clear();
reducedDataList.forEach(data -> {
Circle dot = new Circle(dotRadius);
dot.addEventHandler(MouseEvent.MOUSE_ENTERED, mouseListener);
dot.addEventHandler(MouseEvent.MOUSE_EXITED, mouseListener);
dots.put(data, dot);
});
if (tile.getDataPointsVisible()) {
dotGroup.getChildren().setAll(dots.values());
dotGroup.getChildren().add(path);
} else {
dotGroup.getChildren().setAll(path);
}
redraw();
} else if (TileEvt.REGIONS_ON_TOP.getName().equals(EVENT_TYPE)) {
valueUnitFlow.setPrefWidth(width - size * 0.1);
valueUnitFlow.relocate(size * 0.05, contentBounds.getY());
fractionLine.setStartX(width - 0.17 * size);
fractionLine.setStartY(tile.getTitle().isEmpty() ? size * 0.2 : size * 0.3);
fractionLine.setEndX(width - 0.05 * size);
fractionLine.setEndY(tile.getTitle().isEmpty() ? size * 0.2 : size * 0.3);
fractionLine.setStroke(tile.getUnitColor());
fractionLine.setStrokeWidth(size * 0.005);
} else if (TileEvt.CLEAR_DATA.getName().equals(EVENT_TYPE)) {
tile.clearChartData();
dataList.clear();
reducedDataList.clear();
handleCurrentValue(minValue);
Platform.runLater(() -> {
path.getElements().clear();
dots.clear();
dotGroup.getChildren().clear();
});
} else if (TileEvt.THRESHOLD_EXCEEDED.equals(EVENT_TYPE)) {
} else if (TileEvt.THRESHOLD_UNDERRUN.equals(EVENT_TYPE)) {
} else if (TileEvt.LOWER_THRESHOLD_EXCEEDED.equals(EVENT_TYPE)) {
} else if (TileEvt.LOWER_THRESHOLD_UNDERRUN.equals(EVENT_TYPE)) {
}
*/
}
private void handleMouseEvents(final MouseEvent e) {
EventType extends Event> type = e.getEventType();
Circle dot = (Circle) e.getSource();
ChartData data = dots.entrySet().stream().filter(entry -> entry.getValue().equals(dot)).map(entry -> entry.getKey()).findAny().orElse(null);
if (MouseEvent.MOUSE_ENTERED.equals(type)) {
if (null != data) {
dotTooltip.setX(e.getScreenX());
dotTooltip.setY(e.getScreenY());
LocalDateTime localDateTime = LocalDateTime.ofInstant(data.getTimestamp(), tile.getZoneId());
dotTooltip.setText(String.join("\n", DTF.format(localDateTime), String.format(tile.getLocale(), String.join(" ", formatString, tile.getUnit()), data.getValue())));
dotTooltip.show(tile.getScene().getWindow());
}
} else if (MouseEvent.MOUSE_EXITED.equals(type)) {
dotTooltip.hide();
}
}
@Override protected void handleCurrentValue(final double VALUE) {
low = reducedDataList.stream().min(Comparator.comparingDouble(ChartData::getValue)).map(data -> data.getValue()).orElse(tile.getLowerThreshold());
high = reducedDataList.stream().max(Comparator.comparingDouble(ChartData::getValue)).map(data -> data.getValue()).orElse(tile.getThreshold());
range = (maxValue - minValue);
Instant now = Instant.now();
lastUpdate = now;
long maxTime = now.getEpochSecond();
long minTime = now.minus(timePeriod.toSeconds(), ChronoUnit.SECONDS).getEpochSecond();
TimeUnit resolution = tile.getTimePeriodResolution();
long resolutionStep;
switch(resolution) {
case DAYS : resolutionStep = Helper.SECONDS_PER_DAY; break;
case HOURS : resolutionStep = Helper.SECONDS_PER_HOUR; break;
case MINUTES: resolutionStep = Helper.SECONDS_PER_MINUTE; break;
case SECONDS:
default : resolutionStep = 1; break;
}
double minX = graphBounds.getX();
double maxX = minX + graphBounds.getWidth();
double minY = graphBounds.getY();
double maxY = minY + graphBounds.getHeight();
double stepX = graphBounds.getWidth() / timePeriod.getSeconds();
double stepY = graphBounds.getHeight() / range;
niceScaleY.setMinMax(minValue, maxValue);
int lineCountY = 1;
int tickLabelOffsetY = 1;
double tickSpacingY = niceScaleY.getTickSpacing();
double tickStepY = tickSpacingY * stepY;
double tickStartY = maxY - tickStepY;
if (tickSpacingY < minValue) {
tickLabelOffsetY = (int) (minValue / tickSpacingY) + 1;
tickStartY = maxY - (tickLabelOffsetY * tickSpacingY - minValue) * stepY;
}
verticalTickLines.forEach(line -> line.setStroke(Color.TRANSPARENT));
horizontalTickLines.forEach(line -> line.setStroke(Color.TRANSPARENT));
tickLabelsX.forEach(label -> label.setFill(Color.TRANSPARENT));
tickLabelsY.forEach(label -> label.setFill(Color.TRANSPARENT));
horizontalLineOffset = 0;
for (double y = tickStartY; Math.round(y) > minY; y -= tickStepY) {
Line line = horizontalTickLines.get(lineCountY);
Text label = tickLabelsY.get(lineCountY);
label.setText(String.format(locale, "%.0f", minValue + lineCountY * tickSpacingY));
label.setY(y + graphBounds.getHeight() * 0.03);
label.setFill(tickLabelColor);
horizontalLineOffset = Math.max(label.getLayoutBounds().getWidth(), horizontalLineOffset);
line.setStartX(minX);
line.setStartY(y);
line.setEndY(y);
line.setStroke(tickLineColor);
lineCountY++;
lineCountY = clamp(0, 4, lineCountY);
}
int lineCountX = 0;
ZonedDateTime dateTime;
for (long t = minTime ; t < maxTime ; t++) {
dateTime = ZonedDateTime.ofInstant(Instant.ofEpochSecond(t), tile.getZoneId());
double x = -1;
String timeText = "";
if (timePeriod.getSeconds() > Helper.SECONDS_PER_MONTH) {
if (1 == dateTime.getDayOfMonth() && 0 == dateTime.getHour() && 0 == dateTime.getMinute() && 0 == dateTime.getSecond()) { // Full day
x = minX + ((t - minTime) * stepX);
timeText = MONTH_FORMATTER.format(dateTime);
}
} else if (timePeriod.getSeconds() > Helper.SECONDS_PER_DAY) {
if (0 == dateTime.getHour() && 0 == dateTime.getMinute() && 0 == dateTime.getSecond()) { // Full day
x = minX + ((t - minTime) * stepX);
timeText = DAY_FORMATTER.format(dateTime);
}
} else if (timePeriod.getSeconds() > Helper.SECONDS_PER_DAY / 2) {
if (dateTime.getHour() % 2 == 0 && 0 == dateTime.getMinute() && 0 == dateTime.getSecond()) { // Full hour
x = minX + ((t - minTime) * stepX);
timeText = HOUR_FORMATTER.format(dateTime);
}
} else if (timePeriod.getSeconds() > Helper.SECONDS_PER_DAY / 4) {
if (0 == dateTime.getMinute() && 0 == dateTime.getSecond()) { // Full hour
x = minX + ((t - minTime) * stepX);
timeText = HOUR_FORMATTER.format(dateTime);
}
} else if (timePeriod.getSeconds() > Helper.SECONDS_PER_HOUR) {
if ((0 == dateTime.getMinute() || 30 == dateTime.getMinute()) && 0 == dateTime.getSecond()) { // Full hour and half hour
x = minX + ((t - minTime) * stepX);
timeText = HOUR_FORMATTER.format(dateTime);
}
} else if (timePeriod.getSeconds() > Helper.SECONDS_PER_MINUTE) {
if (0 == dateTime.getSecond() && dateTime.getMinute() % 5 == 0) { // 5 minutes
x = minX + ((t - minTime) * stepX);
timeText = MINUTE_FORMATTER.format(dateTime);
}
} else {
if (dateTime.getSecond() % 10 == 0) { // 10 seconds
x = minX + ((t - minTime) * stepX);
timeText = SECOND_FORMATTER.format(dateTime);
}
}
if (x > -1) {
x = minX + ((t - minTime) * stepX);
Line line = verticalTickLines.get(lineCountX);
Text label = tickLabelsX.get(lineCountX);
label.setText(timeText);
label.setX(x - (label.getLayoutBounds().getWidth() * 0.5));
label.setY(graphBounds.getY());
label.setFill(tickLabelColor);
line.setStartX(x);
line.setEndX(x);
line.setStartY(minY);
line.setEndY(maxY);
line.setStroke(tickLineColor);
lineCountX++;
}
}
if (tickLabelFontSize < 6) { horizontalLineOffset = 0; }
horizontalTickLines.forEach(line -> line.setEndX(maxX - horizontalLineOffset));
tickLabelsY.forEach(label -> label.setX(maxX - label.getLayoutBounds().getWidth()));
minText.setText(String.format(locale, formatString, minValue));
maxText.setText(String.format(locale, formatString, maxValue));
lowText.setText(String.format(locale, formatString, low));
highText.setText(String.format(locale, formatString, high));
minText.setX((maxX - minText.getLayoutBounds().getWidth()));
maxText.setX((maxX - maxText.getLayoutBounds().getWidth()));
// Draw dots and line
if (!reducedDataList.isEmpty()) {
if (tile.isStrokeWithGradient()) { setupGradient(); }
Iterator entries = dots.entrySet().iterator();
Map.Entry entry = (Map.Entry) entries.next();
ChartData data = entry.getKey();
Circle dot = entry.getValue();
path.getElements().clear();
path.getElements().add(new MoveTo(maxX - (maxTime - data.getTimestamp().getEpochSecond()) * stepX, maxY - Math.abs(minValue - Helper.clamp(minValue, maxValue, data.getValue())) * stepY));
for (long timeSlot = maxTime ; timeSlot >= minTime ; timeSlot -= resolutionStep) {
if (data.getTimestamp().getEpochSecond() > timeSlot - resolutionStep) {
dot.setCenterX(maxX - (maxTime - data.getTimestamp().getEpochSecond()) * stepX);
dot.setCenterY(maxY - Math.abs(minValue - Helper.clamp(minValue, maxValue, data.getValue())) * stepY);
dot.setFill(tile.isStrokeWithGradient() ? gradient : tile.getBarColor());
path.getElements().add(new LineTo(dot.getCenterX(), dot.getCenterY()));
if (entries.hasNext()) {
entry = (Map.Entry) entries.next();
data = entry.getKey();
dot = entry.getValue();
}
}
}
path.setStroke(tile.isStrokeWithGradient() ? gradient : tile.getBarColor());
if (tile.isSmoothing()) {
Helper.smoothPath(path, false);
}
sections.entrySet().forEach(e -> {
Section section = e.getKey();
Rectangle rectangle = e.getValue();
rectangle.setX(minX);
rectangle.setY(clamp(minY, maxY, maxY - Math.abs(minValue - section.getStop()) * stepY));
rectangle.setWidth(graphBounds.getWidth());
rectangle.setHeight(Math.abs(section.getStop() - section.getStart()) * stepY);
rectangle.setFill(section.getColor());
});
double average = Statistics.getAverage(reducedDataList.stream().map(ChartData::getValue).collect(Collectors.toList()));
double averageY = clamp(minY, maxY, maxY - Math.abs(minValue - average) * stepY);
averageLine.setStartX(minX);
averageLine.setStartY(averageY);
averageLine.setEndX(maxX);
averageLine.setEndY(averageY);
double threshold = tile.getThreshold();
double thresholdY = clamp(minY, maxY, maxY - Math.abs(minValue - threshold) * stepY);
thresholdLine.setStartX(minX);
thresholdLine.setStartY(thresholdY);
thresholdLine.setEndX(maxX);
thresholdLine.setEndY(thresholdY);
double lowerThreshold = tile.getLowerThreshold();
double lowerThresholdY = clamp(minY, maxY, maxY - Math.abs(minValue - lowerThreshold) * stepY);
lowerThresholdLine.setStartX(minX);
lowerThresholdLine.setStartY(lowerThresholdY);
lowerThresholdLine.setEndX(maxX);
lowerThresholdLine.setEndY(lowerThresholdY);
stdDeviationArea.setY(averageLine.getStartY() - (stdDeviation * 0.5 * stepY));
stdDeviationArea.setHeight(stdDeviation * stepY);
averageText.setText(String.format(locale, "\u2300 " + formatString, average));
averageText2.setText(String.format(locale, "\u2300 " + formatString, average));
}
if (tile.getShortenNumbers()) {
valueText.setText(Helper.shortenNumber((long) VALUE));
} else if (tile.getCustomDecimalFormatEnabled()) {
valueText.setText(decimalFormat.format(VALUE));
} else {
valueText.setText(String.format(locale, formatString, VALUE));
}
if (!tile.isTextVisible() && null != movingAverage.getTimeSpan()) {
timeSpanText.setText(createTimeSpanText());
text.setText(HOUR_FORMATTER.format(movingAverage.getLastEntry().getTimestampAsDateTime(tile.getZoneId())));
}
resizeDynamicText();
}
private void addData(final ChartData DATA) {
if (dataList.size() >= maxNoOfDatapoints) {
Collections.rotate(dataList, -1);
if (!dataList.isEmpty()) { dataList.set((noOfDatapoints - 1), DATA); }
} else {
dataList.add(DATA);
if (tile.isAveragingEnabled()) { movingAverage.addData(new TimeData(DATA.getValue(), DATA.getTimestamp())); }
}
Predicate isNotInTimePeriod = chartData -> !chartData.isWithinTimePeriod(Instant.now(), timePeriod);
reducedDataList.clear();
reducedDataList.addAll(dataList);
reducedDataList.removeIf(isNotInTimePeriod);
if (reducedDataList.size() == Integer.MAX_VALUE - 1 || reducedDataList.size() >= noOfDatapoints) {
Collections.rotate(reducedDataList, -1);
if (!reducedDataList.isEmpty()) { reducedDataList.set((noOfDatapoints - 1), DATA); }
}
Collections.sort(reducedDataList, Comparator.comparing(ChartData::getTimestamp).reversed());
dots.values().forEach(dot -> {
dot.removeEventHandler(MouseEvent.MOUSE_ENTERED, mouseListener);
dot.removeEventHandler(MouseEvent.MOUSE_EXITED, mouseListener);
});
dots.clear();
reducedDataList.forEach(data -> {
Circle dot = new Circle(dotRadius);
dot.addEventHandler(MouseEvent.MOUSE_ENTERED, mouseListener);
dot.addEventHandler(MouseEvent.MOUSE_EXITED, mouseListener);
dots.put(data, dot);
});
if (tile.getDataPointsVisible()) {
dotGroup.getChildren().setAll(dots.values());
dotGroup.getChildren().add(path);
} else {
dotGroup.getChildren().setAll(path);
}
int n = Helper.clamp(2, reducedDataList.size(), tile.getNumberOfValuesForTrendCalculation());
if (reducedDataList.size() > n) {
List firstNValues = reducedDataList.stream().map(ChartData::getValue).limit(n).collect(Collectors.toList());
Model model = DoubleExponentialSmoothingForLinearSeries.fit(firstNValues.stream().mapToDouble(Double::doubleValue).toArray(), 0.8, 0.2);
String forecast = String.format(tile.getLocale(), "%.0f", model.forecast(1)[0]);
double stepX = graphBounds.getWidth() / (noOfDatapoints - 1);
double trendAngle = (Helper.getAngleFromXY(0, DATA.getValue(), stepX, model.forecast(1)[0]) - 90);
if (90 <= trendAngle && trendAngle < 112.5) {
trendText.setText("\u2191");
} else if (112.5 <= trendAngle && trendAngle < 147.5) {
trendText.setText("\u2197");
} else if (147.5 <= trendAngle && trendAngle < 202.5) {
trendText.setText("\u2192");
} else if (202.5 <= trendAngle && trendAngle < 247.5) {
trendText.setText("\u2198");
} else if (247.5 <= trendAngle && trendAngle < 270) {
trendText.setText("\u2193");
} else {
trendText.setText("");
}
}
stdDeviation = Statistics.getStdDev(reducedDataList.stream().map(ChartData::getValue).collect(Collectors.toList()));
analyse(reducedDataList);
handleCurrentValue(DATA.getValue());
}
private void setupGradient() {
gradient = new LinearGradient(0, graphBounds.getY() + graphBounds.getHeight(), 0, graphBounds.getY(), false, CycleMethod.NO_CYCLE, tile.getGradientStops());
}
private int calcNumberOfDatapointsForPeriod(final Duration TIME_PERIOD) {
return Helper.calcNumberOfDatapointsForPeriod(TIME_PERIOD, tile.getTimePeriodResolution());
}
private String createTimeSpanText() {
long timeSpan = timePeriod.getSeconds();
StringBuilder timeSpanBuilder = new StringBuilder();
if (timeSpan > SEC_MONTH) { // 1 Month (30 days)
int months = (int)(timeSpan / SEC_MONTH);
double days = timeSpan % SEC_MONTH;
timeSpanBuilder.append(months).append("M");
if(days > 0) { timeSpanBuilder.append(String.format(Locale.US, "%.0f", days)).append("d"); }
} else if (timeSpan > SEC_DAY) { // 1 Day
int days = (int) (timeSpan / SEC_DAY);
double hours = (timeSpan - (days * SEC_DAY)) / SEC_HOUR;
timeSpanBuilder.append(days).append("d");
if (hours > 0) { timeSpanBuilder.append(String.format(Locale.US, "%.0f", hours)).append("h"); }
} else if (timeSpan > SEC_HOUR) { // 1 Hour
int hours = (int)(timeSpan / SEC_HOUR);
double minutes = (timeSpan - (hours * SEC_HOUR)) / SEC_MINUTE;
timeSpanBuilder.append(hours).append("h");
if (minutes > 0) { timeSpanBuilder.append(String.format(Locale.US, "%.0f", minutes)).append("m"); }
} else if (timeSpan > SEC_MINUTE) { // 1 Minute
int minutes = (int)(timeSpan / SEC_MINUTE);
double seconds = (timeSpan - (minutes * SEC_MINUTE));
timeSpanBuilder.append(minutes).append("m");
if (seconds > 0) { timeSpanBuilder.append(String.format(Locale.US, "%.0f", seconds)).append("s"); }
} else {
int seconds = (int)timeSpan;
timeSpanBuilder.append(seconds).append("s");
}
return timeSpanBuilder.toString();
}
@Override public void dispose() {
tile.timePeriodProperty().removeListener(periodListener);
super.dispose();
}
private void checkForOutdated() {
valueText.setOpacity(((Instant.now().toEpochMilli() - lastUpdate.toEpochMilli())) > tile.getTimeoutMs() ? 0.5 : 1.0);
}
private void analyse(final List clampedDataList) {
double noOfPointsInTimePeriod = clampedDataList.size();
percentageInSections.entrySet().forEach(entry -> {
double noOfPointsInSection = clampedDataList.stream().filter(chartData -> entry.getKey().contains(chartData.getValue())).mapToDouble(ChartData::getValue).count();
entry.getValue().setText(String.format(tile.getLocale(), "%.0f%%", ((noOfPointsInSection / noOfPointsInTimePeriod * 100))));
});
}
private int getNoOfVerticalLines(final Instant START, final Duration TIME_PERIOD) {
long maxTime = START.getEpochSecond();
long minTime = START.minus(TIME_PERIOD.toSeconds(), ChronoUnit.SECONDS).getEpochSecond();
int lineCountX = 0;
ZonedDateTime dateTime;
for (long t = minTime ; t < maxTime ; t++) {
dateTime = ZonedDateTime.ofInstant(Instant.ofEpochSecond(t), tile.getZoneId());
if (TIME_PERIOD.getSeconds() > Helper.SECONDS_PER_MONTH) {
if (1 == dateTime.getDayOfMonth() && 0 == dateTime.getHour() && 0 == dateTime.getMinute() && 0 == dateTime.getSecond()) { // Full day
lineCountX++;
}
} else if (TIME_PERIOD.getSeconds() > Helper.SECONDS_PER_DAY) {
if (0 == dateTime.getHour() && 0 == dateTime.getMinute() && 0 == dateTime.getSecond()) { // Full day
lineCountX++;
}
} else if (TIME_PERIOD.getSeconds() > Helper.SECONDS_PER_DAY / 2) {
if (dateTime.getHour() % 2 == 0 && 0 == dateTime.getMinute() && 0 == dateTime.getSecond()) { // Full hour
lineCountX++;
}
} else if (TIME_PERIOD.getSeconds() > Helper.SECONDS_PER_DAY / 4) {
if (0 == dateTime.getMinute() && 0 == dateTime.getSecond()) { // Full hour
lineCountX++;
}
} else if (TIME_PERIOD.getSeconds() > Helper.SECONDS_PER_HOUR) {
if ((0 == dateTime.getMinute() || 30 == dateTime.getMinute()) && 0 == dateTime.getSecond()) { // Full hour and half hour
lineCountX++;
}
} else if (TIME_PERIOD.getSeconds() > Helper.SECONDS_PER_MINUTE) {
if (0 == dateTime.getSecond() && dateTime.getMinute() % 5 == 0) { // 5 minutes
lineCountX++;
}
} else {
if (dateTime.getSecond() % 10 == 0) { // 10 seconds
lineCountX++;
}
}
}
return lineCountX;
}
// ******************** Resizing ******************************************
@Override protected void resizeDynamicText() {
double maxWidth = valueUnitFlow.isVisible() ? (width - (size * 0.275)) : (width - (size * 0.1));
double fontSize = size * 0.24;
valueText.setFont(Fonts.latoRegular(fontSize));
if (valueText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(valueText, maxWidth, fontSize); }
maxWidth = width - size * 0.7;
fontSize = size * 0.03;
averageText.setFont(Fonts.latoRegular(fontSize));
if (averageText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(averageText, maxWidth, fontSize); }
if (averageLine.getStartY() < graphBounds.getY() + graphBounds.getHeight() * 0.5) {
averageText.setY(averageLine.getStartY() + (size * 0.0425));
} else {
averageText.setY(averageLine.getStartY() - (size * 0.0075));
}
averageText.setVisible(fontSize > 6);
fontSize = size * 0.06;
minText.setFont(Fonts.latoRegular(fontSize));
if (minText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(minText, maxWidth, fontSize); }
minText.setY(height - size * 0.1);
maxText.setFont(Fonts.latoRegular(fontSize));
if (maxText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(maxText, maxWidth, fontSize); }
maxText.setY(graphBounds.getY() - size * 0.0175);
lowText.setFont(Fonts.latoRegular(fontSize));
if (lowText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(lowText, maxWidth, fontSize); }
lowText.setY(height - size * 0.1);
highText.setFont(Fonts.latoRegular(fontSize));
if (highText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(highText, maxWidth, fontSize); }
highText.setY(graphBounds.getY() - size * 0.0175);
trendText.setFont(Fonts.latoRegular(fontSize));
if (trendText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(trendText, maxWidth, fontSize); }
trendText.relocate((width - trendText.getLayoutBounds().getWidth()) * 0.25, height - size * 0.1);
averageText2.setFont(Fonts.latoRegular(fontSize));
if (averageText2.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(averageText2, maxWidth, fontSize); }
averageText2.relocate((width - averageText2.getLayoutBounds().getWidth()) * 0.75, height - size * 0.1);
maxWidth = width - size * 0.25;
fontSize = size * 0.06;
text.setFont(Fonts.latoRegular(fontSize));
if (text.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(text, maxWidth, fontSize); }
text.relocate(width - size * 0.05 - text.getLayoutBounds().getWidth(), height - size * 0.1);
maxWidth = width - size * 0.25;
fontSize = size * 0.06;
timeSpanText.setFont(Fonts.latoRegular(fontSize));
if (timeSpanText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(timeSpanText, maxWidth, fontSize); }
timeSpanText.relocate((width - timeSpanText.getLayoutBounds().getWidth()) * 0.5, height - size * 0.1);
percentageInSections.entrySet().forEach(entry -> {
entry.getValue().setFont(Fonts.latoRegular(size * 0.025));
entry.getValue().setPrefWidth(size * 0.065);
entry.getValue().relocate(size * 0.05, sections.get(entry.getKey()).getLayoutBounds().getCenterY() - entry.getValue().getLayoutBounds().getCenterY());
entry.getValue().setVisible(size * 0.025 > 6);
});
}
@Override protected void resizeStaticText() {
double maxWidth = width - size * 0.1;
double fontSize = size * textSize.factor;
boolean customFontEnabled = tile.isCustomFontEnabled();
Font customFont = tile.getCustomFont();
Font font = (customFontEnabled && customFont != null) ? Font.font(customFont.getFamily(), fontSize) : Fonts.latoRegular(fontSize);
titleText.setFont(font);
if (titleText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(titleText, maxWidth, fontSize); }
switch(tile.getTitleAlignment()) {
default :
case LEFT : titleText.relocate(size * 0.05, size * 0.05); break;
case CENTER: titleText.relocate((width - titleText.getLayoutBounds().getWidth()) * 0.5, size * 0.05); break;
case RIGHT : titleText.relocate(width - (size * 0.05) - titleText.getLayoutBounds().getWidth(), size * 0.05); break;
}
maxWidth = width - (width - size * 0.275);
fontSize = upperUnitText.getText().isEmpty() ? size * 0.12 : size * 0.10;
upperUnitText.setFont(Fonts.latoRegular(fontSize));
if (upperUnitText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(upperUnitText, maxWidth, fontSize); }
fontSize = upperUnitText.getText().isEmpty() ? size * 0.12 : size * 0.10;
unitText.setFont(Fonts.latoRegular(fontSize));
if (unitText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(unitText, maxWidth, fontSize); }
lowText.setX(size * 0.05);
highText.setX(size * 0.05);
averageText.setX(size * 0.15);
}
@Override protected void resize() {
super.resize();
graphBounds = new Rectangle(contentBounds.getX(), titleText.isVisible() ? size * 0.5 : size * 0.4, contentBounds.getWidth(), titleText.isVisible() ? height - size * 0.61 : height - size * 0.51);
tickLabelFontSize = graphBounds.getHeight() * 0.1;
Font tickLabelFont = Fonts.latoRegular(tickLabelFontSize);
tickLabelsY.forEach(label -> {
enableNode(label, tickLabelFontSize >= 6);
label.setFont(tickLabelFont);
});
horizontalTickLines.forEach(line -> line.setStrokeWidth(0.5));
double miniLabelFontSize = size * 0.022;
Font miniTickLabelFont = Fonts.latoRegular(miniLabelFontSize);
tickLabelsX.forEach(label -> {
enableNode(label, miniLabelFontSize >= 6);
label.setFont(miniTickLabelFont);
});
verticalTickLines.forEach(line -> line.setStrokeWidth(0.5));
stdDeviationArea.setX(graphBounds.getX());
stdDeviationArea.setWidth(graphBounds.getWidth());
thresholdLine.getStrokeDashArray().setAll(graphBounds.getWidth() * 0.01, graphBounds.getWidth() * 0.01);
lowerThresholdLine.getStrokeDashArray().setAll(graphBounds.getWidth() * 0.01, graphBounds.getWidth() * 0.01);
averageLine.getStrokeDashArray().setAll(graphBounds.getWidth() * 0.01, graphBounds.getWidth() * 0.01);
handleCurrentValue(Double.parseDouble(valueText.getText()));
if (noOfDatapoints < 60) {
dotRadius = size * 0.01;
} else if (noOfDatapoints < 3600) {
dotRadius = size * 0.0075;
} else {
dotRadius = size * 0.005;
}
dots.values().forEach(dot -> dot.setRadius(dotRadius));
path.setStrokeWidth(size * 0.01);
if (tile.isStrokeWithGradient()) { setupGradient(); }
resizeStaticText();
resizeDynamicText();
valueUnitFlow.setPrefWidth(width - size * 0.1);
valueUnitFlow.relocate(size * 0.05, contentBounds.getY());
valueUnitFlow.setMaxHeight(valueText.getFont().getSize());
fractionLine.setStartX(width - 0.17 * size);
fractionLine.setStartY(tile.getTitle().isEmpty() ? size * 0.2 : size * 0.3);
fractionLine.setEndX(width - 0.05 * size);
fractionLine.setEndY(tile.getTitle().isEmpty() ? size * 0.2 : size * 0.3);
fractionLine.setStroke(tile.getUnitColor());
fractionLine.setStrokeWidth(size * 0.005);
unitFlow.setTranslateY(-size * 0.005);
}
@Override protected void redraw() {
super.redraw();
titleText.setText(tile.getTitle());
text.setText(tile.getText());
if (tile.getUnit().contains("/")) {
String[] units = tile.getUnit().split("/");
upperUnitText.setText(units[0]);
unitText.setText(units[1]);
Helper.enableNode(fractionLine, true);
} else {
upperUnitText.setText(" ");
unitText.setText(tile.getUnit());
Helper.enableNode(fractionLine, false);
}
if (!tile.getDescription().isEmpty()) { text.setText(tile.getDescription()); }
if (tile.isTextVisible()) {
text.setText(tile.getText());
} else if (!tile.isTextVisible() && null != movingAverage.getTimeSpan()) {
timeSpanText.setText(createTimeSpanText());
text.setText(timeFormatter.format(movingAverage.getLastEntry().getTimestampAsDateTime(tile.getZoneId())));
}
resizeStaticText();
titleText.setFill(tile.getTitleColor());
valueText.setFill(tile.getValueColor());
upperUnitText.setFill(tile.getUnitColor());
fractionLine.setStroke(tile.getUnitColor());
unitText.setFill(tile.getUnitColor());
minText.setFill(tile.getValueColor());
maxText.setFill(tile.getValueColor());
lowText.setFill(tile.getValueColor());
highText.setFill(tile.getValueColor());
trendText.setFill(tile.getTextColor());
text.setFill(tile.getTextColor());
averageText.setFill(tile.getForegroundColor());
averageText2.setFill(tile.getForegroundColor());
timeSpanText.setFill(tile.getTextColor());
stdDeviationArea.setFill(Helper.getColorWithOpacity(Tile.FOREGROUND, 0.1));
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy