From b0938535d9a3160655509963c8c3703058ccd46b Mon Sep 17 00:00:00 2001 From: Kilian Brachtendorf Date: Thu, 13 Dec 2018 18:34:12 +0100 Subject: [PATCH] Refactor layout to use a "strategy pattern for different chart types" --- .classpath | 58 +- .settings/org.eclipse.core.resources.prefs | 6 + pom.xml | 131 +- .../com/github/kilianB/MultiTypeChart.java | 1722 +++++++++-------- .../java/com/github/kilianB/TypedSeries.java | 40 +- .../java/com/github/kilianB/ValueMarker.java | 6 + .../github/kilianB/renderer/AreaRenderer.java | 57 + .../github/kilianB/renderer/LineRenderer.java | 18 + .../com/github/kilianB/renderer/Renderer.java | 14 + .../com/github/kilianB/utility/ColorUtil.java | 161 -- .../com/github/kilianB/utility/MathUtil.java | 133 -- 11 files changed, 1253 insertions(+), 1093 deletions(-) create mode 100644 .settings/org.eclipse.core.resources.prefs create mode 100644 src/main/java/com/github/kilianB/renderer/AreaRenderer.java create mode 100644 src/main/java/com/github/kilianB/renderer/LineRenderer.java create mode 100644 src/main/java/com/github/kilianB/renderer/Renderer.java delete mode 100644 src/main/java/com/github/kilianB/utility/ColorUtil.java delete mode 100644 src/main/java/com/github/kilianB/utility/MathUtil.java diff --git a/.classpath b/.classpath index c511809..159690e 100644 --- a/.classpath +++ b/.classpath @@ -1,20 +1,38 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..04cfa2c --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,6 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding//src/test/resources=UTF-8 +encoding/=UTF-8 diff --git a/pom.xml b/pom.xml index 24f41a3..e219c90 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,127 @@ - - 4.0.0 - com.github.kilianB - MultiTypeChart - 0.0.1-SNAPSHOT + + 4.0.0 + com.github.kilianB + MultiTypeChart + 1.0.0 + + + maven + MultiTypeChart + UTF-8 + + + + + MIT + https://opensource.org/licenses/MIT + repo + + + + + + Kilian Brachtendorf + Kilian.Brachtendorf@t-online.de + + developer + + Europe/Berlin + + + + + + + jcenter + https://jcenter.bintray.com/ + + + + + + com.github.kilianB + UtilityCode + 1.5.1 + + + + + + + + maven-compiler-plugin + 3.7.0 + + 10 + 10 + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.21.0 + + + org.junit.platform + junit-platform-surefire-provider + 1.2.0-M1 + + + org.junit.jupiter + junit-jupiter-engine + 5.2.0-M1 + + + + + maven-source-plugin + + + **/kilianB/demo/** + + + + + attach-sources + + jar + + + + + + maven-javadoc-plugin + + + **/kilianB/demo/** + + + + + attach-javadocs + + jar + + + + + + + + + + + bintray-kilianb-maven + kilianb-maven + https://api.bintray.com/maven/kilianb/${bintrayRepository}//${bintrayPackage}/ + + + \ No newline at end of file diff --git a/src/main/java/com/github/kilianB/MultiTypeChart.java b/src/main/java/com/github/kilianB/MultiTypeChart.java index d62336a..e00fc5e 100644 --- a/src/main/java/com/github/kilianB/MultiTypeChart.java +++ b/src/main/java/com/github/kilianB/MultiTypeChart.java @@ -14,7 +14,7 @@ import java.util.logging.Logger; import com.github.kilianB.Legend.LegendItem; -import com.github.kilianB.utility.ColorUtil; +import com.github.kilianB.graphics.ColorUtil; import javafx.animation.FadeTransition; import javafx.animation.KeyFrame; @@ -31,17 +31,22 @@ import javafx.collections.ObservableList; import javafx.event.EventHandler; import javafx.geometry.Insets; +import javafx.geometry.Orientation; import javafx.geometry.Side; import javafx.scene.AccessibleRole; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.chart.Axis; +import javafx.scene.chart.CategoryAxis; import javafx.scene.chart.XYChart; import javafx.scene.control.Label; import javafx.scene.effect.ColorAdjust; import javafx.scene.input.MouseEvent; import javafx.scene.chart.LineChart.SortingPolicy; +import javafx.scene.chart.XYChart.Data; +import javafx.scene.chart.XYChart.Series; import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.ValueAxis; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.CornerRadii; @@ -59,12 +64,12 @@ // the classloader public class MultiTypeChart extends XYChart { - private static final Logger LOG = Logger.getLogger(MultiTypeChart.class.getName()); - public enum SeriesType { - SCATTER, LINE, AREA, STACKED_AREA, STACKED_BAR + SCATTER, LINE, AREA, BAR, STACKED_AREA, STACKED_BAR } + private static final Logger LOG = Logger.getLogger(MultiTypeChart.class.getName()); + // Global chart properties private static final String COLOR_CSS_CLASS = "default-color"; private static final String LINE_CHART_LINE_CSS_CLASS = "chart-series-line"; @@ -75,6 +80,10 @@ public enum SeriesType { private static final String AREA_CHART_SYMBOL_CSS_CLASS = "chart-area-symbol"; private static final String AREA_CHART_SERIES_LINE_CSS_CLASS = "chart-series-area-line"; private static final String AREA_CHART_FILL_CSS_CLASS = "chart-series-area-fill"; + + //Bar + private static String NEGATIVE_STYLE = "negative"; + // Markers ReentrantLock markerLock = new ReentrantLock(); private HashMap, Path> valueMarkers = new HashMap<>(); @@ -85,7 +94,6 @@ public enum SeriesType { protected HashMap, TypedSeries> typedSeries = new HashMap<>(); protected HashMap, Boolean> seriesVisibility = new HashMap<>(); - /** A package private hashmap in the chart object */ protected Map, Integer> seriesColorMap = new HashMap<>(); protected BitSet availableColors = new BitSet(8); @@ -94,122 +102,24 @@ public enum SeriesType { /** Remember the current animation state of area charts for layout */ private Map, DoubleProperty> seriesYMultiplierMap = new HashMap<>(); + // Bar chart + + // For every series save data inside here + private Map, Map>> seriesCategoryMap = new HashMap<>(); + protected Legend legend = new Legend(); private Group plotArea; // Implement multi axis support private final int AXIS_PADDING = 5; - private HashMap> additionalXAxis = new HashMap<>(); - private HashMap> additionalYAxis = new HashMap<>(); - - /** - * Legend items - */ - private HashMap,LegendItem> legendMap; - + private HashMap> additionalXAxis = new HashMap<>(); + private HashMap> additionalYAxis = new HashMap<>(); + /** - * - * @param index - * @param xAxis true xAxis false yAxis + * Legend items */ - @SuppressWarnings("unchecked") - protected void addAdditionalAxis(int index, boolean isX, Side side) { - - // todo only number? - Axis axis; - - if(isX) { - //XAxis - axis = new NumberAxis(); - additionalXAxis.put(index,(Axis) axis); - }else { - //YAxis - axis = new NumberAxis(); - additionalYAxis.put(index,(Axis) axis); - } - - axis.setSide(side); - - // package private. But as far as I can see this is not necessary when the side - // is set? - // axis.setEffectiveOrientation(Orientation.VERTICAL); - - axis.autoRangingProperty().addListener((ov, t, t1) -> { - updateAxisRange(); - }); - - getChartChildren().add(axis); - - //Make space for the axis. We are using the padding since we don't have enough access to other components - - - //Construct the binding - ReadOnlyDoubleProperty[] axisWidthAndHeightProperty = new ReadOnlyDoubleProperty[additionalYAxis.size()+additionalXAxis.size()]; - - int i = 0; - for(var entry : additionalYAxis.entrySet()) { - axisWidthAndHeightProperty[i++] = entry.getValue().widthProperty(); - } - - for(var entry : additionalXAxis.entrySet()) { - axisWidthAndHeightProperty[i++] = entry.getValue().heightProperty(); - } - - - ObjectBinding paddingBinding = new ObjectBinding<>() { - - {super.bind(axisWidthAndHeightProperty);} - - @Override - protected Insets computeValue() { - - double top = 0, right = 0, bottom = 0, left = 0; - - for(var entry : additionalYAxis.entrySet()) { - - double width = entry.getValue().getWidth(); - - switch(entry.getValue().getSide()) { - case LEFT: - left += (width+AXIS_PADDING); - break; - case RIGHT: - right += (width+AXIS_PADDING); - break; - default: - throw new IllegalStateException("Y Axis may only be positioned right or left"); - } - } - - for(var entry : additionalXAxis.entrySet()) { - double height = entry.getValue().getHeight(); - - System.out.println("X Axis: " + height + " " + entry.getValue() + " " + entry.getValue().getWidth() - + entry.getValue().getSide()); - - switch(entry.getValue().getSide()) { - case TOP: - top += (height+AXIS_PADDING); - break; - case BOTTOM: - bottom += (height+AXIS_PADDING); - break; - default: - throw new IllegalStateException("X Axis may only be positioned top or bottom"); - } - } - System.out.println(top + " " + right + " " + bottom + " " + left); - return new Insets(top,right,bottom,left); - - } - - }; - - this.paddingProperty().bind(paddingBinding); - - this.requestChartLayout(); - } + private HashMap, LegendItem> legendMap; public MultiTypeChart(@NamedArg("xAxis") Axis xAxis, @NamedArg("yAxis") Axis yAxis) { super(xAxis, yAxis); @@ -234,9 +144,7 @@ public MultiTypeChart(@NamedArg("xAxis") Axis xAxis, @NamedArg("yAxis") Axis< } - - - public boolean addSeries(TypedSeries series) throws IllegalArgumentException{ + public boolean addSeries(TypedSeries series) throws IllegalArgumentException { if (!typedSeries.containsKey(series.getSeries())) { // JavaFX uses weak listeners? No need to dispose afterwards @@ -290,18 +198,34 @@ public boolean addSeries(TypedSeries series) throws IllegalArgumentExcepti typedSeries.put(series.getSeries(), series); seriesVisibility.put(series.getSeries(), Boolean.TRUE); - //Check if we need to add an additional axis + // Axis validation. int yAxisIndex = series.getYAxisIndex(); int xAxisIndex = series.getXAxisIndex(); - - if(yAxisIndex != 0 && !additionalYAxis.containsKey(yAxisIndex)) { - addAdditionalAxis(yAxisIndex,false,series.getyAxisSide()); + + if (yAxisIndex != 0 && !additionalYAxis.containsKey(yAxisIndex)) { + addAdditionalAxis(yAxisIndex, false, series.getyAxisSide()); } - - if(xAxisIndex != 0 && !additionalXAxis.containsKey(xAxisIndex)) { - addAdditionalAxis(xAxisIndex,true,series.getxAxisSide()); + + if (xAxisIndex != 0 && !additionalXAxis.containsKey(xAxisIndex)) { + addAdditionalAxis(xAxisIndex, true, series.getxAxisSide()); } - + + // Check if we need to add an additional axis + if (series.getSeriesType().equals(SeriesType.BAR)) { + + Axis yAxis = additionalYAxis.get(yAxisIndex); + Axis xAxis = additionalXAxis.get(xAxisIndex); + + if (!((xAxis instanceof ValueAxis && yAxis instanceof CategoryAxis) + || (yAxis instanceof ValueAxis && xAxis instanceof CategoryAxis))) { + throw new IllegalArgumentException( + "Axis type incorrect, one of X,Y should be CategoryAxis and the other NumberAxis"); + + } + // One of them has to be a categorial axis. + + } + getData().add(series.getSeries()); return true; @@ -310,303 +234,566 @@ public boolean addSeries(TypedSeries series) throws IllegalArgumentExcepti } } - @Override - protected void dataItemAdded(Series series, int itemIndex, Data item) { - switch (typedSeries.get(series).getSeriesType()) { - case AREA: - addAreaItem(series, itemIndex, item); - break; - case LINE: - addLineItem(series, itemIndex, item); - break; - case SCATTER: - addScatterItem(series, itemIndex, item); - break; - case STACKED_AREA: - break; - case STACKED_BAR: - break; - default: - break; + /** + * Add a value marker to the chart + * + * @param markerToAdd the marker to add + */ + public void addValueMarker(ValueMarker markerToAdd) { + markerLock.lock(); + Path p = new Path(); + valueMarkers.put(markerToAdd, p); + + ChangeListener cl = (obs, oldValue, newValue) -> { + if (newValue) { + getChartChildren().add(((ValueMarker) obs).getLabel()); + } else { + getChartChildren().remove(((ValueMarker) obs).getLabel()); + } + requestChartLayout(); + }; + + valueMarkerLabelMap.put(markerToAdd, cl); + markerToAdd.enableLabelProperty().addListener(cl); + + // Add it to the chart + getChartChildren().add(p); + + if (markerToAdd.enableLabelProperty().get()) { + getChartChildren().add(markerToAdd.getLabel()); } + + markerLock.unlock(); } - @Override - protected void dataItemRemoved(Data item, Series series) { - // TODO Auto-generated method stub + /** + * The current displayed data value plotted on the X axis. This may be the same + * as xValue or different. It is used by XYChart to animate the xValue from the + * old value to the new value. This is what you should plot in any custom + * XYChart implementations. Some XYChart chart implementations such as LineChart + * also use this to animate when data is added or removed. + * + * @param item The XYChart.Data item from which the current X axis data value is + * obtained + * @return The current displayed X data value + */ + public X getCoordinate(Data item) { + return this.getCurrentDisplayedXValue(item); + } + /** + * The current displayed data value plotted on the Y axis. This may be the same + * as yValue or different. It is used by XYChart to animate the yValue from the + * old value to the new value. This is what you should plot in any custom + * XYChart implementations. Some XYChart chart implementations such as LineChart + * also use this to animate when data is added or removed. + * + * @param item The XYChart.Data item from which the current Y axis data value is + * obtained + * @return The current displayed Y data value + */ + public Y getYCoordinate(Data item) { + return this.getCurrentDisplayedYValue(item); } - // Never overwritten. some other requestChartLayout probably catches it - @Override - protected void dataItemChanged(Data item) { + /** + * @param series the typed series + * @return true if the series is displayed false if it is hidden + */ + public boolean isSeriesVisible(TypedSeries series) { + return seriesVisibility.get(series.getSeries()); } - @Override - protected void seriesAdded(Series series, int seriesIndex) { + /** + * Remove a value marker from the chart + * + * @param markerToRemove the marker to remove + */ + public void removeValueMarker(ValueMarker markerToRemove) { + markerLock.lock(); + Path p = valueMarkers.remove(markerToRemove); + if (p != null) { + getChartChildren().remove(p); + } - int freeIndex = availableColors.nextClearBit(0) % availableColors.size(); - availableColors.set(freeIndex); + // We also have to remove the listener or else we leake memory + ChangeListener l = valueMarkerLabelMap.remove(markerToRemove); + if (l != null) { + markerToRemove.enableLabelProperty().removeListener(l); + } + markerLock.unlock(); + } - seriesColorMap.put(series, freeIndex); + public void setSeriesVisibility(TypedSeries series, boolean b) { + toggleSeriesVisability(series.getSeries(), b); - // Chart specific setup - TypedSeries ser = typedSeries.get(series); + LegendItem lItem = legendMap.get(series.getSeries()); - switch (ser.getSeriesType()) { - case AREA: - // create new paths for series - Path seriesLine = new Path(); - Path fillPath = new Path(); - seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL); - Group areaGroup = new Group(fillPath, seriesLine); - series.setNode(areaGroup); - // create series Y multiplier - DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier"); - seriesYMultiplierMap.put(series, seriesYAnimMultiplier); - seriesYAnimMultiplier.setValue(1d); - getPlotChildren().add(areaGroup); - break; - case LINE: - // Line and area charts require a path node - seriesLine = new Path(); - seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL); - series.setNode(seriesLine); - // create series Y multiplier - seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier"); - seriesYMultiplierMap.put(series, seriesYAnimMultiplier); - seriesYAnimMultiplier.setValue(1d); - getPlotChildren().add(seriesLine); - break; - case SCATTER: - // No need to set up - break; - case STACKED_AREA: - case STACKED_BAR: - throw new UnsupportedOperationException("Not implemented yet"); - default: - break; + // Get the legends and update it accordingly + Node symbol = lItem.getSymbol(); + ObservableList cssClass = symbol.getStyleClass(); + // TODO duplicated code. + if (cssClass.contains("hide-series")) { + symbol.setEffect(null); + symbol.getStyleClass().remove("hide-series"); + toggleSeriesVisability(series.getSeries(), true); + } else { + symbol.getStyleClass().add("hide-series"); + ColorAdjust colorAdjust = new ColorAdjust(); + colorAdjust.setSaturation(-0.5); + symbol.setEffect(colorAdjust); + toggleSeriesVisability(series.getSeries(), false); + } + } + private void createSymbols(Series series, int itemIndex, Data item, String symbolCSSIdentifier) { + Node symbol = item.getNode(); + // TODO should we also check here is e.g. scatter chart ticks should be drawn? + if (symbol == null) { + symbol = new StackPane(); + symbol.setAccessibleRole(AccessibleRole.TEXT); + symbol.setAccessibleRoleDescription("Point"); + symbol.focusTraversableProperty().bind(Platform.accessibilityActiveProperty()); + item.setNode(symbol); } - // Finally add the individual data points - for (int j = 0; j < series.getData().size(); j++) { - dataItemAdded(series, j, series.getData().get(j)); + if (symbol != null) { + symbol.getStyleClass().addAll(symbolCSSIdentifier, "series" + getData().indexOf(series), "data" + itemIndex, + COLOR_CSS_CLASS + seriesColorMap.get(series)); } } - @Override - protected void seriesRemoved(Series series) { + // Layout - SeriesType removedSeriesType = typedSeries.remove(series).getSeriesType(); + /** + * Add an additional axis to the chart. This method is automatically called + * whenever a series is added to the chart with not already present axis index. + * + * @param index the index this chart can be accessed in the future. Reusing + * already existing indices will overwrite the axis currently + * present. + * @param isX if true is xaxis, if false yaxis + * @param side specify the position of the axis. Valid values for XAxis are Top + * and Bottom. And Left and Right for YAxis. + */ + @SuppressWarnings("unchecked") + protected void addAdditionalAxis(int index, boolean isX, Side side) { - if (shouldAnimate()) { + // todo only number? + Axis axis; - int animationDuration = 0; - switch (removedSeriesType) { - case LINE: - animationDuration = 900; - break; - case SCATTER: - animationDuration = 400; - break; - default: - animationDuration = 500; - } + if (isX) { + // XAxis + axis = new NumberAxis(); + additionalXAxis.put(index, (Axis) axis); + } else { + // YAxis + axis = new NumberAxis(); + additionalYAxis.put(index, (Axis) axis); + } - // Can't we use this for all chart types? Why do we need an additional one for - // catter? + axis.setSide(side); - // What happened to the exceptions? - // TODO do we want to use reflection or simply copy and paste the method? - try { - Method method = XYChart.class.getMethod("createSeriesRemoveTimeLine", Series.class, Integer.class); - method.setAccessible(true); - KeyFrame[] keyframes = (KeyFrame[]) method.invoke(this, series, animationDuration); - method.setAccessible(false); - Timeline tl = new Timeline(keyframes); - tl.play(); - } catch (Exception e) { - e.printStackTrace(); - } - } else { + // package private. But as far as I can see this is not necessary when the side + // is set? + // axis.setEffectiveOrientation(Orientation.VERTICAL); - // Not all types of charts use series nodes. Check before removing - if (series.getNode() != null) { - getPlotChildren().remove(series.getNode()); - } + axis.autoRangingProperty().addListener((ov, t, t1) -> { + updateAxisRange(); + }); - // Everything else is the same - for (final Data d : series.getData()) { - final Node symbol = d.getNode(); - getPlotChildren().remove(symbol); - } - removeSeriesFromDisplay(series); + getChartChildren().add(axis); + + // Make space for the axis. We are using the padding since we don't have enough + // access to other components + + // Construct the binding + ReadOnlyDoubleProperty[] axisWidthAndHeightProperty = new ReadOnlyDoubleProperty[additionalYAxis.size() + + additionalXAxis.size()]; + + int i = 0; + for (var entry : additionalYAxis.entrySet()) { + axisWidthAndHeightProperty[i++] = entry.getValue().widthProperty(); } - // if (shouldAnimate()) { - // ParallelTransition pt = new ParallelTransition(); - // pt.setOnFinished(event -> { - // removeSeriesFromDisplay(series); - // }); - // for (final Data d : series.getData()) { - // final Node symbol = d.getNode(); - // // fade out old symbol - // FadeTransition ft = new FadeTransition(Duration.millis(500), symbol); - // ft.setToValue(0); - // ft.setOnFinished(actionEvent -> { - // getPlotChildren().remove(symbol); - // symbol.setOpacity(1.0); - // }); - // pt.getChildren().add(ft); - // } - // pt.play(); - // } + for (var entry : additionalXAxis.entrySet()) { + axisWidthAndHeightProperty[i++] = entry.getValue().heightProperty(); + } - } + ObjectBinding paddingBinding = new ObjectBinding<>() { - @Override - protected void seriesChanged(ListChangeListener.Change c) { + { + super.bind(axisWidthAndHeightProperty); + } - for (int i = 0; i < getData().size(); i++) { - final Series s = getData().get(i); + @Override + protected Insets computeValue() { - TypedSeries type = typedSeries.get(s); + double top = 0, right = 0, bottom = 0, left = 0; - String seriesColor = COLOR_CSS_CLASS + seriesColorMap.get(s); + for (var entry : additionalYAxis.entrySet()) { - switch (type.getSeriesType()) { - case AREA: - Path seriesLine = (Path) ((Group) s.getNode()).getChildren().get(1); - Path fillPath = (Path) ((Group) s.getNode()).getChildren().get(0); - seriesLine.getStyleClass().setAll(AREA_CHART_SERIES_LINE_CSS_CLASS, "series" + i, seriesColor); - fillPath.getStyleClass().setAll(AREA_CHART_FILL_CSS_CLASS, "series" + i, seriesColor); - for (int j = 0; j < s.getData().size(); j++) { - final Data item = s.getData().get(j); - final Node node = item.getNode(); - if (node != null) - node.getStyleClass().setAll(AREA_CHART_SYMBOL_CSS_CLASS, "series" + i, "data" + j, seriesColor); - } - break; - case LINE: - Node seriesNode = s.getNode(); - if (seriesNode != null) - seriesNode.getStyleClass().setAll(LINE_CHART_LINE_CSS_CLASS, "series" + i, seriesColor); - for (int j = 0; j < s.getData().size(); j++) { - final Node symbol = s.getData().get(j).getNode(); - if (symbol != null) - symbol.getStyleClass().setAll(LINE_CHART_SYMBOL_CSS_CLASS, "series" + i, "data" + j, - seriesColor); + double width = entry.getValue().getWidth(); + + switch (entry.getValue().getSide()) { + case LEFT: + left += (width + AXIS_PADDING); + break; + case RIGHT: + right += (width + AXIS_PADDING); + break; + default: + throw new IllegalStateException("Y Axis may only be positioned right or left"); + } } - break; - case SCATTER: - for (int j = 0; j < s.getData().size(); j++) { - final Node symbol = s.getData().get(j).getNode(); - if (symbol != null) - symbol.getStyleClass().setAll(SCATTER_CHART_SYMBOL_CSS_CLASS, "series" + i, "data" + j, - seriesColor); + for (var entry : additionalXAxis.entrySet()) { + double height = entry.getValue().getHeight(); + + System.out.println("X Axis: " + height + " " + entry.getValue() + " " + entry.getValue().getWidth() + + entry.getValue().getSide()); + + switch (entry.getValue().getSide()) { + case TOP: + top += (height + AXIS_PADDING); + break; + case BOTTOM: + bottom += (height + AXIS_PADDING); + break; + default: + throw new IllegalStateException("X Axis may only be positioned top or bottom"); + } } - break; - case STACKED_AREA: - case STACKED_BAR: - default: - throw new UnsupportedOperationException("Not implemented yet"); - } + System.out.println(top + " " + right + " " + bottom + " " + left); + return new Insets(top, right, bottom, left); - } - } + } - // Layout + }; - @Override - protected void layoutPlotChildren() { + this.paddingProperty().bind(paddingBinding); - List constructedPath = new ArrayList<>(getDataSizeMultiType()); + this.requestChartLayout(); + } - series: for (int seriesIndex = 0; seriesIndex < getDataSizeMultiType(); seriesIndex++) { - Series series = getData().get(seriesIndex); + // Add data item + protected void addAreaItem(Series series, int itemIndex, Data item) { + // TODO add animation code + createSymbols(series, itemIndex, item, AREA_CHART_SYMBOL_CSS_CLASS); + getPlotChildren().add(item.getNode()); + } - //If it's not visible don't layout - - var typed = typedSeries.get(series); - - SeriesType type = typed.getSeriesType(); - int xAxisIndex = typed.getXAxisIndex(); - int yAxisIndex = typed.getYAxisIndex(); - - final Axis xAxis; - final Axis yAxis; - - if(xAxisIndex == 0) { - xAxis = getXAxis(); - }else { - xAxis = additionalXAxis.get(xAxisIndex); - } - - if(yAxisIndex == 0) { - yAxis = getYAxis(); - }else { - yAxis = additionalYAxis.get(yAxisIndex); - } + protected void addLineItem(Series series, int itemIndex, Data item) { - // Not really object oriented but hey. Work with what we got here ... - switch (type) { - case AREA: - layoutAreaChart(series, constructedPath,xAxis,yAxis); - break; - case LINE: - layoutLineChart(series, constructedPath,xAxis,yAxis); - break; - case SCATTER: - layoutScatterSeries(series,xAxis,yAxis); - break; - default: - LOG.warning("No layout method found for: " + type.name() + ". Skip series: " + series); - continue series; - } + // Create the node + if (typedSeries.get(series).showSymbolsProperty().get()) { + createSymbols(series, itemIndex, item, LINE_CHART_SYMBOL_CSS_CLASS); } + Node symbol = item.getNode(); - // Take care of markers - - final Axis xAxis = getXAxis(); - final Axis yAxis = getYAxis(); - //double maxY = xAxis.getWidth(); - //double maxX = yAxis.getHeight(); + if (symbol != null) { + getPlotChildren().add(symbol); + } - // 10 px padding - double yMin = yAxis.getBoundsInLocal().getMaxY() + 5; - double yMax = yMin - plotArea.getBoundsInParent().getHeight(); + // TODO add animation code + } - markerLock.lock(); - Iterator, Path>> iter = valueMarkers.entrySet().iterator(); + protected void addScatterItem(Series series, int itemIndex, Data item) { - while (iter.hasNext()) { + createSymbols(series, itemIndex, item, SCATTER_CHART_SYMBOL_CSS_CLASS); + Node symbol = item.getNode(); - var entry = iter.next(); - ValueMarker marker = entry.getKey(); - Path p = entry.getValue(); - p.getElements().clear(); - try { - p.setStroke(marker.getColor()); + // add and fade in new symbol if animated + if (shouldAnimate()) { + symbol.setOpacity(0); + getPlotChildren().add(symbol); + FadeTransition ft = new FadeTransition(Duration.millis(500), symbol); + ft.setToValue(1); + ft.play(); + } else { + getPlotChildren().add(symbol); + } + } - Label label = null; - if (marker.enableLabelProperty().get()) { - label = (Label) marker.getLabel(); - label.setTextFill(ColorUtil.getContrastColor(marker.getColor())); - label.setBackground( - new Background(new BackgroundFill(marker.getColor(), new CornerRadii(3), null))); - // relocate label + protected void addBarItem(Series series, int itemIndex, Data item) { - } + // Add bar nodes - if (marker.isVertical()) { + } - double x = xAxis.getDisplayPosition((X) marker.getValue()) + xAxis.getLayoutX(); - // Check if it is currently displayed - if (!Double.valueOf(x).isNaN() && (x != xAxis.getZeroPosition() || !isVerticalZeroLineVisible()) + @Override + protected void dataItemAdded(Series series, int itemIndex, Data item) { + switch (typedSeries.get(series).getSeriesType()) { + case AREA: + addAreaItem(series, itemIndex, item); + break; + case LINE: + addLineItem(series, itemIndex, item); + break; + case SCATTER: + addScatterItem(series, itemIndex, item); + break; + case BAR: + addBarItem(series, itemIndex, item); + case STACKED_AREA: + break; + case STACKED_BAR: + break; + default: + break; + } + } + + // Never overwritten. some other requestChartLayout probably catches it + @Override + protected void dataItemChanged(Data item) { + } + + @Override + protected void dataItemRemoved(Data item, Series series) { + } + + // utility + protected int getDataSizeMultiType() { + final ObservableList> data = getData(); + return (data != null) ? data.size() : 0; + } + + // Go with the more specific implementation for line and area charts. + // TODO we could auto range to not stick to the 0 line + + protected void layoutAreaChart(Series series, List constructedPath, Axis xAxis, Axis yAxis) { + final ObservableList children = ((Group) series.getNode()).getChildren(); + + boolean visible = seriesVisibility.get(series); + + Path fillPath = (Path) children.get(0); + Path linePath = (Path) children.get(1); + + var cssPath = fillPath.getStyleClass(); + var cssLine = linePath.getStyleClass(); + + if (!visible) { + if (!cssPath.contains("hide-series-symbol")) { + cssPath.add("hide-series-symbol"); + cssLine.add("hide-series-symbol"); + + // Also add the hide artifact to the symbols + for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) { + Node symbol = it.next().getNode(); + symbol.getStyleClass().add("hide-series-symbol"); + } + } + } else { + if (cssPath.contains("hide-series-symbol")) { + cssPath.remove("hide-series-symbol"); + cssLine.add("hide-series-symbol"); + // Also add the hide artifact to the symbols + for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) { + Node symbol = it.next().getNode(); + symbol.getStyleClass().remove("hide-series-symbol"); + } + } + makePaths(series, constructedPath, fillPath, linePath, SortingPolicy.X_AXIS, xAxis, yAxis); + } + } + + protected void layoutLineChart(Series series, List constructedPath, Axis xAxis, Axis yAxis) { + final Node seriesNode = series.getNode(); + if (seriesNode instanceof Path) { + + boolean visible = seriesVisibility.get(series); + var cssPath = seriesNode.getStyleClass(); + + if (!visible) { + if (!cssPath.contains("hide-series-symbol")) { + cssPath.add("hide-series-symbol"); + + // Also add the hide artifact to the symbols + for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) { + Node symbol = it.next().getNode(); + symbol.getStyleClass().add("hide-series-symbol"); + } + } + } else { + if (cssPath.contains("hide-series-symbol")) { + cssPath.remove("hide-series-symbol"); + // Also add the hide artifact to the symbols + for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) { + Node symbol = it.next().getNode(); + symbol.getStyleClass().remove("hide-series-symbol"); + } + } + makePaths(series, constructedPath, null, (Path) seriesNode, typedSeries.get(series).getSortingPolicy(), + xAxis, yAxis); + } + + } + } + + protected void layoutBarChart(Series series, Axis xAxis, Axis yAxis) { + + + // Define which axis is which axis + + + + //TODO + Orientation orientation = Orientation.VERTICAL; + CategoryAxis categoryAxis; + ValueAxis valueAxis; + + double catSpace = categoryAxis.getCategorySpacing(); + // calculate bar spacing + + final double availableBarSpace = catSpace - (getCategoryGap() + getBarGap()); + double barWidth = (availableBarSpace / series.getData().size()) - getBarGap(); + final double barOffset = -((catSpace - getCategoryGap()) / 2); + final double zeroPos = (valueAxis.getLowerBound() > 0) ? valueAxis.getDisplayPosition(valueAxis.getLowerBound()) + : valueAxis.getZeroPosition(); + // RT-24813 : if the data in a series gets too large, barWidth can get negative. + if (barWidth <= 0) + barWidth = 1; + // update bar positions and sizes + int catIndex = 0; + for (String category : categoryAxis.getCategories()) { + int index = 0; +// for (Iterator> sit = getDisplayedSeriesIterator(); sit.hasNext(); ) { +// +// } + final Data item = getDataItem(series, category); + if (item != null) { + final Node bar = item.getNode(); + final double categoryPos; + final double valPos; + if (orientation == Orientation.VERTICAL) { + + //TODO + categoryPos = xAxis.getDisplayPosition(item.getXValue()); + valPos = yAxis.getDisplayPosition(item.getYValue()); +// categoryPos = getXAxis().getDisplayPosition(item.getCurrentX()); +// valPos = getYAxis().getDisplayPosition(item.getCurrentY()); + } else { + categoryPos = yAxis.getDisplayPosition(item.getYValue()); + valPos = xAxis.getDisplayPosition(item.getXValue()); + +// categoryPos = getYAxis().getDisplayPosition(item.getCurrentY()); +// valPos = getXAxis().getDisplayPosition(item.getCurrentX()); + } + if (Double.isNaN(categoryPos) || Double.isNaN(valPos)) { + continue; + } + final double bottom = Math.min(valPos, zeroPos); + final double top = Math.max(valPos, zeroPos); + + //TODO + //bottomPos = bottom; + + if (orientation == Orientation.VERTICAL) { + bar.resizeRelocate(categoryPos + barOffset + (barWidth + getBarGap()) * index, bottom, barWidth, + top - bottom); + } else { + // noinspection SuspiciousNameCombination + bar.resizeRelocate(bottom, categoryPos + barOffset + (barWidth + getBarGap()) * index, top - bottom, + barWidth); + } + + index++; + } + catIndex++; + } + } + + @Override + protected void layoutPlotChildren() { + + List constructedPath = new ArrayList<>(getDataSizeMultiType()); + + series: for (int seriesIndex = 0; seriesIndex < getDataSizeMultiType(); seriesIndex++) { + Series series = getData().get(seriesIndex); + + // If it's not visible don't layout + + var typed = typedSeries.get(series); + + SeriesType type = typed.getSeriesType(); + int xAxisIndex = typed.getXAxisIndex(); + int yAxisIndex = typed.getYAxisIndex(); + + final Axis xAxis; + final Axis yAxis; + + if (xAxisIndex == 0) { + xAxis = getXAxis(); + } else { + xAxis = additionalXAxis.get(xAxisIndex); + } + + if (yAxisIndex == 0) { + yAxis = getYAxis(); + } else { + yAxis = additionalYAxis.get(yAxisIndex); + } + + // Not really object oriented but hey. Work with what we got here ... + switch (type) { + case AREA: + layoutAreaChart(series, constructedPath, xAxis, yAxis); + break; + case LINE: + layoutLineChart(series, constructedPath, xAxis, yAxis); + break; + case SCATTER: + layoutScatterSeries(series, xAxis, yAxis); + break; + case BAR: + layoutBarChart(series, xAxis, yAxis); + default: + LOG.warning("No layout method found for: " + type.name() + ". Skip series: " + series); + continue series; + } + } + + // Take care of markers + + final Axis xAxis = getXAxis(); + final Axis yAxis = getYAxis(); + // double maxY = xAxis.getWidth(); + // double maxX = yAxis.getHeight(); + + // 10 px padding + double yMin = yAxis.getBoundsInLocal().getMaxY() + 5; + double yMax = yMin - plotArea.getBoundsInParent().getHeight(); + + markerLock.lock(); + Iterator, Path>> iter = valueMarkers.entrySet().iterator(); + + while (iter.hasNext()) { + + var entry = iter.next(); + ValueMarker marker = entry.getKey(); + Path p = entry.getValue(); + p.getElements().clear(); + try { + p.setStroke(marker.getColor()); + + Label label = null; + if (marker.enableLabelProperty().get()) { + label = (Label) marker.getLabel(); + label.setTextFill(ColorUtil.getContrastColor(marker.getColor())); + label.setBackground( + new Background(new BackgroundFill(marker.getColor(), new CornerRadii(3), null))); + // relocate label + + } + + if (marker.isVertical()) { + + double x = xAxis.getDisplayPosition((X) marker.getValue()) + xAxis.getLayoutX(); + // Check if it is currently displayed + if (!Double.valueOf(x).isNaN() && (x != xAxis.getZeroPosition() || !isVerticalZeroLineVisible()) && x > 0 && x <= xAxis.getWidth()) { - // 0.5 offest to counter anti aliasing + // 0.5 offset to counter anti aliasing p.getElements().addAll(new MoveTo(x + 0.5, yMin), new LineTo(x + 0.5, yMax)); if (label != null) { @@ -646,48 +833,51 @@ protected void layoutPlotChildren() { } markerLock.unlock(); - // layout additional axis should be done in requestAxisLayout but it's once again final .... - + // layout additional axis should be done in requestAxisLayout but it's once + // again final .... + double offsetRight = 0; - double offsetLeft = - (yAxis.getWidth()*2 + AXIS_PADDING); + double offsetLeft = -(yAxis.getWidth() * 2 + AXIS_PADDING); double offsetTop = 0; double offsetBottom = xAxis.getHeight() + AXIS_PADDING; - - for(var axisEntry : additionalYAxis.entrySet()) { - //Shall we create an array after each new axis addition / deletion for quicker traversal? - //Hashmaps are more expensive for sure... + + for (var axisEntry : additionalYAxis.entrySet()) { + // Shall we create an array after each new axis addition / deletion for quicker + // traversal? + // Hashmaps are more expensive for sure... Axis additional = axisEntry.getValue(); - //Original Y Axis is already layed out. We still need to compute the width as label sizes might - //differ depending on values + // Original Y Axis is already layed out. We still need to compute the width as + // label sizes might + // differ depending on values double prefWidth = snapSizeY(additional.prefWidth(getYAxis().getHeight())); - - if(additional.getSide().equals(Side.RIGHT)) { - additional.resizeRelocate(plotArea.getBoundsInParent().getMaxX()+offsetRight, getYAxis().getLayoutY(), + + if (additional.getSide().equals(Side.RIGHT)) { + additional.resizeRelocate(plotArea.getBoundsInParent().getMaxX() + offsetRight, getYAxis().getLayoutY(), prefWidth, getYAxis().getHeight()); offsetRight += prefWidth + AXIS_PADDING; - }else if(additional.getSide().equals(Side.LEFT)) { - additional.resizeRelocate(plotArea.getBoundsInParent().getMinX()+offsetLeft, getYAxis().getLayoutY(), + } else if (additional.getSide().equals(Side.LEFT)) { + additional.resizeRelocate(plotArea.getBoundsInParent().getMinX() + offsetLeft, getYAxis().getLayoutY(), prefWidth, getYAxis().getHeight()); offsetLeft -= (prefWidth + AXIS_PADDING); } } - - for(var axisEntry : additionalXAxis.entrySet()) { - //Shall we create an array after each new axis addition / deletion for quicker traversal? - //Hashmaps are more expensive for sure... + + for (var axisEntry : additionalXAxis.entrySet()) { + // Shall we create an array after each new axis addition / deletion for quicker + // traversal? + // Hashmaps are more expensive for sure... Axis additional = axisEntry.getValue(); - //Original Y Axis is already layed out. We still need to compute the width as label sizes might - //differ depending on values + // Original Y Axis is already layed out. We still need to compute the width as + // label sizes might + // differ depending on values double prefHeight = snapSizeY(additional.prefHeight(getXAxis().getWidth())); - if(additional.getSide().equals(Side.TOP)) { + if (additional.getSide().equals(Side.TOP)) { additional.resizeRelocate(plotArea.getBoundsInParent().getMinX(), - plotArea.getBoundsInParent().getMinY()+offsetTop-prefHeight, - xAxis.getWidth(), - prefHeight); - + plotArea.getBoundsInParent().getMinY() + offsetTop - prefHeight, xAxis.getWidth(), prefHeight); + offsetTop -= (prefHeight + AXIS_PADDING); - }else if(additional.getSide().equals(Side.BOTTOM)) { + } else if (additional.getSide().equals(Side.BOTTOM)) { // additional.resizeRelocate( // plotArea.getBoundsInParent().getMinX(), // xAxis.getLayoutBounds().getMaxY() + offsetBottom, @@ -695,88 +885,16 @@ protected void layoutPlotChildren() { // prefHeight); // offsetLeft -= (prefHeight + AXIS_PADDING); } - + } // } - protected void layoutLineChart(Series series, List constructedPath,Axis xAxis, Axis yAxis) { - final Node seriesNode = series.getNode(); - if (seriesNode instanceof Path) { - - boolean visible = seriesVisibility.get(series); - var cssPath = seriesNode.getStyleClass(); - - if(!visible) { - if(!cssPath.contains("hide-series-symbol")) { - cssPath.add("hide-series-symbol"); - - //Also add the hide artifact to the symbols - for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) { - Node symbol = it.next().getNode(); - symbol.getStyleClass().add("hide-series-symbol"); - } - } - }else { - if(cssPath.contains("hide-series-symbol")) { - cssPath.remove("hide-series-symbol"); - //Also add the hide artifact to the symbols - for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) { - Node symbol = it.next().getNode(); - symbol.getStyleClass().remove("hide-series-symbol"); - } - } - makePaths(series, constructedPath, null, (Path) seriesNode, typedSeries.get(series).getSortingPolicy(),xAxis,yAxis); - } - - - - - } - } + protected void layoutScatterSeries(Series series, Axis xAxis, Axis yAxis) { - protected void layoutAreaChart(Series series, List constructedPath,Axis xAxis, Axis yAxis) { - final ObservableList children = ((Group) series.getNode()).getChildren(); - boolean visible = seriesVisibility.get(series); - - Path fillPath = (Path) children.get(0); - Path linePath = (Path) children.get(1); - - var cssPath = fillPath.getStyleClass(); - var cssLine = linePath.getStyleClass(); - - if(!visible) { - if(!cssPath.contains("hide-series-symbol")) { - cssPath.add("hide-series-symbol"); - cssLine.add("hide-series-symbol"); - - //Also add the hide artifact to the symbols - for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) { - Node symbol = it.next().getNode(); - symbol.getStyleClass().add("hide-series-symbol"); - } - } - }else { - if(cssPath.contains("hide-series-symbol")) { - cssPath.remove("hide-series-symbol"); - cssLine.add("hide-series-symbol"); - //Also add the hide artifact to the symbols - for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) { - Node symbol = it.next().getNode(); - symbol.getStyleClass().remove("hide-series-symbol"); - } - } - makePaths(series, constructedPath, fillPath, linePath, SortingPolicy.X_AXIS,xAxis,yAxis); - } - } - protected void layoutScatterSeries(Series series,Axis xAxis, Axis yAxis) { - - boolean visible = seriesVisibility.get(series); - - for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) { Data item = it.next(); // We don't have access to the item's current value circumvent using chart @@ -787,14 +905,14 @@ protected void layoutScatterSeries(Series series,Axis xAxis, Axis yA return; } Node symbol = item.getNode(); - + var css = symbol.getStyleClass(); - if(!visible) { - if(!css.contains("hide-series-symbol")) { + if (!visible) { + if (!css.contains("hide-series-symbol")) { css.add("hide-series-symbol"); } - }else { - if(css.contains("hide-series-symbol")) { + } else { + if (css.contains("hide-series-symbol")) { css.remove("hide-series-symbol"); } @@ -807,125 +925,379 @@ protected void layoutScatterSeries(Series series,Axis xAxis, Axis yA } } - // Add data item - protected void addAreaItem(Series series, int itemIndex, Data item) { - // TODO add animation code - createSymbols(series, itemIndex, item, AREA_CHART_SYMBOL_CSS_CLASS); - getPlotChildren().add(item.getNode()); - } + protected void makePaths(Series series, List constructedPath, Path fillPath, Path linePath, + SortingPolicy sortAxis, Axis xAxis, Axis yAxis) { - protected void addScatterItem(Series series, int itemIndex, Data item) { + double yAnimMultiplier = seriesYMultiplierMap.get(series).get(); + final double hlw = linePath.getStrokeWidth() / 2.0; + final boolean sortX = (sortAxis == SortingPolicy.X_AXIS); + final boolean sortY = (sortAxis == SortingPolicy.Y_AXIS); + final double dataXMin = sortX ? -hlw : Double.NEGATIVE_INFINITY; + final double dataXMax = sortX ? xAxis.getWidth() + hlw : Double.POSITIVE_INFINITY; + final double dataYMin = sortY ? -hlw : Double.NEGATIVE_INFINITY; + final double dataYMax = sortY ? yAxis.getHeight() + hlw : Double.POSITIVE_INFINITY; + LineTo prevDataPoint = null; + LineTo nextDataPoint = null; + constructedPath.clear(); + for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) { + Data item = it.next(); + double x = xAxis.getDisplayPosition(getCurrentDisplayedXValue(item)); + double y = yAxis.getDisplayPosition( + yAxis.toRealValue(yAxis.toNumericValue(getCurrentDisplayedYValue(item)) * yAnimMultiplier)); + boolean skip = (Double.isNaN(x) || Double.isNaN(y)); + Node symbol = item.getNode(); + if (symbol != null) { + final double w = symbol.prefWidth(-1); + final double h = symbol.prefHeight(-1); + if (skip) { + symbol.resizeRelocate(-w * 2, -h * 2, w, h); + } else { + symbol.resizeRelocate(x - (w / 2), y - (h / 2), w, h); + } + } + if (skip) + continue; + if (x < dataXMin || y < dataYMin) { + if (prevDataPoint == null) { + prevDataPoint = new LineTo(x, y); + } else if ((sortX && prevDataPoint.getX() <= x) || (sortY && prevDataPoint.getY() <= y)) { + prevDataPoint.setX(x); + prevDataPoint.setY(y); + } + } else if (x <= dataXMax && y <= dataYMax) { + constructedPath.add(new LineTo(x, y)); + } else { + if (nextDataPoint == null) { + nextDataPoint = new LineTo(x, y); + } else if ((sortX && x <= nextDataPoint.getX()) || (sortY && y <= nextDataPoint.getY())) { + nextDataPoint.setX(x); + nextDataPoint.setY(y); + } + } + } - createSymbols(series, itemIndex, item, SCATTER_CHART_SYMBOL_CSS_CLASS); - Node symbol = item.getNode(); + if (!constructedPath.isEmpty() || prevDataPoint != null || nextDataPoint != null) { + if (sortX) { + Collections.sort(constructedPath, (e1, e2) -> Double.compare(e1.getX(), e2.getX())); + } else if (sortY) { + Collections.sort(constructedPath, (e1, e2) -> Double.compare(e1.getY(), e2.getY())); + } else { + // assert prevDataPoint == null && nextDataPoint == null + } + if (prevDataPoint != null) { + constructedPath.add(0, prevDataPoint); + } + if (nextDataPoint != null) { + constructedPath.add(nextDataPoint); + } - // add and fade in new symbol if animated - if (shouldAnimate()) { - symbol.setOpacity(0); - getPlotChildren().add(symbol); - FadeTransition ft = new FadeTransition(Duration.millis(500), symbol); - ft.setToValue(1); - ft.play(); - } else { - getPlotChildren().add(symbol); + // assert !constructedPath.isEmpty() + LineTo first = constructedPath.get(0); + LineTo last = constructedPath.get(constructedPath.size() - 1); + + final double displayYPos = first.getY(); + + ObservableList lineElements = linePath.getElements(); + lineElements.clear(); + lineElements.add(new MoveTo(first.getX(), displayYPos)); + lineElements.addAll(constructedPath); + + if (fillPath != null) { + ObservableList fillElements = fillPath.getElements(); + fillElements.clear(); + double yOrigin = yAxis.getDisplayPosition(yAxis.toRealValue(0.0)); + + fillElements.add(new MoveTo(first.getX(), yOrigin)); + fillElements.addAll(constructedPath); + fillElements.add(new LineTo(last.getX(), yOrigin)); + fillElements.add(new ClosePath()); + } } } - protected void addLineItem(Series series, int itemIndex, Data item) { + @Override + protected void seriesAdded(Series series, int seriesIndex) { + + int freeIndex = availableColors.nextClearBit(0) % availableColors.size(); + availableColors.set(freeIndex); + + seriesColorMap.put(series, freeIndex); + + // Chart specific setup + TypedSeries ser = typedSeries.get(series); + + switch (ser.getSeriesType()) { + case AREA: + // create new paths for series + Path seriesLine = new Path(); + Path fillPath = new Path(); + seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL); + Group areaGroup = new Group(fillPath, seriesLine); + series.setNode(areaGroup); + // create series Y multiplier + DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier"); + seriesYMultiplierMap.put(series, seriesYAnimMultiplier); + seriesYAnimMultiplier.setValue(1d); + getPlotChildren().add(areaGroup); + break; + case LINE: + // Line and area charts require a path node + seriesLine = new Path(); + seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL); + series.setNode(seriesLine); + // create series Y multiplier + seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier"); + seriesYMultiplierMap.put(series, seriesYAnimMultiplier); + seriesYAnimMultiplier.setValue(1d); + getPlotChildren().add(seriesLine); + break; + + case BAR: + //TODO + Orientation orientation = Orientation.VERTICAL; + + Map> categoryMap = new HashMap>(); + for (int j = 0; j < series.getData().size(); j++) { + Data item = series.getData().get(j); + Node bar = createBar(series, seriesIndex, item, j); + String category; + if (orientation == Orientation.VERTICAL) { + category = (String) item.getXValue(); + } else { + category = (String) item.getYValue(); + } + categoryMap.put(category, item); + + // RT-21164 check if bar value is negative to add NEGATIVE_STYLE style class + double barVal = (orientation == Orientation.VERTICAL) ? ((Number) item.getYValue()).doubleValue() + : ((Number) item.getXValue()).doubleValue(); + if (barVal < 0) { + bar.getStyleClass().add(NEGATIVE_STYLE); + } + getPlotChildren().add(bar); + + } + if (categoryMap.size() > 0) + seriesCategoryMap.put(series, categoryMap); + break; + + case SCATTER: + // No need to set up + break; + case STACKED_AREA: + case STACKED_BAR: + throw new UnsupportedOperationException("Not implemented yet"); + default: + break; - // Create the node - if (typedSeries.get(series).showSymbolsProperty().get()) { - createSymbols(series, itemIndex, item, LINE_CHART_SYMBOL_CSS_CLASS); } - Node symbol = item.getNode(); - if (symbol != null) { - getPlotChildren().add(symbol); + // Finally add the individual data points + for (int j = 0; j < series.getData().size(); j++) { + dataItemAdded(series, j, series.getData().get(j)); } + } - // TODO add animation code + @Override + protected void seriesChanged(ListChangeListener.Change c) { + + for (int i = 0; i < getData().size(); i++) { + final Series s = getData().get(i); + + TypedSeries type = typedSeries.get(s); + + String seriesColor = COLOR_CSS_CLASS + seriesColorMap.get(s); + + switch (type.getSeriesType()) { + case AREA: + Path seriesLine = (Path) ((Group) s.getNode()).getChildren().get(1); + Path fillPath = (Path) ((Group) s.getNode()).getChildren().get(0); + seriesLine.getStyleClass().setAll(AREA_CHART_SERIES_LINE_CSS_CLASS, "series" + i, seriesColor); + fillPath.getStyleClass().setAll(AREA_CHART_FILL_CSS_CLASS, "series" + i, seriesColor); + for (int j = 0; j < s.getData().size(); j++) { + final Data item = s.getData().get(j); + final Node node = item.getNode(); + if (node != null) + node.getStyleClass().setAll(AREA_CHART_SYMBOL_CSS_CLASS, "series" + i, "data" + j, seriesColor); + } + break; + case LINE: + Node seriesNode = s.getNode(); + if (seriesNode != null) + seriesNode.getStyleClass().setAll(LINE_CHART_LINE_CSS_CLASS, "series" + i, seriesColor); + for (int j = 0; j < s.getData().size(); j++) { + final Node symbol = s.getData().get(j).getNode(); + if (symbol != null) + symbol.getStyleClass().setAll(LINE_CHART_SYMBOL_CSS_CLASS, "series" + i, "data" + j, + seriesColor); + } + break; + + case SCATTER: + for (int j = 0; j < s.getData().size(); j++) { + final Node symbol = s.getData().get(j).getNode(); + if (symbol != null) + symbol.getStyleClass().setAll(SCATTER_CHART_SYMBOL_CSS_CLASS, "series" + i, "data" + j, + seriesColor); + } + break; + case STACKED_AREA: + case STACKED_BAR: + default: + throw new UnsupportedOperationException("Not implemented yet"); + } + + } } - private void createSymbols(Series series, int itemIndex, Data item, String symbolCSSIdentifier) { - Node symbol = item.getNode(); - // TODO should we also check here is e.g. scatter chart ticks should be drawn? - if (symbol == null) { - symbol = new StackPane(); - symbol.setAccessibleRole(AccessibleRole.TEXT); - symbol.setAccessibleRoleDescription("Point"); - symbol.focusTraversableProperty().bind(Platform.accessibilityActiveProperty()); - item.setNode(symbol); + // public double + + @Override + protected void seriesRemoved(Series series) { + + SeriesType removedSeriesType = typedSeries.remove(series).getSeriesType(); + + if (shouldAnimate()) { + + int animationDuration = 0; + switch (removedSeriesType) { + case LINE: + animationDuration = 900; + break; + case SCATTER: + animationDuration = 400; + break; + default: + animationDuration = 500; + } + + // Can't we use this for all chart types? Why do we need an additional one for + // catter? + + // What happened to the exceptions? + // TODO do we want to use reflection or simply copy and paste the method? + try { + Method method = XYChart.class.getMethod("createSeriesRemoveTimeLine", Series.class, Integer.class); + method.setAccessible(true); + KeyFrame[] keyframes = (KeyFrame[]) method.invoke(this, series, animationDuration); + method.setAccessible(false); + Timeline tl = new Timeline(keyframes); + tl.play(); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + + // Not all types of charts use series nodes. Check before removing + if (series.getNode() != null) { + getPlotChildren().remove(series.getNode()); + } + + // Everything else is the same + for (final Data d : series.getData()) { + final Node symbol = d.getNode(); + getPlotChildren().remove(symbol); + } + removeSeriesFromDisplay(series); } - if (symbol != null) { - symbol.getStyleClass().addAll(symbolCSSIdentifier, "series" + getData().indexOf(series), "data" + itemIndex, - COLOR_CSS_CLASS + seriesColorMap.get(series)); - } + // if (shouldAnimate()) { + // ParallelTransition pt = new ParallelTransition(); + // pt.setOnFinished(event -> { + // removeSeriesFromDisplay(series); + // }); + // for (final Data d : series.getData()) { + // final Node symbol = d.getNode(); + // // fade out old symbol + // FadeTransition ft = new FadeTransition(Duration.millis(500), symbol); + // ft.setToValue(0); + // ft.setOnFinished(actionEvent -> { + // getPlotChildren().remove(symbol); + // symbol.setOpacity(1.0); + // }); + // pt.getChildren().add(ft); + // } + // pt.play(); + // } + } - // Go with the more specific implementation for line and area charts. - // TODO we could auto range to not stick to the 0 line + protected void toggleSeriesVisability(Series series, boolean b) { + seriesVisibility.put(series, b); + this.requestChartLayout(); + } + + /* + * Value markers + * + * 3 ways we can implement value markers. 1. Either we create a separate series + * 2. Draw a line on top using stackpanes 3. draw on the plot area emulating + * grid line approach + * + * 1. Is a bit more tricky if we want seperate colors and custom labels + * therefore le + */ @Override protected void updateAxisRange() { - //default axis + // default axis final Axis xa = getXAxis(); final Axis ya = getYAxis(); - - //Feetch axis related data - HashMap> xData = new HashMap<>(); - HashMap> yData = new HashMap<>(); - + + // Feetch axis related data + HashMap> xData = new HashMap<>(); + HashMap> yData = new HashMap<>(); + /* * Check if we have at least one axis which requires automatic layouting */ boolean oneAutoRanging = false; - - //Default axis + + // Default axis if (xa.isAutoRanging()) { xData.put(0, new ArrayList()); oneAutoRanging = true; } - + if (ya.isAutoRanging()) { yData.put(0, new ArrayList()); oneAutoRanging = true; } - + for (var entry : additionalYAxis.entrySet()) { - - if(entry.getValue().isAutoRanging()) { + + if (entry.getValue().isAutoRanging()) { yData.put(entry.getKey(), new ArrayList()); oneAutoRanging = true; } } - - for (var entry : additionalXAxis.entrySet()) { - if(entry.getValue().isAutoRanging()) { + + for (var entry : additionalXAxis.entrySet()) { + if (entry.getValue().isAutoRanging()) { xData.put(entry.getKey(), new ArrayList()); oneAutoRanging = true; } } if (oneAutoRanging) { - - //Collect data for each axis + + // Collect data for each axis for (Series series : getData()) { - - //If the series is not visible don't include it in the axis range - if(!seriesVisibility.get(series)) { + + // If the series is not visible don't include it in the axis range + if (!seriesVisibility.get(series)) { continue; } - - //To which axis does it belong to? + + // To which axis does it belong to? var typed = typedSeries.get(series); int yAxisIndex = typed.getYAxisIndex(); int xAxisIndex = typed.getXAxisIndex(); - + var xDataAxis = xData.get(xAxisIndex); var yDataAxis = yData.get(yAxisIndex); - - if(xDataAxis == null && yDataAxis == null) { + + if (xDataAxis == null && yDataAxis == null) { continue; } for (Data data : series.getData()) { @@ -935,178 +1307,46 @@ protected void updateAxisRange() { yDataAxis.add(data.getYValue()); } } - - //Push new data to each axis - for(var entry : xData.entrySet()) { - + + // Push new data to each axis + for (var entry : xData.entrySet()) { + int axisIndex = entry.getKey(); List xDataPoints = entry.getValue(); Axis axis; - - if(axisIndex == 0) { + + if (axisIndex == 0) { axis = xa; - }else { + } else { axis = additionalXAxis.get(axisIndex); } - - if (xDataPoints != null && !xDataPoints.isEmpty() && !(xDataPoints.size() == 1 && axis.toNumericValue(xDataPoints.get(0)) == 0)) { + + if (xDataPoints != null && !xDataPoints.isEmpty() + && !(xDataPoints.size() == 1 && axis.toNumericValue(xDataPoints.get(0)) == 0)) { axis.invalidateRange(xDataPoints); } } - - for(var entry : yData.entrySet()) { - + + for (var entry : yData.entrySet()) { + int axisIndex = entry.getKey(); List yDataPoints = entry.getValue(); Axis axis; - - if(axisIndex == 0) { + + if (axisIndex == 0) { axis = ya; - }else { + } else { axis = additionalYAxis.get(axisIndex); } - - if (yDataPoints != null && !yDataPoints.isEmpty() && !(yDataPoints.size() == 1 && axis.toNumericValue(yDataPoints.get(0)) == 0)) { - axis.invalidateRange(yDataPoints); - } - } - } - } - - // utility - protected int getDataSizeMultiType() { - final ObservableList> data = getData(); - return (data != null) ? data.size() : 0; - } - - protected void makePaths(Series series, List constructedPath, Path fillPath, Path linePath, - SortingPolicy sortAxis, Axis xAxis, Axis yAxis) { - double yAnimMultiplier = seriesYMultiplierMap.get(series).get(); - final double hlw = linePath.getStrokeWidth() / 2.0; - final boolean sortX = (sortAxis == SortingPolicy.X_AXIS); - final boolean sortY = (sortAxis == SortingPolicy.Y_AXIS); - final double dataXMin = sortX ? -hlw : Double.NEGATIVE_INFINITY; - final double dataXMax = sortX ? xAxis.getWidth() + hlw : Double.POSITIVE_INFINITY; - final double dataYMin = sortY ? -hlw : Double.NEGATIVE_INFINITY; - final double dataYMax = sortY ? yAxis.getHeight() + hlw : Double.POSITIVE_INFINITY; - LineTo prevDataPoint = null; - LineTo nextDataPoint = null; - constructedPath.clear(); - for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) { - Data item = it.next(); - double x = xAxis.getDisplayPosition(getCurrentDisplayedXValue(item)); - double y = yAxis.getDisplayPosition( - yAxis.toRealValue(yAxis.toNumericValue(getCurrentDisplayedYValue(item)) * yAnimMultiplier)); - boolean skip = (Double.isNaN(x) || Double.isNaN(y)); - Node symbol = item.getNode(); - if (symbol != null) { - final double w = symbol.prefWidth(-1); - final double h = symbol.prefHeight(-1); - if (skip) { - symbol.resizeRelocate(-w * 2, -h * 2, w, h); - } else { - symbol.resizeRelocate(x - (w / 2), y - (h / 2), w, h); - } - } - if (skip) - continue; - if (x < dataXMin || y < dataYMin) { - if (prevDataPoint == null) { - prevDataPoint = new LineTo(x, y); - } else if ((sortX && prevDataPoint.getX() <= x) || (sortY && prevDataPoint.getY() <= y)) { - prevDataPoint.setX(x); - prevDataPoint.setY(y); - } - } else if (x <= dataXMax && y <= dataYMax) { - constructedPath.add(new LineTo(x, y)); - } else { - if (nextDataPoint == null) { - nextDataPoint = new LineTo(x, y); - } else if ((sortX && x <= nextDataPoint.getX()) || (sortY && y <= nextDataPoint.getY())) { - nextDataPoint.setX(x); - nextDataPoint.setY(y); + if (yDataPoints != null && !yDataPoints.isEmpty() + && !(yDataPoints.size() == 1 && axis.toNumericValue(yDataPoints.get(0)) == 0)) { + axis.invalidateRange(yDataPoints); } } } - - if (!constructedPath.isEmpty() || prevDataPoint != null || nextDataPoint != null) { - if (sortX) { - Collections.sort(constructedPath, (e1, e2) -> Double.compare(e1.getX(), e2.getX())); - } else if (sortY) { - Collections.sort(constructedPath, (e1, e2) -> Double.compare(e1.getY(), e2.getY())); - } else { - // assert prevDataPoint == null && nextDataPoint == null - } - if (prevDataPoint != null) { - constructedPath.add(0, prevDataPoint); - } - if (nextDataPoint != null) { - constructedPath.add(nextDataPoint); - } - - // assert !constructedPath.isEmpty() - LineTo first = constructedPath.get(0); - LineTo last = constructedPath.get(constructedPath.size() - 1); - - final double displayYPos = first.getY(); - - ObservableList lineElements = linePath.getElements(); - lineElements.clear(); - lineElements.add(new MoveTo(first.getX(), displayYPos)); - lineElements.addAll(constructedPath); - - if (fillPath != null) { - ObservableList fillElements = fillPath.getElements(); - fillElements.clear(); - double yOrigin = yAxis.getDisplayPosition(yAxis.toRealValue(0.0)); - - fillElements.add(new MoveTo(first.getX(), yOrigin)); - fillElements.addAll(constructedPath); - fillElements.add(new LineTo(last.getX(), yOrigin)); - fillElements.add(new ClosePath()); - } - } } - - public void setSeriesVisibility(TypedSeries series, boolean b) { - toggleSeriesVisability(series.getSeries(),b); - - LegendItem lItem = legendMap.get(series.getSeries()); - - //Get the legends and update it accordingly - Node symbol = lItem.getSymbol(); - ObservableList cssClass = symbol.getStyleClass(); - //TODO duplicated code. - if(cssClass.contains("hide-series")) { - symbol.setEffect(null); - symbol.getStyleClass().remove("hide-series"); - toggleSeriesVisability(series.getSeries(),true); - }else { - symbol.getStyleClass().add("hide-series"); - ColorAdjust colorAdjust = new ColorAdjust(); - colorAdjust.setSaturation(-0.5); - symbol.setEffect(colorAdjust); - toggleSeriesVisability(series.getSeries(),false); - } - - } - - protected void toggleSeriesVisability(Series series, boolean b) { - seriesVisibility.put(series,b); - this.requestChartLayout(); - } - - /** - * - * @param series - * @return true if the series is displayed false if it is hidden - */ - public boolean isSeriesVisible(TypedSeries series) { - return seriesVisibility.get(series.getSeries()); - } - @Override protected void updateLegend() { legendMap = new LinkedHashMap<>(); @@ -1124,23 +1364,23 @@ protected void updateLegend() { "chart-symbol"); // This css class contains background-color of the chart. Just go ahead and use // it LegendItem lItem = new LegendItem(name, symbol); - + lItem.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler() { @Override public void handle(MouseEvent event) { - + ObservableList cssClass = symbol.getStyleClass(); - - if(cssClass.contains("hide-series")) { + + if (cssClass.contains("hide-series")) { symbol.setEffect(null); symbol.getStyleClass().remove("hide-series"); - toggleSeriesVisability(series,true); - }else { + toggleSeriesVisability(series, true); + } else { symbol.getStyleClass().add("hide-series"); ColorAdjust colorAdjust = new ColorAdjust(); colorAdjust.setSaturation(-0.5); symbol.setEffect(colorAdjust); - toggleSeriesVisability(series,false); + toggleSeriesVisability(series, false); } } }); @@ -1164,83 +1404,33 @@ public void handle(MouseEvent event) { * */ - // public double - - /** - * Get the JavaFX X coordinate of the item - * - * @param item - * @return - */ - public X getCoordinate(Data item) { - return this.getCurrentDisplayedXValue(item); + // Bar chart specific + // TODO + private double getCategoryGap() { + return 10; } - /** - * Get the JavaFX Y coordinate of the item - * - * @param item - * @return - */ - public Y getYCoordinate(Data item) { - return this.getCurrentDisplayedYValue(item); + private double getBarGap() { + return 4; } - /* - * Value markers - * - * 3 ways we can implement value markers. 1. Either we create a separate series - * 2. Draw a line on top using stackpanes 3. draw on the plot area emulating - * grid line approach - * - * 1. Is a bit more tricky if we want seperate colors and custom labels - * therefore le - */ - - public void addValueMarker(ValueMarker markerToAdd) { - markerLock.lock(); - Path p = new Path(); - valueMarkers.put(markerToAdd, p); - - ChangeListener cl = (obs, oldValue, newValue) -> { - if (newValue) { - getChartChildren().add(((ValueMarker) obs).getLabel()); - } else { - getChartChildren().remove(((ValueMarker) obs).getLabel()); - } - requestChartLayout(); - }; - - valueMarkerLabelMap.put(markerToAdd, cl); - markerToAdd.enableLabelProperty().addListener(cl); - - // Add it to the chart - getChartChildren().add(p); - - if (markerToAdd.enableLabelProperty().get()) { - getChartChildren().add(markerToAdd.getLabel()); - } - - markerLock.unlock(); + private Data getDataItem(Series series, String category) { + Map> catmap = seriesCategoryMap.get(series); + return (catmap != null) ? catmap.get(category) : null; } - public void removeValueMarker(ValueMarker markerToRemove) { - markerLock.lock(); - Path p = valueMarkers.remove(markerToRemove); - if (p != null) { - getChartChildren().remove(p); - } - - // We also have to remove the listener or else we leake memory - ChangeListener l = valueMarkerLabelMap.remove(markerToRemove); - if (l != null) { - markerToRemove.enableLabelProperty().removeListener(l); + private Node createBar(Series series, int seriesIndex, final Data item, int itemIndex) { + Node bar = item.getNode(); + if (bar == null) { + bar = new StackPane(); + bar.setAccessibleRole(AccessibleRole.TEXT); + bar.setAccessibleRoleDescription("Bar"); + bar.focusTraversableProperty().bind(Platform.accessibilityActiveProperty()); + item.setNode(bar); } - - markerLock.unlock(); + bar.getStyleClass().setAll("chart-bar", "series" + seriesIndex, "data" + itemIndex, + "default-color" + seriesColorMap.get(series)); + return bar; } - - - } diff --git a/src/main/java/com/github/kilianB/TypedSeries.java b/src/main/java/com/github/kilianB/TypedSeries.java index 18a12d9..9d97ecf 100644 --- a/src/main/java/com/github/kilianB/TypedSeries.java +++ b/src/main/java/com/github/kilianB/TypedSeries.java @@ -121,15 +121,16 @@ public static Builder builder(Series series) { public interface ITypeStage{ - IAxisStage line(); + ILineAxisStage line(); + IBarAxisStage bar(); IAxisStage scatter(); IAxisStage area(); + } public interface ILineAxisStage{ //Only line chart options - ILineAxisStage withXAxisSortingPolicy(SortingPolicy sortingPolicy); - + IAxisStage withXAxisSortingPolicy(SortingPolicy sortingPolicy); IAxisStage withXAxisIndex(int xAxisIndex); IAxisStage withXAxisSide(Side side); IAxisStage withYAxisIndex(int yAxisIndex); @@ -137,6 +138,12 @@ public interface ILineAxisStage{ TypedSeries build(); } + public interface IBarAxisStage{ + //Which axis is the categorical axis? + IBarAxisStage withDomainAxisIndex(int index); + IBarAxisStage withRangeAxisIndex(int index ); + } + public interface IAxisStage{ IAxisStage withXAxisIndex(int xAxisIndex); @@ -147,7 +154,7 @@ public interface IAxisStage{ TypedSeries build(); } - public static final class Builder implements ITypeStage,ILineAxisStage,IAxisStage{ + public static final class Builder implements ITypeStage,ILineAxisStage,IAxisStage,IBarAxisStage{ private Series series; private ReadOnlyObjectWrapper seriesType; @@ -168,7 +175,7 @@ private Builder(Series series) { } @Override - public IAxisStage line() { + public ILineAxisStage line() { seriesType = new ReadOnlyObjectWrapper(SeriesType.LINE); return this; } @@ -186,7 +193,13 @@ public IAxisStage area() { } @Override - public ILineAxisStage withXAxisSortingPolicy(SortingPolicy sortingPolicy) { + public IBarAxisStage bar() { + seriesType = new ReadOnlyObjectWrapper(SeriesType.BAR); + return this; + } + + @Override + public IAxisStage withXAxisSortingPolicy(SortingPolicy sortingPolicy) { xAxisSortingPolicy = sortingPolicy; return this; } @@ -196,6 +209,18 @@ public IAxisStage withXAxisIndex(int xAxisIndex) { this.xAxisIndex = xAxisIndex; return this; } + + @Override + public IBarAxisStage withDomainAxisIndex(int index) { + // TODO Auto-generated method stub + return null; + } + + @Override + public IBarAxisStage withRangeAxisIndex(int index) { + // TODO Auto-generated method stub + return null; + } @Override public IAxisStage withXAxisSide(Side side) { @@ -234,9 +259,8 @@ public TypedSeries build() { return new TypedSeries(series,seriesType,yAxisIndex,xAxisIndex,xAxisSortingPolicy, xAxisSide,yAxisSide); } - - + } } \ No newline at end of file diff --git a/src/main/java/com/github/kilianB/ValueMarker.java b/src/main/java/com/github/kilianB/ValueMarker.java index ba8241e..b3fd480 100644 --- a/src/main/java/com/github/kilianB/ValueMarker.java +++ b/src/main/java/com/github/kilianB/ValueMarker.java @@ -6,6 +6,12 @@ import javafx.scene.control.Label; import javafx.scene.paint.Color; +/** + * A value marker represents a horizontal or vertical line overlaid over the chart. + * @author Kilian + * + * @param type of the axis + */ public class ValueMarker{ // public enum Direction{ diff --git a/src/main/java/com/github/kilianB/renderer/AreaRenderer.java b/src/main/java/com/github/kilianB/renderer/AreaRenderer.java new file mode 100644 index 0000000..6461628 --- /dev/null +++ b/src/main/java/com/github/kilianB/renderer/AreaRenderer.java @@ -0,0 +1,57 @@ +package com.github.kilianB.renderer; + +import java.util.Iterator; + +import javafx.collections.ObservableList; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.chart.Axis; +import javafx.scene.chart.LineChart.SortingPolicy; +import javafx.scene.chart.XYChart.Data; +import javafx.scene.chart.XYChart.Series; +import javafx.scene.shape.Path; + +/** + * @author Kilian + * + */ +public class AreaRenderer implements Renderer { + + @Override + public void layout(Series series, Axis xAxis, Axis yAxis, boolean visible) { + final ObservableList children = ((Group) series.getNode()).getChildren(); + + Path fillPath = (Path) children.get(0); + Path linePath = (Path) children.get(1); + + var cssPath = fillPath.getStyleClass(); + var cssLine = linePath.getStyleClass(); + + if (!visible) { + if (!cssPath.contains("hide-series-symbol")) { + cssPath.add("hide-series-symbol"); + cssLine.add("hide-series-symbol"); + + // Also add the hide artifact to the symbols + for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) { + Node symbol = it.next().getNode(); + symbol.getStyleClass().add("hide-series-symbol"); + } + } + } else { + if (cssPath.contains("hide-series-symbol")) { + cssPath.remove("hide-series-symbol"); + cssLine.add("hide-series-symbol"); + // Also add the hide artifact to the symbols + for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) { + Node symbol = it.next().getNode(); + symbol.getStyleClass().remove("hide-series-symbol"); + } + } + makePaths(series, constructedPath, fillPath, linePath, SortingPolicy.X_AXIS, xAxis, yAxis); + } + } + + + +} diff --git a/src/main/java/com/github/kilianB/renderer/LineRenderer.java b/src/main/java/com/github/kilianB/renderer/LineRenderer.java new file mode 100644 index 0000000..f6ff6b9 --- /dev/null +++ b/src/main/java/com/github/kilianB/renderer/LineRenderer.java @@ -0,0 +1,18 @@ +package com.github.kilianB.renderer; + +import javafx.scene.chart.Axis; +import javafx.scene.chart.XYChart.Series; + +/** + * @author Kilian + * + */ +public class LineRenderer implements Renderer{ + + @Override + public void layout(Series series, Axis xAxis, Axis yAxis, boolean visible) { + // TODO Auto-generated method stub + + } + +} diff --git a/src/main/java/com/github/kilianB/renderer/Renderer.java b/src/main/java/com/github/kilianB/renderer/Renderer.java new file mode 100644 index 0000000..4333274 --- /dev/null +++ b/src/main/java/com/github/kilianB/renderer/Renderer.java @@ -0,0 +1,14 @@ +package com.github.kilianB.renderer; + +import javafx.scene.chart.Axis; +import javafx.scene.chart.XYChart.Series; + +/** + * @author Kilian + * + */ +public interface Renderer { + + void layout(Series series, Axis xAxis, Axis yAxis, boolean visible); + +} diff --git a/src/main/java/com/github/kilianB/utility/ColorUtil.java b/src/main/java/com/github/kilianB/utility/ColorUtil.java deleted file mode 100644 index 647f46f..0000000 --- a/src/main/java/com/github/kilianB/utility/ColorUtil.java +++ /dev/null @@ -1,161 +0,0 @@ -package com.github.kilianB.utility; - -import javafx.scene.paint.Color; - -public class ColorUtil { - - /** - * Return either white or black depending on the supplied color to guarantee - * readability if the contrast color is used as text overlay ontop of the input - * color. - * - * @param input - * @return - */ - public static Color getContrastColor(Color input) { - // Luminascense - double y = 0.299 * input.getRed() + 0.587 * input.getGreen() + 0.114 * input.getBlue(); - if (y > 0.55) { - return Color.BLACK; - } else { - return Color.WHITE; - } - } - - /** - * Create color palettes - * - * @author Kilian - * - */ - public static class ColorPalette { - - public static Color[] getPallette(int numColors) { - return getPallette(numColors, Color.web("#003f5c"), Color.web("#ffa600")); - } - - public static Color[] getPallette(int numColors, Color startColor, Color endColor) { - - Color[] cols = new Color[numColors]; - for (int i = 0; i < numColors; i++) { - double factor = i / (double) numColors; - cols[i] = startColor.interpolate(endColor, factor); - } - return cols; - } - - public static Color[] getPalletteHue(int numColors) { - return getPalletteHue(numColors, Color.web("#003f5c"), Color.web("#ffa600")); - } - - public static Color[] getPalletteHue(int numColors, Color startColor, Color endColor) { - - double hDelta = (endColor.getHue() - startColor.getHue()) / numColors; - double sDelta = (endColor.getSaturation() - startColor.getSaturation()) / numColors; - double bDelta = (endColor.getBrightness() - startColor.getBrightness()) / numColors; - - Color[] cols = new Color[numColors]; - for (int i = 0; i < numColors; i++) { - - double newSat = startColor.getSaturation() + sDelta * i; - double newBrightness = startColor.getBrightness() + bDelta * i; - - // Wrap around - if (newSat > 1) { - newSat = MathUtil.getFractionalPart(newSat); - } else if (newSat < 0) { - newSat = 1 - newSat; - } - - if (newBrightness > 1) { - newBrightness = MathUtil.getFractionalPart(newBrightness); - } else if (newBrightness < 0) { - newBrightness = 1 - newBrightness; - } - - cols[i] = Color.hsb(startColor.getHue() + hDelta * i, newSat, newBrightness); - } - return cols; - } - } - - public static java.awt.Color fxToAwtColor(Color fxColor) { - return new java.awt.Color((float) fxColor.getRed(), (float) fxColor.getGreen(), (float) fxColor.getBlue(), - (float) fxColor.getOpacity()); - } - - public static Color awtToFxColor(java.awt.Color awtColor) { - return new Color(awtColor.getRed() / 255d, awtColor.getGreen() / 255d, awtColor.getBlue() / 255d, - awtColor.getAlpha() / 255d); - } - - /** - * Convert an argb value to it's individual components in range of 0 - 255 - * - * @param argb - * values as int - * @return [0] Alpha, [1] Red, [2] Green, [3] Blue - * - */ - public static int[] argbToComponents(int argb) { - return new int[] { argb >> 24 & 0xFF, argb >> 16 & 0xFF, argb >> 8 & 0xFF, argb & 0xFF }; - } - - /** - * Converts the components to a single int argb representation. The individual - * values are not range checked - * - * @param alpha - * in range of 0 - 255 - * @param red - * in range of 0 - 255 - * @param green - * in range of 0 - 255 - * @param blue - * in range of 0 - 255 - * @return a single int representing the argb value - */ - public static int componentsToARGB(int alpha, int red, int green, int blue) { - return (alpha << 24) | (red << 16) | (green << 8) | blue; - } - - public static javafx.scene.paint.Color argbToFXColor(int argb) { - - int[] components = argbToComponents(argb); - - return new javafx.scene.paint.Color(components[1] / 255d, components[2] / 255d, components[3] / 255d, - components[0] / 255d); - } - - public static String fxToHex(Color color) { - return String.format("#%02X%02X%02X", (int) (color.getRed() * 255), (int) (color.getGreen() * 255), - (int) (color.getBlue() * 255)); - } - - // https://stackoverflow.com/a/2103608/3244464 - // https://www.compuphase.com/cmetric.htm - public static double distance(Color c1, Color c2) { - double rmean = (c1.getRed() * 255 + c2.getRed() * 255) / 2; - int r = (int) (c1.getRed() * 255 - c2.getRed() * 255); - int g = (int) (c1.getGreen() * 255 - c2.getGreen() * 255); - int b = (int) (c1.getBlue() * 255 - c2.getBlue() * 255); - double weightR = 2 + rmean / 256; - double weightG = 4.0; - double weightB = 2 + (255 - rmean) / 256; - return Math.sqrt(weightR * r * r + weightG * g * g + weightB * b * b); - } - - // https://stackoverflow.com/a/2103608/3244464 - // https://www.compuphase.com/cmetric.htm - public static double distance(java.awt.Color c1, java.awt.Color c2) { - double rmean = (c1.getRed() + c2.getRed()) / 2; - int r = (int) (c1.getRed() - c2.getRed()); - int g = (int) (c1.getGreen() - c2.getGreen()); - int b = (int) (c1.getBlue() - c2.getBlue()); - double weightR = 2 + rmean / 256; - double weightG = 4.0; - double weightB = 2 + (255 - rmean) / 256; - return Math.sqrt(weightR * r * r + weightG * g * g + weightB * b * b); - } - -} diff --git a/src/main/java/com/github/kilianB/utility/MathUtil.java b/src/main/java/com/github/kilianB/utility/MathUtil.java deleted file mode 100644 index f1a3725..0000000 --- a/src/main/java/com/github/kilianB/utility/MathUtil.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.github.kilianB.utility; - -public class MathUtil { - - - - /** - * Clamp a number between its lower and upper bound. - * If x > upper bound return upper bound. - * If x < lower bound return lower bound - * - * @param value - * @param lowerBound - * @param upperBound - * @return - */ - public static > T clampNumber(T value,T lowerBound,T upperBound) { - if(value.compareTo(lowerBound) <= 0) { - return lowerBound; - }else if(value.compareTo(upperBound) >= 0) { - return upperBound; - } - return value; - } - - /** - * Find the nearest integer which will divide the given number. - * number%x == 0 - * @param dividend - * @param divisor - * @return - */ - public static long findClosestDivisibleInteger(long dividend, long divisor) { - long lowerBound = dividend - (dividend % divisor); - long upperBound = (dividend + divisor) - (dividend % divisor); - if (dividend - lowerBound > upperBound - dividend) { - return upperBound; - } else { - return lowerBound; - } - } - - /** - * Return the fractional part of the number - * @param d a double - * @return the fractional part of the number - */ - public static double getFractionalPart(double d) { - return d - (int)d; - } - - public static boolean isDoubleEquals(double needle, double target, double epsilon) { - return Math.abs(needle - target) < epsilon; - - //We could use mashine precision e.g. Math.ulp(d) - - } - - /** - * - * @param gaussian A gaussian with std 1 and mean 0 - * @param newStd new standard deviation - * @param newMean new mean - * @return - */ - public static int fitGaussian(int gaussian, int newStd, int newMean) { - return gaussian * newStd + newMean; - } - - public static double fitGaussian(double gaussian, double newStd, double newMean) { - return gaussian * newStd + newMean; - } - - /** - * Checks if the supplied argument is a positive non null numeric value and - * throws a IllegalArgumentException if it isn't - * @param value to be checked - * @return The supplied value - */ - public static T requirePositiveValue(T value) { - return requirePositiveValue(value,""); - } - - /** - * Checks if the supplied argument is a positive non null numeric value and - * throws a IllegalArgumentException if it isn't - * @param value to be checked - * @param message to be thrown in case of error - * @return The supplied value - */ - public static T requirePositiveValue(T value,String message) { - if(value.doubleValue() <= 0) { - throw new IllegalArgumentException(message); - } - return value; - } - - /** - * Checks if the supplied argument is lays within the given bounds - * throws a IllegalArgumentException if it doesn't - * @param value to be checked - * @param lowerBound inclusively - * @param higherBound inclusively - * @param message to be thrown in case of error - * @return The supplied value - */ - public static T requireInRange(T value, T lowerBound, T higherBound, String message) { - if(value.doubleValue() < lowerBound.doubleValue() && value.doubleValue() > higherBound.doubleValue()) { - throw new IllegalArgumentException(message); - } - return value; - } - - /** - * Calculate the number of characters needed to present the integer part of a number - *
-	 * 1234     -> 4
-	 * 12345    -> 5
-	 * 12345.12 -> 5
-	 * 
- * @param n A number - * @return the character count of the integer part of the number - */ - public static int charsNeeded(Number n) { - - double numberAsDouble = n.doubleValue(); - int negative = (numberAsDouble < 0 ? 1 : 0); - if(isDoubleEquals(numberAsDouble,0d,1e-5)) { - return 1 + negative; - } - return (int) Math.floor(Math.log10(Math.abs(numberAsDouble))+1) + negative; - } -} \ No newline at end of file