好的,这就是我使用的应用程序,它不是很整洁,但它会完成这项工作:
首先,我使用 Javafx 的 Date 轴类:
https://github.com/dukke/FXCharts/blob/master/DateAxis.java
然后我添加了另一个类,这个类是必要的,但我更容易使用它,所以:
package linechartwithdateaxis;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
/**
*
* @author yschellekens
*/
class HoveredThresholdNode extends StackPane {
DateFormat df = new SimpleDateFormat("MM/dd/yyyy");
HoveredThresholdNode(Date date, double value) {
setPrefSize(5, 5);
final Label label = createDataThresholdLabel(date, value);
setOnMouseEntered(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
getChildren().setAll(label);
setCursor(Cursor.NONE);
toFront();
}
});
setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
}
});
setOnMouseDragEntered(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
getChildren().setAll(label);
}
});
setOnMouseExited(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
getChildren().clear();
setCursor(Cursor.CROSSHAIR);
toBack();
}
});
}
private Label createDataThresholdLabel(Date date, double value) {
final Label label = new Label( java.lang.Math.round(value)+", On " +df.format(date));
label.setStyle("-fx-font-size: 20; -fx-font-weight: bold; -fx-background-color: transparent; -fx-color:transparent;");
label.setTextFill(Color.BLACK);
label.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
return label;
}
}
还有实际的代码,虽然丑陋但有效:
package linechartwithdateaxis;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
/**
*
* @author yschellekens
*/
public class LineChartWithDateAxis extends Application {
DateFormat df = new SimpleDateFormat("MM/dd/yyyy");
final DateAxis dateAxis = new DateAxis(new Date(2014-1900,11,10),new Date(2014-1900,11,25));
final NumberAxis yAxis = new NumberAxis(0,20,1);
Date [] xData ;
ObservableList<XYChart.Data<Date, Number>> series1Data;
ObservableList<XYChart.Series<Date, Number>> series;
private double[] anArray;
final LineChart<Date, Number> lineChart = new LineChart<>(dateAxis, yAxis);
private int i;
@Override
public void start(Stage primaryStage) {
xData = new Date[9];
for (i = 0; i < xData.length ; i++) {xData[i]= new Date(2014-1900,11,i+15); }
dateAxis.setLowerBound(xData[0]);
dateAxis.setUpperBound(xData[xData.length-1]);
anArray = new double[9];
anArray[0] = 2;
anArray[1] = 19;
anArray[2] = 3;
anArray[3] = 5;
anArray[4] = 12;
anArray[5] = 6;
anArray[6] = 2;
anArray[7] = 12;
anArray[8] = 6;
XYChart.Series Dates = new XYChart.Series( "Dates", plotWithVisableLabeles(xData,anArray) );
lineChart.getData().add(Dates);
final BorderPane chartContainer = new BorderPane();
final Button zoomButton = new Button("Zoom");
chartContainer.setCenter(lineChart);
final Button unZoomButton = new Button("Un Zoom");
chartContainer.setBottom(zoomButton);
chartContainer.setRight(unZoomButton);
final Rectangle zoomRect = new Rectangle();
zoomRect.setManaged(false);
zoomRect.setFill(Color.LIGHTSEAGREEN.deriveColor(0, 1, 1, 0.5));
chartContainer.getChildren().add(zoomRect);
setUpZooming(zoomRect, lineChart);
StackPane root = new StackPane();
root.getChildren().add(chartContainer);
Scene scene = new Scene(root, 600, 600);
// final Button resetButton = new Button("Reset");
final BooleanBinding disableControls =
zoomRect.widthProperty().lessThan(5)
.or(zoomRect.heightProperty().lessThan(5));
zoomButton.disableProperty().bind(disableControls);
zoomButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
doZoom(zoomRect);
}
});
unZoomButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
dateAxis.setLowerBound(new GregorianCalendar(2014, 11, 10).getTime());
dateAxis.setUpperBound(new GregorianCalendar(2014, 11, 25).getTime());
}
});
primaryStage.setTitle("zoomable Line chart with Date axis");
primaryStage.setScene(scene);
primaryStage.show();
}
private void setUpZooming(final Rectangle rect, final Node zoomingNode) {
final ObjectProperty<Point2D> mouseAnchor = new SimpleObjectProperty<>();
zoomingNode.setOnMousePressed(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
mouseAnchor.set(new Point2D(event.getX(), event.getY()));
rect.setWidth(0);
rect.setHeight(0);
}
});
zoomingNode.setOnMouseDragged(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
double x = event.getX();
double y = event.getY();
rect.setX(Math.min(x, mouseAnchor.get().getX()));
rect.setY(Math.min(y, mouseAnchor.get().getY()));
rect.setWidth(Math.abs(x - mouseAnchor.get().getX()));
rect.setHeight(Math.abs(y - mouseAnchor.get().getY()));
}
});
}
private void doZoom(Rectangle zoomRect) {
Date leftBorder = dateAxis.getValueForDisplay(zoomRect.getX());
Date RightBorder = dateAxis.getValueForDisplay(zoomRect.getX() + zoomRect.getWidth());
dateAxis.setLowerBound(leftBorder);
dateAxis.setUpperBound(RightBorder);
zoomRect.setWidth(0);
zoomRect.setHeight(0);
}
public ObservableList<XYChart.Data<Date, Double>> plotWithVisableLabeles(Date[] x ,double[] y) {
final ObservableList<XYChart.Data<Date, Double>> dataset = FXCollections.observableArrayList();
i = 0;
while (i < y.length) {
final XYChart.Data< Date, Double> data = new XYChart.Data<>(x[i], y[i]);
final StackPane node = new HoveredThresholdNode(x[i],y[i]);
node.setStyle("-fx-background-color: linear-gradient(black,white);");
data.setNode(node);
dataset.add(data);
i++;
}
return dataset;
}
public static void main(String[] args) {
launch(args);
}
}
我添加了我使用的日期轴版本(它的旧版本,并且由于 SO 限制我删除了所有评论)
package linechartwithdateaxis;
import com.sun.javafx.charts.ChartLayoutAnimator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.SimpleLongProperty;
import javafx.scene.chart.Axis;
import javafx.util.Duration;
import javafx.util.StringConverter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
public final class DateAxis extends Axis<Date> {
private final LongProperty currentLowerBound = new SimpleLongProperty(this, "currentLowerBound");
private final LongProperty currentUpperBound = new SimpleLongProperty(this, "currentUpperBound");
private final ObjectProperty<StringConverter<Date>> tickLabelFormatter = new ObjectPropertyBase<StringConverter<Date>>() {
@Override
protected void invalidated() {
if (!isAutoRanging()) {
invalidateRange();
requestAxisLayout();
}
}
@Override
public Object getBean() {
return DateAxis.this;
}
@Override
public String getName() {
return "tickLabelFormatter";
}
};
private Date minDate, maxDate;
private ObjectProperty<Date> lowerBound = new ObjectPropertyBase<Date>() {
@Override
protected void invalidated() {
if (!isAutoRanging()) {
invalidateRange();
requestAxisLayout();
}
}
@Override
public Object getBean() {
return DateAxis.this;
}
@Override
public String getName() {
return "lowerBound";
}
};
private ObjectProperty<Date> upperBound = new ObjectPropertyBase<Date>() {
@Override
protected void invalidated() {
if (!isAutoRanging()) {
invalidateRange();
requestAxisLayout();
}
}
@Override
public Object getBean() {
return DateAxis.this;
}
@Override
public String getName() {
return "upperBound";
}
};
private ChartLayoutAnimator animator = new ChartLayoutAnimator(this);
private Object currentAnimationID;
private DateAxis.Interval actualInterval = DateAxis.Interval.DECADE;
public DateAxis() {
}
public DateAxis(Date lowerBound, Date upperBound) {
this();
setAutoRanging(false);
setLowerBound(lowerBound);
setUpperBound(upperBound);
}
public DateAxis(String axisLabel, Date lowerBound, Date upperBound) {
this(lowerBound, upperBound);
setLabel(axisLabel);
}
@Override
public void invalidateRange(List<Date> list) {
super.invalidateRange(list);
Collections.sort(list);
if (list.isEmpty()) {
minDate = maxDate = new Date();
} else if (list.size() == 1) {
minDate = maxDate = list.get(0);
} else if (list.size() > 1) {
minDate = list.get(0);
maxDate = list.get(list.size() - 1);
}
}
@Override
protected Object autoRange(double length) {
if (isAutoRanging()) {
return new Object[]{minDate, maxDate};
} else {
if (getLowerBound() == null || getUpperBound() == null) {
throw new IllegalArgumentException("If autoRanging is false, a lower and upper bound must be set.");
}
return getRange();
}
}
@Override
protected void setRange(Object range, boolean animating) {
Object[] r = (Object[]) range;
Date oldLowerBound = getLowerBound();
Date oldUpperBound = getUpperBound();
Date lower = (Date) r[0];
Date upper = (Date) r[1];
setLowerBound(lower);
setUpperBound(upper);
if (animating) {
animator.stop(currentAnimationID);
currentAnimationID = animator.animate(
new KeyFrame(Duration.ZERO,
new KeyValue(currentLowerBound, oldLowerBound.getTime()),
new KeyValue(currentUpperBound, oldUpperBound.getTime())
),
new KeyFrame(Duration.millis(700),
new KeyValue(currentLowerBound, lower.getTime()),
new KeyValue(currentUpperBound, upper.getTime())
)
);
} else {
currentLowerBound.set(getLowerBound().getTime());
currentUpperBound.set(getUpperBound().getTime());
}
}
@Override
protected Object getRange() {
return new Object[]{getLowerBound(), getUpperBound()};
}
@Override
public double getZeroPosition() {
return 0;
}
@Override
public double getDisplayPosition(Date date) {
final double length = getSide().isHorizontal() ? getWidth() : getHeight();
double diff = currentUpperBound.get() - currentLowerBound.get();
double range = length - getZeroPosition();
double d = (date.getTime() - currentLowerBound.get()) / diff;
if (getSide().isVertical()) {
return getHeight() - d * range + getZeroPosition();
} else {
return d * range + getZeroPosition();
}
}
@Override
public Date getValueForDisplay(double displayPosition) {
final double length = getSide().isHorizontal() ? getWidth() : getHeight();
double diff = currentUpperBound.get() - currentLowerBound.get();
double range = length - getZeroPosition();
if (getSide().isVertical()) {
return new Date((long) ((displayPosition - getZeroPosition() - getHeight()) / -range * diff + currentLowerBound.get()));
} else {
return new Date((long) ((displayPosition - getZeroPosition()) / range * diff + currentLowerBound.get()));
}
}
@Override
public boolean isValueOnAxis(Date date) {
return date.getTime() > currentLowerBound.get() && date.getTime() < currentUpperBound.get();
}
@Override
public double toNumericValue(Date date) {
return date.getTime();
}
@Override
public Date toRealValue(double v) {
return new Date((long) v);
}
@Override
protected List<Date> calculateTickValues(double v, Object range) {
Object[] r = (Object[]) range;
Date lower = (Date) r[0];
Date upper = (Date) r[1];
List<Date> dateList = new ArrayList<Date>();
Calendar calendar = Calendar.getInstance();
// The preferred gap which should be between two tick marks.
double averageTickGap = 100;
double averageTicks = v / averageTickGap;
List<Date> previousDateList = new ArrayList<Date>();
DateAxis.Interval previousInterval = DateAxis.Interval.values()[0];
// Starting with the greatest interval, add one of each calendar unit.
for (DateAxis.Interval interval : DateAxis.Interval.values()) {
// Reset the calendar.
calendar.setTime(lower);
dateList.clear();
previousDateList.clear();
actualInterval = interval;
while (calendar.getTime().getTime() <= upper.getTime()) {
dateList.add(calendar.getTime());
calendar.add(interval.interval, interval.amount);
}
if (dateList.size() > averageTicks) {
calendar.setTime(lower);
// Recheck if the previous interval is better suited.
while (calendar.getTime().getTime() <= upper.getTime()) {
previousDateList.add(calendar.getTime());
calendar.add(previousInterval.interval, previousInterval.amount);
}
break;
}
previousInterval = interval;
}
if (previousDateList.size() - averageTicks > averageTicks - dateList.size()) {
dateList = previousDateList;
actualInterval = previousInterval;
}
// At last add the upper bound.
dateList.add(upper);
List<Date> evenDateList = makeDatesEven(dateList, calendar);
if (evenDateList.size() > 2) {
Date secondDate = evenDateList.get(1);
Date thirdDate = evenDateList.get(2);
Date lastDate = evenDateList.get(dateList.size() - 2);
Date previousLastDate = evenDateList.get(dateList.size() - 3);
if (secondDate.getTime() - lower.getTime() < (thirdDate.getTime() - secondDate.getTime()) / 2) {
evenDateList.remove(secondDate);
}
if (upper.getTime() - lastDate.getTime() < (lastDate.getTime() - previousLastDate.getTime()) / 2) {
evenDateList.remove(lastDate);
}
}
return evenDateList;
}
@Override
protected void layoutChildren() {
if (!isAutoRanging()) {
currentLowerBound.set(getLowerBound().getTime());
currentUpperBound.set(getUpperBound().getTime());
}
super.layoutChildren();
}
@Override
protected String getTickMarkLabel(Date date) {
StringConverter<Date> converter = getTickLabelFormatter();
if (converter != null) {
return converter.toString(date);
}
DateFormat dateFormat;
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
if (actualInterval.interval == Calendar.YEAR && calendar.get(Calendar.MONTH) == 0 && calendar.get(Calendar.DATE) == 1) {
dateFormat = new SimpleDateFormat("yyyy");
} else if (actualInterval.interval == Calendar.MONTH && calendar.get(Calendar.DATE) == 1) {
dateFormat = new SimpleDateFormat("MMM yy");
} else {
switch (actualInterval.interval) {
case Calendar.DATE:
case Calendar.WEEK_OF_YEAR:
default:
dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM);
break;
case Calendar.HOUR:
case Calendar.MINUTE:
dateFormat = DateFormat.getTimeInstance(DateFormat.SHORT);
break;
case Calendar.SECOND:
dateFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM);
break;
case Calendar.MILLISECOND:
dateFormat = DateFormat.getTimeInstance(DateFormat.FULL);
break;
}
}
return dateFormat.format(date);
}
private List<Date> makeDatesEven(List<Date> dates, Calendar calendar) {
if (dates.size() > 2) {
List<Date> evenDates = new ArrayList<Date>();
for (int i = 0; i < dates.size(); i++) {
calendar.setTime(dates.get(i));
switch (actualInterval.interval) {
case Calendar.YEAR:
if (i != 0 && i != dates.size() - 1) {
calendar.set(Calendar.MONTH, 0);
calendar.set(Calendar.DATE, 1);
}
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 6);
break;
case Calendar.MONTH:
if (i != 0 && i != dates.size() - 1) {
calendar.set(Calendar.DATE, 1);
}
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 5);
break;
case Calendar.WEEK_OF_YEAR:
// Make weeks begin with first day of week?
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 4);
break;
case Calendar.DATE:
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 3);
break;
case Calendar.HOUR:
if (i != 0 && i != dates.size() - 1) {
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
}
calendar.set(Calendar.MILLISECOND, 2);
break;
case Calendar.MINUTE:
if (i != 0 && i != dates.size() - 1) {
calendar.set(Calendar.SECOND, 0);
}
calendar.set(Calendar.MILLISECOND, 1);
break;
case Calendar.SECOND:
calendar.set(Calendar.MILLISECOND, 0);
break;
}
evenDates.add(calendar.getTime());
}
return evenDates;
} else {
return dates;
}
}
public final ObjectProperty<Date> lowerBoundProperty() {
return lowerBound;
}
public final Date getLowerBound() {
return lowerBound.get();
}
public final void setLowerBound(Date date) {
lowerBound.set(date);
}
public final ObjectProperty<Date> upperBoundProperty() {
return upperBound;
}
public final Date getUpperBound() {
return upperBound.get();
}
public final void setUpperBound(Date date) {
upperBound.set(date);
}
public final StringConverter<Date> getTickLabelFormatter() {
return tickLabelFormatter.getValue();
}
public final void setTickLabelFormatter(StringConverter<Date> value) {
tickLabelFormatter.setValue(value);
}
public final ObjectProperty<StringConverter<Date>> tickLabelFormatterProperty() {
return tickLabelFormatter;
}
private enum Interval {
DECADE(Calendar.YEAR, 10),
YEAR(Calendar.YEAR, 1),
MONTH_6(Calendar.MONTH, 6),
MONTH_3(Calendar.MONTH, 3),
MONTH_1(Calendar.MONTH, 1),
WEEK(Calendar.WEEK_OF_YEAR, 1),
DAY(Calendar.DATE, 1),
HOUR_12(Calendar.HOUR, 12),
HOUR_6(Calendar.HOUR, 6),
HOUR_3(Calendar.HOUR, 3),
HOUR_1(Calendar.HOUR, 1),
MINUTE_15(Calendar.MINUTE, 15),
MINUTE_5(Calendar.MINUTE, 5),
MINUTE_1(Calendar.MINUTE, 1),
SECOND_15(Calendar.SECOND, 15),
SECOND_5(Calendar.SECOND, 5),
SECOND_1(Calendar.SECOND, 1),
MILLISECOND(Calendar.MILLISECOND, 1);
private final int amount;
private final int interval;
private Interval(int interval, int amount) {
this.interval = interval;
this.amount = amount;
}
}
}
就是这样: