View Javadoc
1   /*
2    * Tethys: GUI Utilities
3    * Copyright 2012-2026. Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6    * use this file except in compliance with the License.  You may obtain a copy
7    * of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  package io.github.tonywasher.joceanus.tethys.javafx.chart;
18  
19  import io.github.tonywasher.joceanus.oceanus.date.OceanusDate;
20  import io.github.tonywasher.joceanus.tethys.core.chart.TethysUICoreAreaChart;
21  import io.github.tonywasher.joceanus.tethys.core.factory.TethysUICoreFactory;
22  import io.github.tonywasher.joceanus.tethys.javafx.base.TethysUIFXNode;
23  import javafx.collections.ObservableList;
24  import javafx.scene.Node;
25  import javafx.scene.chart.NumberAxis;
26  import javafx.scene.chart.StackedAreaChart;
27  import javafx.scene.chart.XYChart.Data;
28  import javafx.scene.chart.XYChart.Series;
29  import javafx.scene.control.Tooltip;
30  import javafx.scene.input.MouseEvent;
31  import javafx.util.StringConverter;
32  
33  import java.time.LocalDate;
34  import java.util.HashMap;
35  import java.util.Map;
36  
37  /**
38   * javaFX AreaChart.
39   */
40  public class TethysUIFXAreaChart
41          extends TethysUICoreAreaChart {
42      /**
43       * The border factor.
44       */
45      private static final double BORDER_FACTOR = 0.05;
46  
47      /**
48       * The border factor.
49       */
50      private static final double TICK_FACTOR = 0.05;
51  
52      /**
53       * The Node.
54       */
55      private final TethysUIFXNode theNode;
56  
57      /**
58       * The chart.
59       */
60      private final StackedAreaChart<Number, Number> theChart;
61  
62      /**
63       * The series map.
64       */
65      private final Map<String, Series<Number, Number>> theSeries;
66  
67      /**
68       * The minimun date.
69       */
70      private Number theMinimum;
71  
72      /**
73       * The maximun date.
74       */
75      private Number theMaximum;
76  
77      /**
78       * Constructor.
79       *
80       * @param pFactory the Gui Factory
81       */
82      TethysUIFXAreaChart(final TethysUICoreFactory<?> pFactory) {
83          /* initialise underlying class */
84          super(pFactory);
85  
86          /* Create chart */
87          final NumberAxis myXAxis = new NumberAxis();
88          myXAxis.setAutoRanging(false);
89          myXAxis.setForceZeroInRange(false);
90          myXAxis.setTickLabelRotation(LABEL_ANGLE);
91          myXAxis.setTickLabelFormatter(new StringConverter<>() {
92              @Override
93              public String toString(final Number pValue) {
94                  return new OceanusDate(LocalDate.ofEpochDay(pValue.longValue())).toString();
95              }
96  
97              @Override
98              public Number fromString(final String pValue) {
99                  return null;
100             }
101         });
102         final NumberAxis myYAxis = new NumberAxis();
103         myYAxis.setTickLabelFormatter(new StringConverter<>() {
104             @Override
105             public String toString(final Number pValue) {
106                 return getFormatter().formatMoney(getParser().parseMoneyValue(pValue.toString()));
107             }
108 
109             @Override
110             public Number fromString(final String pValue) {
111                 return null;
112             }
113         });
114         theChart = new StackedAreaChart<>(myXAxis, myYAxis);
115         theChart.setHorizontalGridLinesVisible(false);
116         theChart.setVerticalGridLinesVisible(false);
117 
118         /* Create the map */
119         theSeries = new HashMap<>();
120 
121         /* Create Node */
122         theNode = new TethysUIFXNode(theChart);
123     }
124 
125     @Override
126     public TethysUIFXNode getNode() {
127         return theNode;
128     }
129 
130     @Override
131     public void setVisible(final boolean pVisible) {
132         theNode.setManaged(pVisible);
133         theNode.setVisible(pVisible);
134     }
135 
136     @Override
137     public void setEnabled(final boolean pEnabled) {
138         theChart.setDisable(!pEnabled);
139     }
140 
141     @Override
142     public void setPreferredWidth(final Integer pWidth) {
143         theChart.setPrefWidth(pWidth);
144     }
145 
146     @Override
147     public void setPreferredHeight(final Integer pHeight) {
148         theChart.setPrefHeight(pHeight);
149     }
150 
151     @Override
152     public void updateAreaChart(final TethysUIAreaChartData pData) {
153         /* Update underlying data */
154         super.updateAreaChart(pData);
155 
156         /* Set the chart title and Axis labels */
157         theChart.setTitle(pData.getTitle());
158 
159         /* Adjust XAxis */
160         final NumberAxis myAxis = (NumberAxis) theChart.getXAxis();
161         myAxis.setLabel(pData.getXAxisLabel());
162         if (theMinimum != null) {
163             final double myAdjust = getBorderAdjust();
164             myAxis.setLowerBound(theMinimum.doubleValue() - myAdjust);
165             myAxis.setUpperBound(theMaximum.doubleValue() + myAdjust);
166             myAxis.setTickUnit(getTickCount());
167         }
168 
169         /* Adjust Y Axis */
170         theChart.getYAxis().setLabel(pData.getYAxisLabel());
171     }
172 
173     @Override
174     protected void resetData() {
175         /* Clear existing data  */
176         final ObservableList<Series<Number, Number>> myData = theChart.getData();
177         myData.clear();
178 
179         /* Clear max/min dates */
180         theMaximum = null;
181         theMinimum = null;
182 
183         /* Clear underlying data  */
184         super.resetData();
185     }
186 
187     /**
188      * Determine border adjustment.
189      *
190      * @return the border adjustment
191      */
192     private double getBorderAdjust() {
193         final double myRange = theMaximum.doubleValue() - theMinimum.doubleValue();
194         return myRange * BORDER_FACTOR;
195     }
196 
197     /**
198      * Determine tick count.
199      *
200      * @return the tick count
201      */
202     private long getTickCount() {
203         final double myRange = theMaximum.doubleValue() - theMinimum.doubleValue();
204         return (long) (myRange * TICK_FACTOR);
205     }
206 
207     @Override
208     protected void createPoint(final String pName,
209                                final TethysUIAreaChartDataPoint pPoint) {
210         /* Access the series */
211         Series<Number, Number> mySeries = theSeries.get(pName);
212         if (mySeries == null) {
213             /* Create the series */
214             mySeries = new Series<>();
215             mySeries.setName(pName);
216             final ObservableList<Series<Number, Number>> myData = theChart.getData();
217             myData.add(mySeries);
218             theSeries.put(pName, mySeries);
219 
220             /* Install click handler for node */
221             mySeries.getNode().addEventHandler(MouseEvent.MOUSE_CLICKED, e -> selectSeries(pName));
222         }
223 
224         /* Add the point */
225         final ObservableList<Data<Number, Number>> myPoints = mySeries.getData();
226         final Data<Number, Number> myData = new Data<>(dateToEpoch(pPoint.getDate()), pPoint.getValue().doubleValue());
227         myPoints.add(myData);
228         final Node myNode = myData.getNode();
229 
230         /* Create the toolTip */
231         final String myTooltip = getToolTip(pName, pPoint.getValue());
232         Tooltip.install(myNode, new Tooltip(myTooltip));
233 
234         /* Adjust max/min */
235         final Number myDate = myData.getXValue();
236         if (theMinimum == null
237                 || theMinimum.longValue() > myDate.longValue()) {
238             theMinimum = myDate;
239         }
240         if (theMaximum == null
241                 || theMaximum.longValue() < myDate.longValue()) {
242             theMaximum = myDate;
243         }
244     }
245 
246     /**
247      * Convert date to epoch.
248      *
249      * @param pDate the date
250      * @return the epoch
251      */
252     private static long dateToEpoch(final OceanusDate pDate) {
253         return pDate.getDate().toEpochDay();
254     }
255 }