在图表上添加文本字段

Add TextField on Chart

我在 Swing 中使用 JavaFX 所以我有方法:

private Scene createScene()  {
Group root = new Group();
Scene scene = new Scene(root);
BorderPane borderPane = new BorderPane();

...
//adding data series to Linechart baseChart
...

StackChart chart = new StackChart(baseChart); //returns StackPane with LineChart-s on it
borderPane.setCenter(chart);

root.getChildren().add(borderPane);

return scene;
}

如何在 Linechart 上放置可编辑的文本,如 here

我尝试在ChartStack中添加到LineChart(chartBackground是StackPane中的第一个LineChart):

chartBackground.setOnMouseClicked(event ->  {
            if (event.getTarget() == chartBackground) {
                chartBackground.add(
                        new EditableDraggableText(event.getX(), event.getY())
                );
                super.getChildren().addAll(new EditableDraggableText(event.getX(), event.getY()));
            }
        });

但它绘制的文本不是在 LineChart 上而是在框架的左侧

所有 MCVE 是下一个 主要 Class:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

import java.util.function.Function;

public class GrinMain extends Application {

    public static final int X_NUMBER = 800;
    public double[] x;
    public double[] y;

    @Override
    public void start(Stage primaryStage) throws Exception {
        NumberAxis xAxis = new NumberAxis(0, X_NUMBER, 200);
        NumberAxis yAxis = new NumberAxis();
        yAxis.setLabel("Series 1");

        LineChart baseChart = new LineChart(xAxis, yAxis);
        baseChart.getData().add(prepareSeries("Serie 1", (x) -> (double) x));

        StackChart chart = new StackChart(baseChart, Color.RED);
        chart.addSeries(prepareSeries("Serie 3", (x) -> (double) -2*x * x), Color.BLUE);


        BorderPane borderPane = new BorderPane();
        borderPane.setCenter(chart);
        borderPane.setBottom(chart.getLegend());

        Scene scene = new Scene(borderPane, 1024, 600);

        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private XYChart.Series<Number, Number> prepareSeries(String name, Function<Integer, Double> function) {
        XYChart.Series<Number, Number> series = new XYChart.Series<>();
        series.setName(name);
        for (int i = 0; i < X_NUMBER; i++) {
            series.getData().add(new XYChart.Data<>(i, function.apply(i)));
        }
        return series;
    }

    private XYChart.Series<Number, Number> prepareSeries(String name, double[] xValues, double[] yValues)   {
        XYChart.Series<Number, Number> series = new XYChart.Series<>();
        series.setName(name);
        for (int i = 0; i < yValues.length; i++){
            series.getData().add(new XYChart.Data(xValues[i], yValues[i]));
        }
        return series;
    }

    public static void main(String[] args) {
        launch(args);
    }
}

堆栈图 class:

import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.chart.Axis;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;

public class StackChart extends StackPane {

    private final LineChart baseChart;
    private final ObservableList<LineChart> backCharts = FXCollections.observableArrayList();

    private final double yAxisWidth = 60;
    private final AnchorPane details;

    private final double yAxisSeparation = 20;
    private double strokeWidth = 0.3;

    public StackChart(LineChart baseChart, Color lineColor) {
        this(baseChart, lineColor, null);
    }

    public StackChart(LineChart baseChart, Color lineColor, Double strokeWidth) {
        if (strokeWidth != null) {
            this.strokeWidth = strokeWidth;
        }
        this.baseChart = baseChart;
        baseChart.setCreateSymbols(false);
        baseChart.setLegendVisible(false);
        baseChart.getXAxis().setAutoRanging(false);
        baseChart.getXAxis().setAnimated(false);
        baseChart.getXAxis().setStyle("-fx-font-size:" + 18);
        baseChart.getYAxis().setAnimated(false);
        baseChart.getYAxis().setStyle("-fx-font-size:" + 18);

        setStyle(baseChart, lineColor);
        setFixedAxisWidth(baseChart);
        setAlignment(Pos.CENTER_LEFT);

        backCharts.addListener((Observable observable) -> rebuildChart());

        details = new AnchorPane();
        bindMouseEvents(baseChart, this.strokeWidth);

        rebuildChart();
    }

    private void bindMouseEvents(LineChart baseChart, Double strokeWidth) {

        getChildren().add(details);

        details.prefHeightProperty().bind(heightProperty());
        details.prefWidthProperty().bind(widthProperty());
        details.setMouseTransparent(true);

        setOnMouseMoved(null);
        setMouseTransparent(false);

        final Axis xAxis = baseChart.getXAxis();
        final Axis yAxis = baseChart.getYAxis();

        final Line xLine = new Line();
        final Line yLine = new Line();
        yLine.setFill(Color.GRAY);
        xLine.setFill(Color.GRAY);
        yLine.setStrokeWidth(strokeWidth/2);
        xLine.setStrokeWidth(strokeWidth/2);
        xLine.setVisible(false);
        yLine.setVisible(false);

        final Node chartBackground = baseChart.lookup(".chart-plot-background");
        for (Node n: chartBackground.getParent().getChildrenUnmodifiable()) {
            if (n != chartBackground && n != xAxis && n != yAxis) {
                n.setMouseTransparent(true);
            }
        }
        chartBackground.setCursor(Cursor.CROSSHAIR);
        chartBackground.setOnMouseEntered((event) -> {
            chartBackground.getOnMouseMoved().handle(event);
            xLine.setVisible(true);
            yLine.setVisible(true);
            details.getChildren().addAll(xLine, yLine);
        });
        chartBackground.setOnMouseExited((event) -> {

            xLine.setVisible(false);
            yLine.setVisible(false);
            details.getChildren().removeAll(xLine, yLine);
        });
        chartBackground.setOnMouseMoved(event -> {
            double x = event.getX() + chartBackground.getLayoutX();
            double y = event.getY() + chartBackground.getLayoutY();

            xLine.setStartX(65);
            xLine.setEndX(details.getWidth()-10);
            xLine.setStartY(y+5);
            xLine.setEndY(y+5);

            yLine.setStartX(x+5);
            yLine.setEndX(x+5);
            yLine.setStartY(12);
            yLine.setEndY(details.getHeight()-28);
        });

        chartBackground.setOnMouseClicked(event ->  {
            if (event.getTarget() == chartBackground) {
                super.getChildren().addAll(new EditableDraggableText(event.getX(), event.getY()));
            }

        });
    }

    private void setFixedAxisWidth(LineChart chart) {
        chart.getYAxis().setPrefWidth(yAxisWidth);
        chart.getYAxis().setMaxWidth(yAxisWidth);
    }

    private void rebuildChart() {
        getChildren().clear();

        getChildren().add(resizeBaseChart(baseChart));
        for (LineChart lineChart : backCharts) {
            getChildren().add(resizeBackgroundChart(lineChart));
        }
        getChildren().add(details);
    }

    private Node resizeBaseChart(LineChart lineChart) {
        HBox hBox = new HBox(lineChart);
        hBox.setAlignment(Pos.CENTER_LEFT);
        hBox.prefHeightProperty().bind(heightProperty());
        hBox.prefWidthProperty().bind(widthProperty());

        lineChart.minWidthProperty().bind(widthProperty().subtract((yAxisWidth+yAxisSeparation)*backCharts.size()));
        lineChart.prefWidthProperty().bind(widthProperty().subtract((yAxisWidth+yAxisSeparation)*backCharts.size()));
        lineChart.maxWidthProperty().bind(widthProperty().subtract((yAxisWidth+yAxisSeparation)*backCharts.size()));

        return lineChart;
    }

    private Node resizeBackgroundChart(LineChart lineChart) {
        HBox hBox = new HBox(lineChart);
        hBox.setAlignment(Pos.CENTER_LEFT);
        hBox.prefHeightProperty().bind(heightProperty());
        hBox.prefWidthProperty().bind(widthProperty());
        hBox.setMouseTransparent(true);

        lineChart.minWidthProperty().bind(widthProperty().subtract((yAxisWidth + yAxisSeparation) * backCharts.size()));
        lineChart.prefWidthProperty().bind(widthProperty().subtract((yAxisWidth + yAxisSeparation) * backCharts.size()));
        lineChart.maxWidthProperty().bind(widthProperty().subtract((yAxisWidth + yAxisSeparation) * backCharts.size()));

        lineChart.translateXProperty().bind(baseChart.getYAxis().widthProperty());
        lineChart.getYAxis().setTranslateX((yAxisWidth + yAxisSeparation) * backCharts.indexOf(lineChart));

        return hBox;
    }

    public void addSeries(XYChart.Series series, Color lineColor) {
        NumberAxis yAxis = new NumberAxis();
        NumberAxis xAxis = new NumberAxis();

        // xAxis
        xAxis.setAutoRanging(false);
        xAxis.setVisible(false);
        xAxis.setOpacity(0.0);
        xAxis.lowerBoundProperty().bind(((NumberAxis) baseChart.getXAxis()).lowerBoundProperty());
        xAxis.upperBoundProperty().bind(((NumberAxis) baseChart.getXAxis()).upperBoundProperty());
        xAxis.tickUnitProperty().bind(((NumberAxis) baseChart.getXAxis()).tickUnitProperty());

        // yAxis
        yAxis.setSide(Side.RIGHT);
        yAxis.setLabel(series.getName());

        // create chart
        LineChart lineChart = new LineChart(xAxis, yAxis);
        lineChart.setAnimated(false);
        lineChart.setLegendVisible(false);
        lineChart.getData().add(series);

        styleBackChart(lineChart, lineColor);
        setFixedAxisWidth(lineChart);

        backCharts.add(lineChart);
    }

    private void styleBackChart(LineChart lineChart, Color lineColor) {
        setStyle(lineChart, lineColor);

        Node contentBackground = lineChart.lookup(".chart-content").lookup(".chart-plot-background");
        contentBackground.setStyle("-fx-background-color: transparent;");

        lineChart.setVerticalZeroLineVisible(false);
        lineChart.setHorizontalZeroLineVisible(false);
        lineChart.setVerticalGridLinesVisible(false);
        lineChart.setHorizontalGridLinesVisible(false);
        lineChart.setCreateSymbols(false);
        lineChart.getXAxis().setStyle("-fx-font-size:" + 18);
        lineChart.getYAxis().setStyle("-fx-font-size:" + 18);
    }

    private String toRGBCode(Color color) {
        return String.format("#%02X%02X%02X",
                (int) (color.getRed() * 255),
                (int) (color.getGreen() * 255),
                (int) (color.getBlue() * 255));
    }

    private void setStyle(LineChart chart, Color lineColor) {
        chart.getYAxis().lookup(".axis-label").setStyle("-fx-text-fill: " + toRGBCode(lineColor) + "; -fx-font-size: 24;");
        Node seriesLine = chart.lookup(".chart-series-line");
        seriesLine.setStyle("-fx-stroke: " + toRGBCode(lineColor) + "; -fx-stroke-width: " + strokeWidth + ";");
    }

    public Node getLegend() {
        HBox hbox = new HBox();

        final CheckBox baseChartCheckBox = new CheckBox(baseChart.getYAxis().getLabel());
        baseChartCheckBox.setSelected(true);
        baseChartCheckBox.setDisable(true);
        baseChartCheckBox.getStyleClass().add("readonly-checkbox");
        baseChartCheckBox.setOnAction(event -> baseChartCheckBox.setSelected(true));
        hbox.getChildren().add(baseChartCheckBox);

        for (final LineChart lineChart : backCharts) {
            CheckBox checkBox = new CheckBox(lineChart.getYAxis().getLabel());
            checkBox.setSelected(true);
            checkBox.setOnAction(event -> {
                if (backCharts.contains(lineChart)) {
                    backCharts.remove(lineChart);
                } else {
                    backCharts.add(lineChart);
                }
            });
            hbox.getChildren().add(checkBox);
        }

        hbox.setAlignment(Pos.CENTER);
        hbox.setSpacing(20);
        hbox.setStyle("-fx-padding: 0 10 20 10");

        return hbox;
    }
}

EditableText Class:

import com.sun.javafx.tk.FontMetrics;
import com.sun.javafx.tk.Toolkit;
import javafx.application.Platform;
import javafx.scene.control.TextField;

class EditableText extends TextField {
// The right margin allows a little bit of space
// to the right of the text for the editor caret.
private final double RIGHT_MARGIN = 5;

EditableText(double x, double y) {
    relocate(x, y);
    getStyleClass().add("editable-text");

    //** CAUTION: this uses a non-public API (FontMetrics) to calculate the field size
    //            the non-public API may be removed in a future JavaFX version.
    // see: https://javafx-jira.kenai.com/browse/RT-8060
    //      Need font/text measurement API
    FontMetrics metrics = Toolkit.getToolkit().getFontLoader().getFontMetrics(getFont());
    setPrefWidth(RIGHT_MARGIN);
    textProperty().addListener((observable, oldTextString, newTextString) ->
                    setPrefWidth(metrics.computeStringWidth(newTextString) + RIGHT_MARGIN)
    );

    Platform.runLater(this::requestFocus);
}

}

EditableDraggableText Class:

import javafx.scene.Cursor;
import javafx.scene.layout.Pane;

class EditableDraggableText extends Pane {
private final double PADDING = 5;
private EditableText text = new EditableText(PADDING, PADDING);

EditableDraggableText(double x, double y) {
    relocate(x - PADDING, y - PADDING);
    getChildren().add(text);
    getStyleClass().add("editable-draggable-text");

    // if the text is empty when we lose focus,
    // the node has no purpose anymore
    // just remove it from the scene.
    text.focusedProperty().addListener((observable, hadFocus, hasFocus) -> {
        if (!hasFocus && getParent() != null && getParent() instanceof Pane &&
                (text.getText() == null || text.getText().trim().isEmpty())) {
            ((Pane) getParent()).getChildren().remove(this);
        }
    });

    enableDrag();
}

public EditableDraggableText(int x, int y, String text) {
    this(x, y);
    this.text.setText(text);
}

// make a node movable by dragging it around with the mouse.
private void enableDrag() {
    final Delta dragDelta = new Delta();
    setOnMousePressed(mouseEvent -> {
        this.toFront();
        // record a delta distance for the drag and drop operation.
        dragDelta.x = mouseEvent.getX();
        dragDelta.y = mouseEvent.getY();
        getScene().setCursor(Cursor.MOVE);
    });
    setOnMouseReleased(mouseEvent -> getScene().setCursor(Cursor.HAND));
    setOnMouseDragged(mouseEvent -> {
        double newX = getLayoutX() + mouseEvent.getX() - dragDelta.x;
        if (newX > 0 && newX < getScene().getWidth()) {
            setLayoutX(newX);
        }
        double newY = getLayoutY() + mouseEvent.getY() - dragDelta.y;
        if (newY > 0 && newY < getScene().getHeight()) {
            setLayoutY(newY);
        }
    });
    setOnMouseEntered(mouseEvent -> {
        if (!mouseEvent.isPrimaryButtonDown()) {
            getScene().setCursor(Cursor.HAND);
        }
    });
    setOnMouseExited(mouseEvent -> {
        if (!mouseEvent.isPrimaryButtonDown()) {
            getScene().setCursor(Cursor.DEFAULT);
        }
    });
}

// records relative x and y co-ordinates.
private class Delta {
    double x, y;
}
}

问题存在是因为您试图在 .chart-plot-background 中添加 Text,这是一个 RegionRegion 适用于显示 Mouse Lines 等选项,它不是您应该添加 EditableDraggableText.

的确切位置

.chart-content 中添加 EditableDraggableText 这是一个 Pane 并且包含

  • 图表背景
  • X 数轴
  • Y 轴

使用以下代码:

Pane chartContent = (Pane)baseChart.lookup(".chart-content");
chartContent.setOnMouseClicked(event ->  {
     chartContent.getChildren().addAll(new EditableDraggableText(event.getX(), event.getY()));
});

Great. But, how do you avoid creating TextFields outside the graph-plot?

将事件的 X 和 Y 坐标与 chart-plot-background 的坐标进行比较,结果有效:)

Pane chartContent = (Pane)baseChart.lookup(".chart-content");
chartContent.setOnMouseClicked(event ->  {
    if(event.getX() >= chartBackground.getBoundsInParent().getMinX() &&
                event.getX() <= chartBackground.getBoundsInParent().getMaxX() &&
                event.getY() >= chartBackground.getBoundsInParent().getMinY() &&
                event.getY() <= chartBackground.getBoundsInParent().getMaxY()) {
        chartContent.getChildren().addAll(new EditableDraggableText(event.getX(), event.getY()));
    }
});