mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 09:52:23 +01:00
Add tooltip to time navigation so from and to date is visible
This commit is contained in:
parent
955c57cfbe
commit
55a4154e74
@ -25,6 +25,8 @@ import bisq.core.locale.Res;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
|
||||
import javafx.stage.PopupWindow;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.Axis;
|
||||
import javafx.scene.chart.LineChart;
|
||||
@ -44,15 +46,21 @@ import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.Side;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
|
||||
import javafx.event.EventHandler;
|
||||
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.time.temporal.TemporalAdjuster;
|
||||
@ -76,31 +84,31 @@ import javax.annotation.Nullable;
|
||||
public abstract class ChartView<T extends ChartViewModel<? extends ChartDataModel>> extends ActivatableViewAndModel<VBox, T> {
|
||||
private Pane center;
|
||||
private SplitPane timelineNavigation;
|
||||
protected NumberAxis xAxis;
|
||||
protected NumberAxis yAxis;
|
||||
protected NumberAxis xAxis, yAxis;
|
||||
protected LineChart<Number, Number> chart;
|
||||
private HBox timelineLabels;
|
||||
private HBox timelineLabels, legendBox2;
|
||||
private final ToggleGroup timeIntervalToggleGroup = new ToggleGroup();
|
||||
|
||||
protected final Set<XYChart.Series<Number, Number>> activeSeries = new HashSet<>();
|
||||
protected final Map<String, Integer> seriesIndexMap = new HashMap<>();
|
||||
protected final Map<String, AutoTooltipSlideToggleButton> legendToggleBySeriesName = new HashMap<>();
|
||||
|
||||
private final List<Node> dividerNodes = new ArrayList<>();
|
||||
|
||||
private final List<Tooltip> dividerNodesTooltips = new ArrayList<>();
|
||||
private ChangeListener<Number> widthListener;
|
||||
private ChangeListener<Toggle> timeIntervalChangeListener;
|
||||
private ListChangeListener<Node> nodeListChangeListener;
|
||||
private int maxSeriesSize;
|
||||
private boolean pressed;
|
||||
private boolean centerPanePressed;
|
||||
private double x;
|
||||
|
||||
@Setter
|
||||
protected boolean isRadioButtonBehaviour;
|
||||
@Setter
|
||||
private int maxDataPointsForShowingSymbols = 100;
|
||||
private HBox legendBox2;
|
||||
private ChangeListener<Number> yAxisWidthListener;
|
||||
private EventHandler<MouseEvent> dividerMouseDraggedEventHandler;
|
||||
private StringProperty fromProperty = new SimpleStringProperty();
|
||||
private StringProperty toProperty = new SimpleStringProperty();
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -123,7 +131,7 @@ public abstract class ChartView<T extends ChartViewModel<? extends ChartDataMode
|
||||
prepareInitialize();
|
||||
|
||||
maxSeriesSize = 0;
|
||||
pressed = false;
|
||||
centerPanePressed = false;
|
||||
x = 0;
|
||||
|
||||
// Series
|
||||
@ -152,6 +160,7 @@ public abstract class ChartView<T extends ChartViewModel<? extends ChartDataMode
|
||||
defineAndAddActiveSeries();
|
||||
|
||||
// Put all together
|
||||
VBox timelineNavigationBox = new VBox();
|
||||
double paddingLeft = 15;
|
||||
double paddingRight = 89;
|
||||
// Y-axis width depends on data so we register a listener to get correct value
|
||||
@ -172,15 +181,17 @@ public abstract class ChartView<T extends ChartViewModel<? extends ChartDataMode
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
VBox.setMargin(timeIntervalBox, new Insets(0, paddingRight, 0, paddingLeft));
|
||||
VBox.setMargin(timelineNavigation, new Insets(0, paddingRight, 0, paddingLeft));
|
||||
VBox.setMargin(timelineLabels, new Insets(0, paddingRight, 0, paddingLeft));
|
||||
VBox.setMargin(legendBox1, new Insets(0, paddingRight, 0, paddingLeft));
|
||||
root.getChildren().addAll(timeIntervalBox, chart, timelineNavigation, timelineLabels, legendBox1);
|
||||
timelineNavigationBox.getChildren().addAll(timelineNavigation, timelineLabels, legendBox1);
|
||||
if (legendBox2 != null) {
|
||||
VBox.setMargin(legendBox2, new Insets(-20, paddingRight, 0, paddingLeft));
|
||||
root.getChildren().add(legendBox2);
|
||||
timelineNavigationBox.getChildren().add(legendBox2);
|
||||
}
|
||||
root.getChildren().addAll(timeIntervalBox, chart, timelineNavigationBox);
|
||||
|
||||
// Listeners
|
||||
widthListener = (observable, oldValue, newValue) -> {
|
||||
@ -208,6 +219,8 @@ public abstract class ChartView<T extends ChartViewModel<? extends ChartDataMode
|
||||
@Override
|
||||
public void activate() {
|
||||
timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]);
|
||||
UserThread.execute(this::applyTimeLineNavigationLabels);
|
||||
UserThread.execute(this::onTimelineChanged);
|
||||
|
||||
TemporalAdjuster temporalAdjuster = model.getTemporalAdjuster();
|
||||
applyTemporalAdjuster(temporalAdjuster);
|
||||
@ -218,8 +231,6 @@ public abstract class ChartView<T extends ChartViewModel<? extends ChartDataMode
|
||||
initBoundsForTimelineNavigation();
|
||||
|
||||
updateChartAfterDataChange();
|
||||
// Need delay to next render frame
|
||||
UserThread.execute(this::applyTimeLineNavigationLabels);
|
||||
|
||||
// Apply listeners and handlers
|
||||
root.widthProperty().addListener(widthListener);
|
||||
@ -257,6 +268,8 @@ public abstract class ChartView<T extends ChartViewModel<? extends ChartDataMode
|
||||
activeSeries.clear();
|
||||
chart.getData().clear();
|
||||
legendToggleBySeriesName.values().forEach(e -> e.setSelected(false));
|
||||
dividerNodes.clear();
|
||||
dividerNodesTooltips.clear();
|
||||
model.invalidateCache();
|
||||
}
|
||||
|
||||
@ -329,7 +342,6 @@ public abstract class ChartView<T extends ChartViewModel<? extends ChartDataMode
|
||||
protected LineChart<Number, Number> getChart() {
|
||||
LineChart<Number, Number> chart = new LineChart<>(xAxis, yAxis);
|
||||
chart.setAnimated(false);
|
||||
chart.setCreateSymbols(true);
|
||||
chart.setLegendVisible(false);
|
||||
chart.setMinHeight(200);
|
||||
chart.setId("charts-dao");
|
||||
@ -385,9 +397,9 @@ public abstract class ChartView<T extends ChartViewModel<? extends ChartDataMode
|
||||
center = new Pane();
|
||||
center.setId("chart-navigation-center-pane");
|
||||
Pane right = new Pane();
|
||||
timelineNavigation = new SplitPane();
|
||||
timelineNavigation = new SplitPane(left, center, right);
|
||||
timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]);
|
||||
timelineNavigation.setMinHeight(25);
|
||||
timelineNavigation.getItems().addAll(left, center, right);
|
||||
timelineLabels = new HBox();
|
||||
}
|
||||
|
||||
@ -419,14 +431,26 @@ public abstract class ChartView<T extends ChartViewModel<? extends ChartDataMode
|
||||
}
|
||||
}
|
||||
|
||||
private void resetTimeNavigation() {
|
||||
timelineNavigation.setDividerPositions(0d, 1d);
|
||||
model.onTimelineNavigationChanged(0, 1);
|
||||
private void onMousePressedSplitPane(MouseEvent e) {
|
||||
x = e.getX();
|
||||
applyFromToDates();
|
||||
showDividerTooltips();
|
||||
}
|
||||
|
||||
private void onMousePressedCenter(MouseEvent e) {
|
||||
centerPanePressed = true;
|
||||
applyFromToDates();
|
||||
showDividerTooltips();
|
||||
}
|
||||
|
||||
private void onMouseReleasedCenter(MouseEvent e) {
|
||||
centerPanePressed = false;
|
||||
onTimelineChanged();
|
||||
hideDividerTooltips();
|
||||
}
|
||||
|
||||
private void onMouseDragged(MouseEvent e) {
|
||||
if (pressed) {
|
||||
if (centerPanePressed) {
|
||||
double newX = e.getX();
|
||||
double width = timelineNavigation.getWidth();
|
||||
double relativeDelta = (x - newX) / width;
|
||||
@ -437,36 +461,71 @@ public abstract class ChartView<T extends ChartViewModel<? extends ChartDataMode
|
||||
model.onTimelineMouseDrag(leftPos, rightPos);
|
||||
timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]);
|
||||
x = newX;
|
||||
|
||||
applyFromToDates();
|
||||
showDividerTooltips();
|
||||
}
|
||||
}
|
||||
|
||||
private void onMouseReleasedCenter(MouseEvent e) {
|
||||
pressed = false;
|
||||
onTimelineChanged();
|
||||
}
|
||||
|
||||
private void onMousePressedSplitPane(MouseEvent e) {
|
||||
x = e.getX();
|
||||
}
|
||||
|
||||
private void onMousePressedCenter(MouseEvent e) {
|
||||
pressed = true;
|
||||
}
|
||||
|
||||
private void addActionHandlersToDividers() {
|
||||
// No API access to dividers ;-( only via css lookup hack (https://stackoverflow.com/questions/40707295/how-to-add-listener-to-divider-position?rq=1)
|
||||
// Need to be done after added to scene and call requestLayout and applyCss. We keep it in a list atm
|
||||
// and set action handler in activate.
|
||||
timelineNavigation.requestLayout();
|
||||
timelineNavigation.applyCss();
|
||||
dividerMouseDraggedEventHandler = event -> {
|
||||
applyFromToDates();
|
||||
showDividerTooltips();
|
||||
};
|
||||
|
||||
for (Node node : timelineNavigation.lookupAll(".split-pane-divider")) {
|
||||
dividerNodes.add(node);
|
||||
node.setOnMouseReleased(e -> onTimelineChanged());
|
||||
node.setOnMouseReleased(e -> {
|
||||
hideDividerTooltips();
|
||||
onTimelineChanged();
|
||||
});
|
||||
node.addEventHandler(MouseEvent.MOUSE_DRAGGED, dividerMouseDraggedEventHandler);
|
||||
|
||||
Tooltip tooltip = new Tooltip("");
|
||||
dividerNodesTooltips.add(tooltip);
|
||||
tooltip.setShowDelay(Duration.millis(300));
|
||||
tooltip.setShowDuration(Duration.seconds(3));
|
||||
tooltip.textProperty().bind(dividerNodes.size() == 1 ? fromProperty : toProperty);
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeActionHandlersToDividers() {
|
||||
dividerNodes.forEach(node -> node.setOnMouseReleased(null));
|
||||
dividerNodes.forEach(node -> {
|
||||
node.setOnMouseReleased(null);
|
||||
node.removeEventHandler(MouseEvent.MOUSE_DRAGGED, dividerMouseDraggedEventHandler);
|
||||
});
|
||||
for (int i = 0; i < dividerNodesTooltips.size(); i++) {
|
||||
Tooltip tooltip = dividerNodesTooltips.get(i);
|
||||
tooltip.textProperty().unbind();
|
||||
Tooltip.uninstall(dividerNodes.get(i), tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetTimeNavigation() {
|
||||
timelineNavigation.setDividerPositions(0d, 1d);
|
||||
model.onTimelineNavigationChanged(0, 1);
|
||||
}
|
||||
|
||||
private void showDividerTooltips() {
|
||||
showDividerTooltip(0);
|
||||
showDividerTooltip(1);
|
||||
}
|
||||
|
||||
private void hideDividerTooltips() {
|
||||
dividerNodesTooltips.forEach(PopupWindow::hide);
|
||||
}
|
||||
|
||||
private void showDividerTooltip(int index) {
|
||||
Node divider = dividerNodes.get(index);
|
||||
Bounds bounds = divider.localToScene(divider.getBoundsInLocal());
|
||||
Tooltip tooltip = dividerNodesTooltips.get(index);
|
||||
tooltip.show(divider, bounds.getMaxX(), bounds.getMaxY() - 20);
|
||||
}
|
||||
|
||||
|
||||
@ -532,15 +591,29 @@ public abstract class ChartView<T extends ChartViewModel<? extends ChartDataMode
|
||||
}
|
||||
|
||||
private void onTimelineChanged() {
|
||||
updateTimeLinePositions();
|
||||
|
||||
model.invalidateCache();
|
||||
applyData();
|
||||
updateChartAfterDataChange();
|
||||
}
|
||||
|
||||
private void updateTimeLinePositions() {
|
||||
double leftPos = timelineNavigation.getDividerPositions()[0];
|
||||
double rightPos = timelineNavigation.getDividerPositions()[1];
|
||||
model.onTimelineNavigationChanged(leftPos, rightPos);
|
||||
// We need to update as model might have adjusted the values
|
||||
timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]);
|
||||
fromProperty.set(model.getTimeAxisStringConverter().toString(model.getFromDate()).replace("\n", " "));
|
||||
toProperty.set(model.getTimeAxisStringConverter().toString(model.getToDate()).replace("\n", " "));
|
||||
}
|
||||
|
||||
model.invalidateCache();
|
||||
applyData();
|
||||
updateChartAfterDataChange();
|
||||
private void applyFromToDates() {
|
||||
double leftPos = timelineNavigation.getDividerPositions()[0];
|
||||
double rightPos = timelineNavigation.getDividerPositions()[1];
|
||||
model.applyFromToDates(leftPos, rightPos);
|
||||
fromProperty.set(model.getTimeAxisStringConverter().toString(model.getFromDate()).replace("\n", " "));
|
||||
toProperty.set(model.getTimeAxisStringConverter().toString(model.getToDate()).replace("\n", " "));
|
||||
}
|
||||
|
||||
private void onSelectLegendToggle(XYChart.Series<Number, Number> series) {
|
||||
|
@ -50,6 +50,10 @@ public abstract class ChartViewModel<T extends ChartDataModel> extends Activatab
|
||||
protected Number upperBound;
|
||||
@Getter
|
||||
protected String dateFormatPatters = "dd MMM\nyyyy";
|
||||
@Getter
|
||||
long fromDate;
|
||||
@Getter
|
||||
long toDate;
|
||||
|
||||
public ChartViewModel(T dataModel) {
|
||||
super(dataModel);
|
||||
@ -94,8 +98,36 @@ public abstract class ChartViewModel<T extends ChartDataModel> extends Activatab
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void onTimelineNavigationChanged(double leftPos, double rightPos) {
|
||||
applyFromToDates(leftPos, rightPos);
|
||||
// TODO find better solution
|
||||
// The TemporalAdjusters map dates to the lower bound (e.g. 1.1.2016) but our from date is the date of
|
||||
// the first data entry so if we filter by that we would exclude the first year data in case YEAR was selected
|
||||
// A trade with data 3.May.2016 gets mapped to 1.1.2016 and our from date will be April 2016, so we would
|
||||
// filter that. It is a bit tricky to sync the TemporalAdjusters with our date filter. To include at least in
|
||||
// the case when we have not set the date filter (left =0 / right =1) we set from date to epoch time 0 and
|
||||
// to date to one year ahead to be sure we include all.
|
||||
|
||||
long from, to;
|
||||
|
||||
// We only manipulate the from, to variables for the date filter, not the fromDate, toDate properties as those
|
||||
// are used by the view for tooltip over the time line navigation dividers
|
||||
if (leftPos == 0) {
|
||||
from = 0;
|
||||
} else {
|
||||
from = fromDate;
|
||||
}
|
||||
if (rightPos == 1) {
|
||||
to = new Date().getTime() / 1000 + TimeUnit.DAYS.toSeconds(365);
|
||||
} else {
|
||||
to = toDate;
|
||||
}
|
||||
|
||||
dividerPositions[0] = leftPos;
|
||||
dividerPositions[1] = rightPos;
|
||||
dataModel.setDateFilter(from, to);
|
||||
}
|
||||
|
||||
void applyFromToDates(double leftPos, double rightPos) {
|
||||
// We need to snap into the 0 and 1 values once we are close as otherwise once navigation has been used we
|
||||
// would not get back to exact 0 or 1. Not clear why but might be rounding issues from values at x positions of
|
||||
// drag operations.
|
||||
@ -105,32 +137,12 @@ public abstract class ChartViewModel<T extends ChartDataModel> extends Activatab
|
||||
if (rightPos > RIGHT_TIMELINE_SNAP_VALUE) {
|
||||
rightPos = 1;
|
||||
}
|
||||
dividerPositions[0] = leftPos;
|
||||
dividerPositions[1] = rightPos;
|
||||
|
||||
long lowerBoundAsLong = lowerBound.longValue();
|
||||
long totalRange = upperBound.longValue() - lowerBoundAsLong;
|
||||
|
||||
// TODO find better solution
|
||||
// The TemporalAdjusters map dates to the lower bound (e.g. 1.1.2016) but our from date is the date of
|
||||
// the first data entry so if we filter by that we would exclude the first year data in case YEAR was selected
|
||||
// A trade with data 3.May.2016 gets mapped to 1.1.2016 and our from date will be April 2016, so we would
|
||||
// filter that. It is a bit tricky to sync the TemporalAdjusters with our date filter. To include at least in
|
||||
// the case when we have not set the date filter (left =0 / right =1) we set from date to epoch time 0 and
|
||||
// to date to one year ahead to be sure we include all.
|
||||
|
||||
if (leftPos == 0) {
|
||||
from = 0;
|
||||
} else {
|
||||
from = (long) (lowerBoundAsLong + totalRange * leftPos);
|
||||
}
|
||||
if (rightPos == 1) {
|
||||
to = new Date().getTime() / 1000 + TimeUnit.DAYS.toSeconds(365);
|
||||
} else {
|
||||
to = (long) (lowerBoundAsLong + totalRange * rightPos);
|
||||
}
|
||||
|
||||
dataModel.setDateFilter(from, to);
|
||||
fromDate = (long) (lowerBoundAsLong + totalRange * leftPos);
|
||||
toDate = (long) (lowerBoundAsLong + totalRange * rightPos);
|
||||
}
|
||||
|
||||
void onTimelineMouseDrag(double leftPos, double rightPos) {
|
||||
|
Loading…
Reference in New Issue
Block a user