-
Notifications
You must be signed in to change notification settings - Fork 93
/
DataPointTooltip.java
312 lines (267 loc) · 12.9 KB
/
DataPointTooltip.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
/*
* Copyright (c) 2016 European Organisation for Nuclear Research (CERN), All Rights Reserved.
*/
package de.gsi.chart.plugins;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import de.gsi.chart.Chart;
import de.gsi.chart.XYChart;
import de.gsi.chart.axes.Axis;
import de.gsi.chart.renderer.Renderer;
import de.gsi.chart.renderer.spi.ErrorDataSetRenderer;
import de.gsi.dataset.DataSet;
import de.gsi.dataset.GridDataSet;
import de.gsi.dataset.spi.utils.Tuple;
/**
* A tool tip label appearing next to the mouse cursor when placed over a data point's symbol. If symbols are not
* created/shown for given plot, the tool tip is shown for the closest data point that is within the
* {@link #pickingDistanceProperty()} from the mouse cursor.
* <p>
* CSS style class name: {@value #STYLE_CLASS_LABEL}
* <p>
* TODO: extend so that label = new Label(); is a generic object and can also be overwritten with
* another implementation (<-> advanced interactor) additional add/remove listener are needed to
* edit/update the custom object based on DataPoint (for the time being private class)
*
* @author Grzegorz Kruk
*/
public class DataPointTooltip extends AbstractDataFormattingPlugin {
/**
* Name of the CSS class of the tool tip label.
*/
public static final String STYLE_CLASS_LABEL = "chart-datapoint-tooltip-label";
/**
* The default distance between the data point coordinates and mouse cursor that triggers showing the tool tip
* label.
*/
public static final int DEFAULT_PICKING_DISTANCE = 5;
private static final int LABEL_X_OFFSET = 15;
private static final int LABEL_Y_OFFSET = 5;
private final Label label = new Label();
private final DoubleProperty pickingDistance = new SimpleDoubleProperty(this, "pickingDistance", DataPointTooltip.DEFAULT_PICKING_DISTANCE) {
@Override
protected void invalidated() {
if (get() <= 0) {
throw new IllegalArgumentException("The " + getName() + " must be a positive value");
}
}
};
private final EventHandler<MouseEvent> mouseMoveHandler = this::updateToolTip;
/**
* Creates a new instance of DataPointTooltip class with {{@link #pickingDistanceProperty() picking distance}
* initialized to {@value #DEFAULT_PICKING_DISTANCE}.
*/
public DataPointTooltip() {
label.getStyleClass().add(DataPointTooltip.STYLE_CLASS_LABEL);
label.setWrapText(true);
label.setMinWidth(0);
registerInputEventHandler(MouseEvent.MOUSE_MOVED, mouseMoveHandler);
}
/**
* Creates a new instance of DataPointTooltip class.
*
* @param pickingDistance the initial value for the {@link #pickingDistanceProperty() pickingDistance} property
*/
public DataPointTooltip(final double pickingDistance) {
this();
setPickingDistance(pickingDistance);
}
private Optional<DataPoint> findDataPoint(final MouseEvent event, final Bounds plotAreaBounds) {
if (!plotAreaBounds.contains(event.getX(), event.getY())) {
return Optional.empty();
}
final Point2D mouseLocation = getLocationInPlotArea(event);
return findNearestDataPointWithinPickingDistance(mouseLocation);
}
private Optional<DataPoint> findNearestDataPointWithinPickingDistance(final Point2D mouseLocation) {
final Chart chart = getChart();
if (!(chart instanceof XYChart)) {
return Optional.empty();
}
final XYChart xyChart = (XYChart) chart;
final ObservableList<DataSet> xyChartDatasets = xyChart.getDatasets();
return xyChart.getRenderers().stream() // for all renderers
.flatMap(renderer -> Stream.of(renderer.getDatasets(), xyChartDatasets) //
.flatMap(List::stream) // combine global and renderer specific Datasets
.flatMap(dataset -> getPointsCloseToCursor(dataset, renderer, mouseLocation))) // get points in range of cursor
.reduce((p1, p2) -> p1.distanceFromMouse < p2.distanceFromMouse ? p1 : p2); // find closest point
}
private Stream<DataPoint> getPointsCloseToCursor(final DataSet dataset, final Renderer renderer, final Point2D mouseLocation) {
// Get Axes for the Renderer
final Axis xAxis = findXAxis(renderer);
final Axis yAxis = findYAxis(renderer);
if (xAxis == null || yAxis == null) {
return Stream.empty(); // ignore this renderer because there are no valid axes available
}
if (dataset instanceof GridDataSet) {
return Stream.empty(); // TODO: correct impl for grid data sets
}
return dataset.lock().readLockGuard(() -> {
int minIdx = 0;
int maxIdx = dataset.getDataCount();
if (isDataSorted(renderer)) {
// get the screen x coordinates and dataset indices between which points can be in picking distance
final double xMin = xAxis.getValueForDisplay(mouseLocation.getX() - getPickingDistance());
final double xMax = xAxis.getValueForDisplay(mouseLocation.getX() + getPickingDistance());
minIdx = Math.max(0, dataset.getIndex(DataSet.DIM_X, xMin) - 1);
maxIdx = Math.min(dataset.getDataCount(), dataset.getIndex(DataSet.DIM_X, xMax) + 1);
}
return IntStream.range(minIdx, maxIdx) // loop over all candidate points
.mapToObj(i -> getDataPointFromDataSet(renderer, dataset, xAxis, yAxis, mouseLocation, i)) // get points with distance to mouse
.filter(p -> p.distanceFromMouse <= getPickingDistance()) // filter out points which are too far away
.map(dataPoint -> dataPoint.withFormattedLabel(formatLabel(dataPoint)))
.collect(Collectors.toList()) // Realize list so that calculations are done within the data set lock
.stream();
});
}
private boolean isDataSorted(final Renderer renderer) {
return renderer instanceof ErrorDataSetRenderer && ((ErrorDataSetRenderer) renderer).isAssumeSortedData();
}
private Axis findYAxis(final Renderer renderer) {
return renderer.getAxes().stream().filter(ax -> ax.getSide().isVertical()).findFirst().orElse(null);
}
private Axis findXAxis(final Renderer renderer) {
return renderer.getAxes().stream().filter(ax -> ax.getSide().isHorizontal()).findFirst().orElse(null);
}
private DataPoint getDataPointFromDataSet(final Renderer renderer, final DataSet dataset, final Axis xAxis, final Axis yAxis, final Point2D mouseLocation, final int index) {
final double xValue = dataset.get(DataSet.DIM_X, index);
final double yValue = dataset.get(DataSet.DIM_Y, index);
final double displayPositionX = xAxis.getDisplayPosition(xValue);
final double displayPositionY = yAxis.getDisplayPosition(yValue);
final double distanceFromMouseLocation = new Point2D(displayPositionX, displayPositionY).distance(mouseLocation);
final String dataLabelSafe = getDataLabelSafe(dataset, index);
return new DataPoint( //
renderer, //
xValue, //
yValue, //
dataLabelSafe, //
distanceFromMouseLocation);
}
private String formatDataPoint(final DataPoint dataPoint) {
return formatData(dataPoint.renderer, new Tuple<>(dataPoint.x, dataPoint.y));
}
protected String formatLabel(DataPoint dataPoint) {
return String.format("'%s'%n%s", dataPoint.label, formatDataPoint(dataPoint));
}
protected String getDataLabelSafe(final DataSet dataSet, final int index) {
String labelString = dataSet.getDataLabel(index);
if (labelString == null) {
return String.format("%s [%d]", dataSet.getName(), index);
}
return labelString;
}
/**
* Returns the value of the {@link #pickingDistanceProperty()}.
*
* @return the current picking distance
*/
public final double getPickingDistance() {
return pickingDistanceProperty().get();
}
/**
* Distance of the mouse cursor from the data point (expressed in display units) that should trigger showing the
* tool tip. By default initialized to {@value #DEFAULT_PICKING_DISTANCE}.
*
* @return the picking distance property
*/
public final DoubleProperty pickingDistanceProperty() {
return pickingDistance;
}
/**
* Sets the value of {@link #pickingDistanceProperty()}.
*
* @param distance the new picking distance
*/
public final void setPickingDistance(final double distance) {
pickingDistanceProperty().set(distance);
}
protected void updateLabel(final MouseEvent event, final Bounds plotAreaBounds, final DataPoint dataPoint) {
label.setText(dataPoint.formattedLabel);
final double mouseX = event.getX();
final double spaceLeft = mouseX - plotAreaBounds.getMinX();
final double spaceRight = plotAreaBounds.getWidth() - spaceLeft;
double width = label.prefWidth(-1);
boolean atSide = true; // set to false if we cannot print the tooltip beside the point
double xLocation;
if (spaceRight >= width + LABEL_X_OFFSET) { // place to right if enough space
xLocation = mouseX + DataPointTooltip.LABEL_X_OFFSET;
} else if (spaceLeft >= width + LABEL_X_OFFSET) { // place left if enough space
xLocation = mouseX - DataPointTooltip.LABEL_X_OFFSET - width;
} else if (width < plotAreaBounds.getWidth()) {
xLocation = spaceLeft > spaceRight ? plotAreaBounds.getMaxX() - width : plotAreaBounds.getMinX();
atSide = false;
} else {
width = plotAreaBounds.getWidth();
xLocation = plotAreaBounds.getMinX();
atSide = false;
}
final double mouseY = event.getY();
final double spaceTop = mouseY - plotAreaBounds.getMinY();
final double spaceBottom = plotAreaBounds.getHeight() - spaceTop;
double height = label.prefHeight(width);
double yLocation;
if (height < spaceBottom) {
yLocation = mouseY + DataPointTooltip.LABEL_Y_OFFSET;
} else if (height < spaceTop) {
yLocation = mouseY - DataPointTooltip.LABEL_Y_OFFSET - height;
} else if (atSide && height < plotAreaBounds.getHeight()) {
yLocation = spaceTop < spaceBottom ? plotAreaBounds.getMaxY() - height : plotAreaBounds.getMinY();
} else if (atSide) {
yLocation = plotAreaBounds.getMinY();
height = plotAreaBounds.getHeight();
} else if (spaceBottom > spaceTop) {
yLocation = mouseY + DataPointTooltip.LABEL_Y_OFFSET;
height = spaceBottom - LABEL_Y_OFFSET;
} else {
yLocation = plotAreaBounds.getMinY();
height = spaceTop - LABEL_Y_OFFSET;
}
label.resizeRelocate(xLocation, yLocation, width, height);
}
private void updateToolTip(final MouseEvent event) {
final Bounds plotAreaBounds = getChart().getPlotArea().getBoundsInLocal();
final Optional<DataPoint> dataPoint = findDataPoint(event, plotAreaBounds);
if (dataPoint.isEmpty()) {
getChartChildren().remove(label);
return;
}
updateLabel(event, plotAreaBounds, dataPoint.get());
if (!getChartChildren().contains(label)) {
getChartChildren().add(label);
label.requestLayout();
}
}
public static class DataPoint {
public final Renderer renderer;
public final double x;
public final double y;
public final String label;
public final String formattedLabel; // may be empty
public final double distanceFromMouse;
public DataPoint(Renderer renderer, double x, double y, String label, double distanceFromMouse, String formattedLabel) {
this.renderer = renderer;
this.x = x;
this.y = y;
this.label = label;
this.distanceFromMouse = distanceFromMouse;
this.formattedLabel = formattedLabel;
}
public DataPoint(Renderer renderer, double x, double y, String label, double distanceFromMouse) {
this(renderer, x, y, label, distanceFromMouse, "");
}
public DataPoint withFormattedLabel(String formattedLabel) {
return new DataPoint(renderer, x, y, formattedLabel, distanceFromMouse, formattedLabel);
}
}
}