2016-04-06

JavaFX: LineChart を使いこなそう (7) - ズーム(改2)

JavaFX のラインチャートをズームするサンプルを以前紹介しましたが、ズームの具合がよくありませんでした。確かにズームはできますが、マウスで指定した矩形の領域が正確に拡大されていません。どうやら座標計算が正確では無いようです。手軽にチャートのズームができる方法で気に入っていたので、じっくり調べて修正しました。いや、どうしてもズーム機能を使う目的があったため、なんとか実用に耐えうるレベルまで修正する必要があった、というのが現実です。

動作環境は次の通りです。

  • OS: Fedora 23 x86_64
  • jdk1.8.0_77-1.8.0_77-fcs.x86_64 (Oracle)
  • IDE: NetBeans IDE 8.1

まず最初に実行例を示します。

ソースは、もともと参考にした GitHub Gist のサイト [1] を fork して、修正を加えましたので、それを埋め込みました [2]。最初の LineChartZoomable.java が、LineChart クラスを継承して、ズーム機能を加えたクラス、その下の LineChartZoomableTest.java は、LineChartZoomable クラスを利用したサンプルです。なお、このクラスは、StackPane で最初の配置を行う必要があります。

package linechartzoomable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Point2D;
import javafx.scene.chart.Axis;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
public class LineChartZoomable<X, Y> extends LineChart<X, Y> {
private final Rectangle rect;
private boolean isFirstZoom = true;
private boolean xAxisAutoRanging, yAxisAutoRanging;
private double xAxisLowerBound, xAxisUpperBound, yAxisLowerBound, yAxisUpperBound;
public LineChartZoomable(Axis<X> xAxis, Axis<Y> yAxis) {
super(xAxis, yAxis);
rect = new Rectangle();
rect.setManaged(false);
rect.setFill(Color.LIGHTSEAGREEN.deriveColor(0, 1, 1, 0.5));
setUpZooming();
}
private void setUpZooming() {
ObjectProperty<Point2D> mouseAnchor = new SimpleObjectProperty<>();
setOnMousePressed((MouseEvent event) -> {
double x = event.getX();
double y = event.getY();
mouseAnchor.set(new Point2D(x, y));
rect.setWidth(0);
rect.setHeight(0);
StackPane pane = (StackPane) getParent();
pane.getChildren().add(rect);
if (isFirstZoom) {
NumberAxis xAxis = (NumberAxis) getXAxis();
xAxisAutoRanging = xAxis.isAutoRanging();
xAxisLowerBound = xAxis.getLowerBound();
xAxisUpperBound = xAxis.getUpperBound();
NumberAxis yAxis = (NumberAxis) getYAxis();
yAxisAutoRanging = yAxis.isAutoRanging();
yAxisLowerBound = yAxis.getLowerBound();
yAxisUpperBound = yAxis.getUpperBound();
isFirstZoom = false;
}
});
setOnMouseDragged((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(x - mouseAnchor.get().getX());
rect.setHeight(y - mouseAnchor.get().getY());
});
setOnMouseReleased((MouseEvent event) -> {
StackPane pane = (StackPane) getParent();
pane.getChildren().remove(rect);
if ((rect.getWidth() > 0) && (rect.getHeight() > 0)) {
doZoom();
} else {
releaseZoom();
}
});
}
private void doZoom() {
Point2D zoomTopLeft = new Point2D(rect.getX(), rect.getY());
Point2D zoomBottomRight = new Point2D(rect.getX() + rect.getWidth(), rect.getY() + rect.getHeight());
Point2D chartInScene = this.localToScene(0, 0);
NumberAxis xAxis = (NumberAxis) getXAxis();
xAxis.setAutoRanging(false);
Point2D xAxisInScene = xAxis.localToScene(0, 0);
NumberAxis yAxis = (NumberAxis) getYAxis();
yAxis.setAutoRanging(false);
double xOffset = zoomTopLeft.getX() - xAxisInScene.getX() + chartInScene.getX();
double yOffset = zoomBottomRight.getY() - xAxisInScene.getY() + chartInScene.getY();
double xAxisScale = xAxis.getScale();
double yAxisScale = yAxis.getScale();
xAxis.setLowerBound(xAxis.getLowerBound() + xOffset / xAxisScale);
xAxis.setUpperBound(xAxis.getLowerBound() + rect.getWidth() / xAxisScale);
yAxis.setLowerBound(yAxis.getLowerBound() + yOffset / yAxisScale);
yAxis.setUpperBound(yAxis.getLowerBound() - rect.getHeight() / yAxisScale);
rect.setWidth(0);
rect.setHeight(0);
}
private void releaseZoom() {
setWidth(0);
setHeight(0);
NumberAxis xAxis = (NumberAxis) getXAxis();
xAxis.setAutoRanging(xAxisAutoRanging);
xAxis.setLowerBound(xAxisLowerBound);
xAxis.setUpperBound(xAxisUpperBound);
NumberAxis yAxis = (NumberAxis) getYAxis();
yAxis.setAutoRanging(yAxisAutoRanging);
yAxis.setLowerBound(yAxisLowerBound);
yAxis.setUpperBound(yAxisUpperBound);
isFirstZoom = true;
}
}
package linechartzoomable;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import static javafx.application.Application.launch;
public class LineChartZoomableTest extends Application {
private static final double[][] DATA = new double[][]{
{1, 17.795}, {2, 18.060}, {3, 18.755}, {4, 19.140}, {5, 19.250},
{6, 18.855}, {7, 18.710}, {8, 18.780}, {9, 19.015}, {10, 19.490},
{11, 19.545}, {12, 19.50}, {13, 19.235}, {14, 19.390}, {15, 19.895},
{16, 20.145}, {17, 20.270}, {18, 19.980}, {19, 19.825}, {20, 19.805},
{21, 19.720}, {22, 19.710}, {23, 19.710}, {24, 19.565}, {25, 19.690},
{26, 19.925}, {27, 20.050}, {28, 19.860}, {29, 20.025}, {30, 20.080},
{31, 20.085}, {32, 20.460}, {33, 20.340}, {34, 20.185}, {35, 20.245},
{36, 20.445}, {37, 20.230}, {38, 20.060}, {39, 20.175}, {40, 20.040},
{41, 19.970}, {42, 20.130}, {43, 20.180}, {44, 20.355}, {45, 19.945},
{46, 19.910}, {47, 19.885}, {48, 19.845}, {49, 19.750}, {50, 19.435},
{51, 19.365}, {52, 19.580}, {53, 19.970}, {54, 19.965}, {55, 19.785},
{56, 19.820}, {57, 20.240}, {58, 20.095}, {59, 22.050}, {60, 22.095},
{61, 22.215}, {62, 22.365}, {63, 21.805}, {64, 21.565}, {65, 21.965},
{66, 22.380}
};
@Override
public void start(Stage stage) {
BorderPane pane = new BorderPane();
MenuBar mbar = createButtonBar();
pane.setTop(mbar);
StackPane container = createChart();
pane.setCenter(container);
Scene scene = new Scene(pane);
stage.setScene(scene);
stage.show();
}
private MenuBar createButtonBar() {
MenuBar menuBar = new MenuBar();
Menu menuFile = new Menu("File");
menuBar.getMenus().addAll(menuFile);
MenuItem itemExit = new MenuItem("Exit");
itemExit.setOnAction((ActionEvent t) -> {
System.exit(0);
});
menuFile.getItems().addAll(itemExit);
return menuBar;
}
private StackPane createChart() {
XYChart.Series<Number, Number> series = new XYChart.Series<>();
series.setName("data");
for (double[] data : DATA) {
series.getData().add(new XYChart.Data<>(data[0], data[1]));
}
NumberAxis xAxis = new NumberAxis();
xAxis.setLabel("DATE");
NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("VALUE");
yAxis.setForceZeroInRange(false);
final LineChartZoomable<Number, Number> chart = new LineChartZoomable<>(xAxis, yAxis);
chart.setAnimated(false);
chart.setCreateSymbols(true);
chart.setTitle("Trend Example");
chart.getData().add(series);
// Tooltip
chart.getData().stream().forEach((s) -> {
s.getData().stream().forEach((d) -> {
double x = d.getXValue().doubleValue();
double y = d.getYValue().doubleValue();
Tooltip tooltip = new Tooltip(String.format("(%2.0f, %6.3f)", x, y));
Tooltip.install(d.getNode(), tooltip);
});
});
StackPane stackPane = new StackPane();
stackPane.getChildren().add(chart);
return stackPane;
}
public static void main(String[] args) {
launch(args);
}
}

参考サイト

  1. Example of a LineChart that can be zoomed via mouse.
  2. LineChartZoomable class that can be zoomed via mouse.

 

ブログランキング・にほんブログ村へ
にほんブログ村

0 件のコメント: