Created
January 3, 2019 10:06
-
-
Save sirolf2009/ae8a7897b57dcf902b4ed747b05641f9 to your computer and use it in GitHub Desktop.
JavaFX LineChart with gaps, use Double.NaN for gaps.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.ArrayList; | |
import java.util.Iterator; | |
import java.util.List; | |
import javafx.collections.ObservableList; | |
import javafx.scene.Node; | |
import javafx.scene.chart.Axis; | |
import javafx.scene.chart.LineChart; | |
import javafx.scene.shape.LineTo; | |
import javafx.scene.shape.MoveTo; | |
import javafx.scene.shape.Path; | |
import javafx.scene.shape.PathElement; | |
public class GapLineChart<X,Y> extends LineChart<X, Y> { | |
public GapLineChart(Axis<X> xAxis, Axis<Y> yAxis) { | |
super(xAxis, yAxis); | |
} | |
public GapLineChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X, Y>> data) { | |
super(xAxis, yAxis, data); | |
} | |
@Override | |
protected void layoutPlotChildren() { | |
List<PathElement> constructedPath = new ArrayList(getData().size()); | |
for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { | |
Series<X, Y> series = getData().get(seriesIndex); | |
if(series.getNode() instanceof Path) { | |
ObservableList<PathElement> seriesLine = ((Path) series.getNode()).getElements(); | |
seriesLine.clear(); | |
constructedPath.clear(); | |
for (Iterator<Data<X, Y>> it = getDisplayedDataIterator(series); it.hasNext();) { | |
Data<X, Y> item = it.next(); | |
double x = getXAxis().getDisplayPosition(item.getXValue()); | |
double y = getYAxis().getDisplayPosition(getYAxis().toRealValue(getYAxis().toNumericValue(item.getYValue()))); | |
if(Double.isNaN(x) || Double.isNaN(y)) { | |
Data<X,Y> next = series.getData().get(series.getData().indexOf(item)+1); | |
double nextX = getXAxis().getDisplayPosition(next.getXValue()); | |
double nextY = getYAxis().getDisplayPosition(getYAxis().toRealValue(getYAxis().toNumericValue(next.getYValue()))); | |
constructedPath.add(new MoveTo(nextX, nextY)); | |
} else { | |
constructedPath.add(new LineTo(x, y)); | |
Node symbol = item.getNode(); | |
if(symbol != null) { | |
double w = symbol.prefWidth(-1); | |
double h = symbol.prefHeight(-1); | |
symbol.resizeRelocate(x - (w / 2), y - (h / 2), w, h); | |
} | |
} | |
} | |
if(!constructedPath.isEmpty()) { | |
PathElement first = constructedPath.get(0); | |
seriesLine.add(new MoveTo(getX(first), getY(first))); | |
seriesLine.addAll(constructedPath); | |
} | |
} | |
} | |
} | |
public double getX(PathElement element) { | |
if(element instanceof LineTo) { | |
return getX((LineTo) element); | |
} else if(element instanceof MoveTo) { | |
return getX((MoveTo) element); | |
} else { | |
throw new IllegalArgumentException(element+" is not a valid type"); | |
} | |
} | |
public double getX(LineTo element) { | |
return element.getX(); | |
} | |
public double getX(MoveTo element) { | |
return element.getX(); | |
} | |
public double getY(PathElement element) { | |
if(element instanceof LineTo) { | |
return getY((LineTo) element); | |
} else if(element instanceof MoveTo) { | |
return getY((MoveTo) element); | |
} else { | |
throw new IllegalArgumentException(element+" is not a valid type"); | |
} | |
} | |
public double getY(LineTo element) { | |
return element.getY(); | |
} | |
public double getY(MoveTo element) { | |
return element.getY(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import static java.util.stream.Collectors.toList; | |
import com.sun.javafx.css.converters.SizeConverter; | |
import java.text.DecimalFormat; | |
import java.text.ParseException; | |
import java.util.ArrayList; | |
import java.util.Collections; | |
import java.util.List; | |
import javafx.beans.property.BooleanProperty; | |
import javafx.beans.property.BooleanPropertyBase; | |
import javafx.beans.property.DoubleProperty; | |
import javafx.beans.property.SimpleStringProperty; | |
import javafx.beans.property.StringProperty; | |
import javafx.beans.value.ChangeListener; | |
import javafx.beans.value.WritableValue; | |
import javafx.css.CssMetaData; | |
import javafx.css.Styleable; | |
import javafx.css.StyleableDoubleProperty; | |
import javafx.css.StyleableProperty; | |
import javafx.geometry.Dimension2D; | |
import javafx.geometry.Side; | |
import javafx.scene.chart.ValueAxis; | |
import javafx.util.StringConverter; | |
/** | |
* A axis class that plots a range of numbers with major tick marks every "tickUnit". You can use any Number type with | |
* this axis, Long, Double, BigDecimal etc. | |
* | |
* @since JavaFX 2.0 | |
*/ | |
public final class GapNumberAxis extends ValueAxis<Number> { | |
private final StringProperty currentFormatterProperty = new SimpleStringProperty(this, "currentFormatter", ""); | |
private final DefaultFormatter defaultFormatter = new DefaultFormatter(this); | |
// -------------- PUBLIC PROPERTIES -------------------------------------------------------------------------------- | |
/** When true zero is always included in the visible range. This only has effect if auto-ranging is on. */ | |
private BooleanProperty forceZeroInRange = new BooleanPropertyBase(true) { | |
@Override | |
protected void invalidated() { | |
// This will effect layout if we are auto ranging | |
if(isAutoRanging()) { | |
requestAxisLayout(); | |
invalidateRange(); | |
} | |
} | |
@Override | |
public Object getBean() { | |
return GapNumberAxis.this; | |
} | |
@Override | |
public String getName() { | |
return "forceZeroInRange"; | |
} | |
}; | |
public final boolean isForceZeroInRange() { | |
return forceZeroInRange.getValue(); | |
} | |
public final void setForceZeroInRange(boolean value) { | |
forceZeroInRange.setValue(value); | |
} | |
public final BooleanProperty forceZeroInRangeProperty() { | |
return forceZeroInRange; | |
} | |
/** The value between each major tick mark in data units. This is automatically set if we are auto-ranging. */ | |
private DoubleProperty tickUnit = new StyleableDoubleProperty(5) { | |
@Override | |
protected void invalidated() { | |
if(!isAutoRanging()) { | |
invalidateRange(); | |
requestAxisLayout(); | |
} | |
} | |
@Override | |
public CssMetaData<GapNumberAxis, Number> getCssMetaData() { | |
return StyleableProperties.TICK_UNIT; | |
} | |
@Override | |
public Object getBean() { | |
return GapNumberAxis.this; | |
} | |
@Override | |
public String getName() { | |
return "tickUnit"; | |
} | |
}; | |
public final double getTickUnit() { | |
return tickUnit.get(); | |
} | |
public final void setTickUnit(double value) { | |
tickUnit.set(value); | |
} | |
public final DoubleProperty tickUnitProperty() { | |
return tickUnit; | |
} | |
// -------------- CONSTRUCTORS ------------------------------------------------------------------------------------- | |
/** | |
* Create a auto-ranging NumberAxis | |
*/ | |
public GapNumberAxis() { | |
} | |
/** | |
* Create a non-auto-ranging NumberAxis with the given upper bound, lower bound and tick unit | |
* | |
* @param lowerBound | |
* The lower bound for this axis, ie min plottable value | |
* @param upperBound | |
* The upper bound for this axis, ie max plottable value | |
* @param tickUnit | |
* The tick unit, ie space between tickmarks | |
*/ | |
public GapNumberAxis(double lowerBound, double upperBound, double tickUnit) { | |
super(lowerBound, upperBound); | |
setTickUnit(tickUnit); | |
} | |
/** | |
* Create a non-auto-ranging NumberAxis with the given upper bound, lower bound and tick unit | |
* | |
* @param axisLabel | |
* The name to display for this axis | |
* @param lowerBound | |
* The lower bound for this axis, ie min plottable value | |
* @param upperBound | |
* The upper bound for this axis, ie max plottable value | |
* @param tickUnit | |
* The tick unit, ie space between tickmarks | |
*/ | |
public GapNumberAxis(String axisLabel, double lowerBound, double upperBound, double tickUnit) { | |
super(lowerBound, upperBound); | |
setTickUnit(tickUnit); | |
setLabel(axisLabel); | |
} | |
@Override | |
public void invalidateRange(List<Number> data) { | |
List<Number> realData = data.stream().filter(number -> number != null && !Double.isNaN(number.doubleValue())).collect(toList()); | |
super.invalidateRange(realData); | |
} | |
// -------------- PROTECTED METHODS -------------------------------------------------------------------------------- | |
/** | |
* Get the string label name for a tick mark with the given value | |
* | |
* @param value | |
* The value to format into a tick label string | |
* @return A formatted string for the given value | |
*/ | |
@Override | |
protected String getTickMarkLabel(Number value) { | |
StringConverter<Number> formatter = getTickLabelFormatter(); | |
if(formatter == null) | |
formatter = defaultFormatter; | |
return formatter.toString(value); | |
} | |
/** | |
* Called to get the current axis range. | |
* | |
* @return A range object that can be passed to setRange() and calculateTickValues() | |
*/ | |
@Override | |
protected Object getRange() { | |
return new Object[] { | |
getLowerBound(), | |
getUpperBound(), | |
getTickUnit(), | |
getScale(), | |
currentFormatterProperty.get() | |
}; | |
} | |
/** | |
* Called to set the current axis range to the given range. If isAnimating() is true then this method should | |
* animate the range to the new range. | |
* | |
* @param range | |
* A range object returned from autoRange() | |
* @param animate | |
* If true animate the change in range | |
*/ | |
@Override | |
protected void setRange(Object range, boolean animate) { | |
final Object[] rangeProps = (Object[]) range; | |
final double lowerBound = (Double) rangeProps[0]; | |
final double upperBound = (Double) rangeProps[1]; | |
final double tickUnit = (Double) rangeProps[2]; | |
final double scale = (Double) rangeProps[3]; | |
final String formatter = (String) rangeProps[4]; | |
currentFormatterProperty.set(formatter); | |
setLowerBound(lowerBound); | |
setUpperBound(upperBound); | |
setTickUnit(tickUnit); | |
currentLowerBound.set(lowerBound); | |
setScale(scale); | |
} | |
/** | |
* Calculate a list of all the data values for each tick mark in range | |
* | |
* @param length | |
* The length of the axis in display units | |
* @param range | |
* A range object returned from autoRange() | |
* @return A list of tick marks that fit along the axis if it was the given length | |
*/ | |
@Override | |
protected List<Number> calculateTickValues(double length, Object range) { | |
final Object[] rangeProps = (Object[]) range; | |
final double lowerBound = (Double) rangeProps[0]; | |
final double upperBound = (Double) rangeProps[1]; | |
final double tickUnit = (Double) rangeProps[2]; | |
List<Number> tickValues = new ArrayList<>(); | |
if(lowerBound == upperBound) { | |
tickValues.add(lowerBound); | |
} else if(tickUnit <= 0) { | |
tickValues.add(lowerBound); | |
tickValues.add(upperBound); | |
} else if(tickUnit > 0) { | |
tickValues.add(lowerBound); | |
if(((upperBound - lowerBound) / tickUnit) > 2000) { | |
// This is a ridiculous amount of major tick marks, something has probably gone wrong | |
System.err.println("Warning we tried to create more than 2000 major tick marks on a NumberAxis. " + | |
"Lower Bound=" + lowerBound + ", Upper Bound=" + upperBound + ", Tick Unit=" + tickUnit); | |
} else { | |
if(lowerBound + tickUnit < upperBound) { | |
// If tickUnit is integer, start with the nearest integer | |
double major = Math.rint(tickUnit) == tickUnit ? Math.ceil(lowerBound) : lowerBound + tickUnit; | |
int count = (int) Math.ceil((upperBound - major) / tickUnit); | |
for(int i = 0; major < upperBound && i < count; major += tickUnit, i++) { | |
if(!tickValues.contains(major)) { | |
tickValues.add(major); | |
} | |
} | |
} | |
} | |
tickValues.add(upperBound); | |
} | |
return tickValues; | |
} | |
/** | |
* Calculate a list of the data values for every minor tick mark | |
* | |
* @return List of data values where to draw minor tick marks | |
*/ | |
@Override | |
protected List<Number> calculateMinorTickMarks() { | |
final List<Number> minorTickMarks = new ArrayList<>(); | |
final double lowerBound = getLowerBound(); | |
final double upperBound = getUpperBound(); | |
final double tickUnit = getTickUnit(); | |
final double minorUnit = tickUnit / Math.max(1, getMinorTickCount()); | |
if(tickUnit > 0) { | |
if(((upperBound - lowerBound) / minorUnit) > 10000) { | |
// This is a ridiculous amount of major tick marks, something has probably gone wrong | |
System.err.println("Warning we tried to create more than 10000 minor tick marks on a NumberAxis. " + | |
"Lower Bound=" + getLowerBound() + ", Upper Bound=" + getUpperBound() + ", Tick Unit=" + tickUnit); | |
return minorTickMarks; | |
} | |
final boolean tickUnitIsInteger = Math.rint(tickUnit) == tickUnit; | |
if(tickUnitIsInteger) { | |
double minor = Math.floor(lowerBound) + minorUnit; | |
int count = (int) Math.ceil((Math.ceil(lowerBound) - minor) / minorUnit); | |
for(int i = 0; minor < Math.ceil(lowerBound) && i < count; minor += minorUnit, i++) { | |
if(minor > lowerBound) { | |
minorTickMarks.add(minor); | |
} | |
} | |
} | |
double major = tickUnitIsInteger ? Math.ceil(lowerBound) : lowerBound; | |
int count = (int) Math.ceil((upperBound - major) / tickUnit); | |
for(int i = 0; major < upperBound && i < count; major += tickUnit, i++) { | |
final double next = Math.min(major + tickUnit, upperBound); | |
double minor = major + minorUnit; | |
int minorCount = (int) Math.ceil((next - minor) / minorUnit); | |
for(int j = 0; minor < next && j < minorCount; minor += minorUnit, j++) { | |
minorTickMarks.add(minor); | |
} | |
} | |
} | |
return minorTickMarks; | |
} | |
/** | |
* Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks | |
* | |
* @param value | |
* tick mark value | |
* @param range | |
* range to use during calculations | |
* @return size of tick mark label for given value | |
*/ | |
@Override | |
protected Dimension2D measureTickMarkSize(Number value, Object range) { | |
final Object[] rangeProps = (Object[]) range; | |
final String formatter = (String) rangeProps[4]; | |
return measureTickMarkSize(value, getTickLabelRotation(), formatter); | |
} | |
/** | |
* Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks | |
* | |
* @param value | |
* tick mark value | |
* @param rotation | |
* The text rotation | |
* @param numFormatter | |
* The number formatter | |
* @return size of tick mark label for given value | |
*/ | |
private Dimension2D measureTickMarkSize(Number value, double rotation, String numFormatter) { | |
String labelText; | |
StringConverter<Number> formatter = getTickLabelFormatter(); | |
if(formatter == null) | |
formatter = defaultFormatter; | |
if(formatter instanceof DefaultFormatter) { | |
labelText = ((DefaultFormatter) formatter).toString(value, numFormatter); | |
} else { | |
labelText = formatter.toString(value); | |
} | |
return measureTickMarkLabelSize(labelText, rotation); | |
} | |
/** | |
* Called to set the upper and lower bound and anything else that needs to be auto-ranged | |
* | |
* @param minValue | |
* The min data value that needs to be plotted on this axis | |
* @param maxValue | |
* The max data value that needs to be plotted on this axis | |
* @param length | |
* The length of the axis in display coordinates | |
* @param labelSize | |
* The approximate average size a label takes along the axis | |
* @return The calculated range | |
*/ | |
@Override | |
protected Object autoRange(double minValue, double maxValue, double length, double labelSize) { | |
final Side side = getSide(); | |
// check if we need to force zero into range | |
if(isForceZeroInRange()) { | |
if(maxValue < 0) { | |
maxValue = 0; | |
} else if(minValue > 0) { | |
minValue = 0; | |
} | |
} | |
// calculate the number of tick-marks we can fit in the given length | |
int numOfTickMarks = (int) Math.floor(length / labelSize); | |
// can never have less than 2 tick marks one for each end | |
numOfTickMarks = Math.max(numOfTickMarks, 2); | |
int minorTickCount = Math.max(getMinorTickCount(), 1); | |
double range = maxValue - minValue; | |
if(range != 0 && range / (numOfTickMarks * minorTickCount) <= Math.ulp(minValue)) { | |
range = 0; | |
} | |
// pad min and max by 2%, checking if the range is zero | |
final double paddedRange = (range == 0) | |
? minValue == 0 ? 2 : Math.abs(minValue) * 0.02 | |
: Math.abs(range) * 1.02; | |
final double padding = (paddedRange - range) / 2; | |
// if min and max are not zero then add padding to them | |
double paddedMin = minValue - padding; | |
double paddedMax = maxValue + padding; | |
// check padding has not pushed min or max over zero line | |
if((paddedMin < 0 && minValue >= 0) || (paddedMin > 0 && minValue <= 0)) { | |
// padding pushed min above or below zero so clamp to 0 | |
paddedMin = 0; | |
} | |
if((paddedMax < 0 && maxValue >= 0) || (paddedMax > 0 && maxValue <= 0)) { | |
// padding pushed min above or below zero so clamp to 0 | |
paddedMax = 0; | |
} | |
// calculate tick unit for the number of ticks can have in the given data range | |
double tickUnit = paddedRange / (double) numOfTickMarks; | |
// search for the best tick unit that fits | |
double tickUnitRounded = 0; | |
double minRounded = 0; | |
double maxRounded = 0; | |
int count = 0; | |
double reqLength = Double.MAX_VALUE; | |
String formatter = "0.00000000"; | |
// loop till we find a set of ticks that fit length and result in a total of less than 20 tick marks | |
while(reqLength > length || count > 20) { | |
int exp = (int) Math.floor(Math.log10(tickUnit)); | |
final double mant = tickUnit / Math.pow(10, exp); | |
double ratio = mant; | |
if(mant > 5d) { | |
exp++; | |
ratio = 1; | |
} else if(mant > 1d) { | |
ratio = mant > 2.5 ? 5 : 2.5; | |
} | |
if(exp > 1) { | |
formatter = "#,##0"; | |
} else if(exp == 1) { | |
formatter = "0"; | |
} else { | |
final boolean ratioHasFrac = Math.rint(ratio) != ratio; | |
final StringBuilder formatterB = new StringBuilder("0"); | |
int n = ratioHasFrac ? Math.abs(exp) + 1 : Math.abs(exp); | |
if(n > 0) | |
formatterB.append("."); | |
for(int i = 0; i < n; ++i) { | |
formatterB.append("0"); | |
} | |
formatter = formatterB.toString(); | |
} | |
tickUnitRounded = ratio * Math.pow(10, exp); | |
// move min and max to nearest tick mark | |
minRounded = Math.floor(paddedMin / tickUnitRounded) * tickUnitRounded; | |
maxRounded = Math.ceil(paddedMax / tickUnitRounded) * tickUnitRounded; | |
// calculate the required length to display the chosen tick marks for real, this will handle if there are | |
// huge numbers involved etc or special formatting of the tick mark label text | |
double maxReqTickGap = 0; | |
double last = 0; | |
count = (int) Math.ceil((maxRounded - minRounded) / tickUnitRounded); | |
double major = minRounded; | |
for(int i = 0; major <= maxRounded && i < count; major += tickUnitRounded, i++) { | |
Dimension2D markSize = measureTickMarkSize(major, getTickLabelRotation(), formatter); | |
double size = side.isVertical() ? markSize.getHeight() : markSize.getWidth(); | |
if(i == 0) { // first | |
last = size / 2; | |
} else { | |
maxReqTickGap = Math.max(maxReqTickGap, last + 6 + (size / 2)); | |
} | |
} | |
reqLength = (count - 1) * maxReqTickGap; | |
tickUnit = tickUnitRounded; | |
// fix for RT-35600 where a massive tick unit was being selected | |
// unnecessarily. There is probably a better solution, but this works | |
// well enough for now. | |
if(numOfTickMarks == 2 && reqLength > length) { | |
break; | |
} | |
if(reqLength > length || count > 20) | |
tickUnit *= 2; // This is just for the while loop, if there are still too many ticks | |
} | |
// calculate new scale | |
final double newScale = calculateNewScale(length, minRounded, maxRounded); | |
// return new range | |
return new Object[] { minRounded, maxRounded, tickUnitRounded, newScale, formatter }; | |
} | |
// -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------ | |
/** @treatAsPrivate implementation detail */ | |
private static class StyleableProperties { | |
private static final CssMetaData<GapNumberAxis, Number> TICK_UNIT = new CssMetaData<GapNumberAxis, Number>("-fx-tick-unit", | |
SizeConverter.getInstance(), 5.0) { | |
@Override | |
public boolean isSettable(GapNumberAxis n) { | |
return n.tickUnit == null || !n.tickUnit.isBound(); | |
} | |
@Override | |
public StyleableProperty<Number> getStyleableProperty(GapNumberAxis n) { | |
return (StyleableProperty<Number>) (WritableValue<Number>) n.tickUnitProperty(); | |
} | |
}; | |
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; | |
static { | |
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(ValueAxis.getClassCssMetaData()); | |
styleables.add(TICK_UNIT); | |
STYLEABLES = Collections.unmodifiableList(styleables); | |
} | |
} | |
/** | |
* @return The CssMetaData associated with this class, which may include the | |
* CssMetaData of its super classes. | |
* @since JavaFX 8.0 | |
*/ | |
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { | |
return StyleableProperties.STYLEABLES; | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @since JavaFX 8.0 | |
*/ | |
@Override | |
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { | |
return getClassCssMetaData(); | |
} | |
// -------------- INNER CLASSES ------------------------------------------------------------------------------------ | |
/** | |
* Default number formatter for NumberAxis, this stays in sync with auto-ranging and formats values appropriately. | |
* You can wrap this formatter to add prefixes or suffixes; | |
* | |
* @since JavaFX 2.0 | |
*/ | |
public static class DefaultFormatter extends StringConverter<Number> { | |
private DecimalFormat formatter; | |
private String prefix = null; | |
private String suffix = null; | |
/** | |
* Construct a DefaultFormatter for the given NumberAxis | |
* | |
* @param axis | |
* The axis to format tick marks for | |
*/ | |
public DefaultFormatter(final GapNumberAxis axis) { | |
formatter = axis.isAutoRanging() ? new DecimalFormat(axis.currentFormatterProperty.get()) : new DecimalFormat(); | |
final ChangeListener<Object> axisListener = (observable, oldValue, newValue) -> { | |
formatter = axis.isAutoRanging() ? new DecimalFormat(axis.currentFormatterProperty.get()) : new DecimalFormat(); | |
}; | |
axis.currentFormatterProperty.addListener(axisListener); | |
axis.autoRangingProperty().addListener(axisListener); | |
} | |
/** | |
* Construct a DefaultFormatter for the given NumberAxis with a prefix and/or suffix. | |
* | |
* @param axis | |
* The axis to format tick marks for | |
* @param prefix | |
* The prefix to append to the start of formatted number, can be null if not needed | |
* @param suffix | |
* The suffix to append to the end of formatted number, can be null if not needed | |
*/ | |
public DefaultFormatter(GapNumberAxis axis, String prefix, String suffix) { | |
this(axis); | |
this.prefix = prefix; | |
this.suffix = suffix; | |
} | |
/** | |
* Converts the object provided into its string form. | |
* Format of the returned string is defined by this converter. | |
* | |
* @return a string representation of the object passed in. | |
* @see StringConverter#toString | |
*/ | |
@Override | |
public String toString(Number object) { | |
return toString(object, formatter); | |
} | |
private String toString(Number object, String numFormatter) { | |
if(numFormatter == null || numFormatter.isEmpty()) { | |
return toString(object, formatter); | |
} else { | |
return toString(object, new DecimalFormat(numFormatter)); | |
} | |
} | |
private String toString(Number object, DecimalFormat formatter) { | |
if(prefix != null && suffix != null) { | |
return prefix + formatter.format(object) + suffix; | |
} else if(prefix != null) { | |
return prefix + formatter.format(object); | |
} else if(suffix != null) { | |
return formatter.format(object) + suffix; | |
} else { | |
return formatter.format(object); | |
} | |
} | |
/** | |
* Converts the string provided into a Number defined by the this converter. | |
* Format of the string and type of the resulting object is defined by this converter. | |
* | |
* @return a Number representation of the string passed in. | |
* @see StringConverter#toString | |
*/ | |
@Override | |
public Number fromString(String string) { | |
try { | |
int prefixLength = (prefix == null) ? 0 : prefix.length(); | |
int suffixLength = (suffix == null) ? 0 : suffix.length(); | |
return formatter.parse(string.substring(prefixLength, string.length() - suffixLength)); | |
} catch(ParseException e) { | |
return null; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
FYI, there is also a bug on line 36-37, if the Nan appears at the end of the chart data, it isn't safe to get the next, you will get an index out of bounds exception. Need to add a out of range check prior to the get.
https://gist.github.com/sirolf2009/ae8a7897b57dcf902b4ed747b05641f9#file-gaplinechart-java-L36-L37
Thanks for publishing the chart, and the other patches above.