2

Looking this post, I've tried to implement in javaFX, with many difficulties, a Scatter Chart 3D where the grid is my x,y and z axis and the spheres are my points.

How Can I put a legend, axis labels and the range numbers along the axis? I can use only javaFX without external library. I'm desperate.. I'm trying for days..without results Please:help me Thanks.

Code

    import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;

public class GraphingData extends Application {

    private static Random rnd = new Random();

    // size of graph
    int graphSize = 400;

    // variables for mouse interaction
    private double mousePosX, mousePosY;
    private double mouseOldX, mouseOldY;

    private final Rotate rotateX = new Rotate(150, Rotate.X_AXIS);
    private final Rotate rotateY = new Rotate(120, Rotate.Y_AXIS);

    @Override
    public void start(Stage primaryStage) {

        // create axis walls
        Group grid = createGrid(graphSize);

        // initial cube rotation
        grid.getTransforms().addAll(rotateX, rotateY);

        // add objects to scene
        StackPane root = new StackPane();
        root.getChildren().add(grid);

        root.setStyle( "-fx-border-color: red;");
        // create bars
        double gridSizeHalf = graphSize / 2;
        double size = 30;

        //Drawing a Sphere  
        Sphere sphere = new Sphere();  

        //Setting the properties of the Sphere
        sphere.setRadius(10.0);  

        sphere.setTranslateX(-50);
        sphere.setTranslateY(-50);      

        //Preparing the phong material of type specular color
        PhongMaterial material6 = new PhongMaterial();  

        //setting the specular color map to the material
        material6.setDiffuseColor(Color.GREEN);

        sphere.setMaterial(material6);

        grid.getChildren().addAll(sphere);

        // scene
        Scene scene = new Scene(root, 1600, 900, true, SceneAntialiasing.BALANCED);
        scene.setCamera(new PerspectiveCamera());

        scene.setOnMousePressed(me -> {
            mouseOldX = me.getSceneX();
            mouseOldY = me.getSceneY();
        });
        scene.setOnMouseDragged(me -> {
            mousePosX = me.getSceneX();
            mousePosY = me.getSceneY();
            rotateX.setAngle(rotateX.getAngle() - (mousePosY - mouseOldY));
            rotateY.setAngle(rotateY.getAngle() + (mousePosX - mouseOldX));
            mouseOldX = mousePosX;
            mouseOldY = mousePosY;

        });

        makeZoomable(root);

        primaryStage.setResizable(false);
        primaryStage.setScene(scene);
        primaryStage.show();

    }

    /**
     * Axis wall
     */
    public static class Axis extends Pane {

        Rectangle wall;

        public Axis(double size) {

            // wall
            // first the wall, then the lines => overlapping of lines over walls
            // works
            wall = new Rectangle(size, size);
            getChildren().add(wall);

            // grid
            double zTranslate = 0;
            double lineWidth = 1.0;
            Color gridColor = Color.RED;

            for (int y = 0; y <= size; y += size / 10) {

                Line line = new Line(0, 0, size, 0);
                line.setStroke(gridColor);
                line.setFill(gridColor);
                line.setTranslateY(y);
                line.setTranslateZ(zTranslate);
                line.setStrokeWidth(lineWidth);

                getChildren().addAll(line);

            }

            for (int x = 0; x <= size; x += size / 10) {

                Line line = new Line(0, 0, 0, size);
                line.setStroke(gridColor);
                line.setFill(gridColor);
                line.setTranslateX(x);
                line.setTranslateZ(zTranslate);
                line.setStrokeWidth(lineWidth);

                getChildren().addAll(line);

            }

        }

        public void setFill(Paint paint) {
            wall.setFill(paint);
        }

    }

    public void makeZoomable(StackPane control) {

        final double MAX_SCALE = 20.0;
        final double MIN_SCALE = 0.1;

        control.addEventFilter(ScrollEvent.ANY, new EventHandler<ScrollEvent>() {

            @Override
            public void handle(ScrollEvent event) {

                double delta = 1.2;

                double scale = control.getScaleX();

                if (event.getDeltaY() < 0) {
                    scale /= delta;
                } else {
                    scale *= delta;
                }

                scale = clamp(scale, MIN_SCALE, MAX_SCALE);

                control.setScaleX(scale);
                control.setScaleY(scale);

                event.consume();

            }

        });

    }

    /**
     * Create axis walls
     *
     * @param size
     * @return
     */
    private Group createGrid(int size) {

        Group cube = new Group();

        // size of the cube
        Color color = Color.LIGHTGRAY;

        List<Axis> cubeFaces = new ArrayList<>();
        Axis r;

        // back face
        r = new Axis(size);
        r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.5 * 1), 1.0));
        r.setTranslateX(-0.5 * size);
        r.setTranslateY(-0.5 * size);
        r.setTranslateZ(0.5 * size);

        cubeFaces.add(r);

        // bottom face
        r = new Axis(size);
        r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.4 * 1), 1.0));
        r.setTranslateX(-0.5 * size);
        r.setTranslateY(0);
        r.setRotationAxis(Rotate.X_AXIS);
        r.setRotate(90);

        cubeFaces.add(r);

        // right face
        r = new Axis(size);
        r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.3 * 1), 1.0));
        r.setTranslateX(-1 * size);
        r.setTranslateY(-0.5 * size);
        r.setRotationAxis(Rotate.Y_AXIS);
        r.setRotate(90);

        // cubeFaces.add( r);

        // left face
        r = new Axis(size);
        r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.2 * 1), 1.0));
        r.setTranslateX(0);
        r.setTranslateY(-0.5 * size);
        r.setRotationAxis(Rotate.Y_AXIS);
        r.setRotate(90);

        cubeFaces.add(r);

        // top face
        r = new Axis(size);
        r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.1 * 1), 1.0));
        r.setTranslateX(-0.5 * size);
        r.setTranslateY(-1 * size);
        r.setRotationAxis(Rotate.X_AXIS);
        r.setRotate(90);

        // cubeFaces.add( r);

        // front face
        r = new Axis(size);
        r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.1 * 1), 1.0));
        r.setTranslateX(-0.5 * size);
        r.setTranslateY(-0.5 * size);
        r.setTranslateZ(-0.5 * size);

        // cubeFaces.add( r);

        cube.getChildren().addAll(cubeFaces);

        return cube;
    }

    public static double normalizeValue(double value, double min, double max, double newMin, double newMax) {

        return (value - min) * (newMax - newMin) / (max - min) + newMin;

    }

    public static double clamp(double value, double min, double max) {

        if (Double.compare(value, min) < 0)
            return min;

        if (Double.compare(value, max) > 0)
            return max;

        return value;
    }

    public static Color randomColor() {
        return Color.rgb(rnd.nextInt(255), rnd.nextInt(255), rnd.nextInt(255));
    }

    public static void main(String[] args) {
        launch(args);
    }
}
Birdasaur
  • 736
  • 7
  • 10

2 Answers2

1

Here's a basic idea to create some measures on the axes. It is not production-ready but should give you enough to start with.

private Group createGrid(int size) {

    // existing code omitted...

    cube.getChildren().addAll(cubeFaces);


    double gridSizeHalf = size / 2;
    double labelOffset = 30 ;
    double labelPos = gridSizeHalf - labelOffset ;

    for (double coord = -gridSizeHalf ; coord < gridSizeHalf ; coord+=50) {
        Text xLabel = new Text(coord, labelPos, String.format("%.0f", coord));
        xLabel.setTranslateZ(labelPos);
        xLabel.setScaleX(-1);
        Text yLabel = new Text(labelPos, coord, String.format("%.0f", coord));
        yLabel.setTranslateZ(labelPos);
        yLabel.setScaleX(-1);
        Text zLabel = new Text(labelPos, labelPos, String.format("%.0f", coord));
        zLabel.setTranslateZ(coord);
        cube.getChildren().addAll(xLabel, yLabel, zLabel);
        zLabel.setScaleX(-1);
    }

    return cube;
}

I would just place a legend outside the graph, which would just be a 2D grid pane not rotating...

James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thanks you for your help! It was very useful . I have tried to put a 2D Legend, and [read the discussion](http://stackoverflow.com/questions/28628702/javafx-2d-part-in-3d-application). Unfortunely, it's one of my first projct in 3D These are the link of my failures tests: [link1](http://pastebin.com/vG16c4QV) and [ink2](http://pastebin.com/tU3UMhQX). How can I edit and correct ? – Maria Laura Bennato Jan 27 '17 at 19:08
  • Translating the labels like this will translate into 3D space a 2D label which will look correct until you move the camera. Assuming you want a scatter chart that doesn't move or rotate then this will be fine. Other wise you will need to automatically transform the 2D labels whenever you move your camera. (ie... the mouse handler). – Birdasaur Jan 16 '18 at 12:40
  • @Birdasaur Yeah, fair point. If I remember correctly earlier versions of the `Camera` couldn't be moved (it wasn't a subclass of `Node` and you couldn't add it to a scene graph): you would simply set the camera on the scene and if you wanted things to move relative to it, you apply transformations to the root of the scene (or portions of it). There was also no `SubScene` class. Most of my experiments with 3D in JavaFX were before the camera could be moved, so you were forced to transform different parts of your scene graph independently. But your approach looks correct for current versions. – James_D Jan 16 '18 at 13:54
  • I definitely wasn't criticizing your portion which would be essential to the final solution. Part of me feels like adding my transformation part to your initial setup is more of a work around for JavaFX 3D's lack of capability rather than a true solution. (OPEN PUBLIC ACCESS TO THE LOW LEVEL RENDER CALLS !) However it does work and can support any 2D Node based object technically. So combining your code and my transform could allow folks to do some interesting 3D overlays. (anchored callouts, 2D targeting reticules, particle effects (flame, smoke, rain, fog) etc) – Birdasaur Jan 17 '18 at 15:06
0

I know this question is getting old but 2D labels in a JavaFX 3D scene is a topic that comes up a lot and I never see it answered "the right way".

Translating the labels like in James_D's answer will translate into 3D space a 2D label which will look correct until you move the camera. Assuming you want a scatter chart that doesn't move or rotate then this will be fine. Other wise you will need to automatically transform the 2D labels whenever you move your camera. (ie... the mouse handler). You could remove your scatter chart and readd the whole thing to the scene each time but that will be murder on your heap memory and won't be feasible for data sets of any real useful size.

The right way to do it is to use OpenGL or DirectDraw text renders which redraw the labels on each render loop pass but JavaFX 3D doesn't give you access (currently). So the "right way in JavaFX" is to float 2D labels on top of a 3D subscene and then translate them whenever the camera moves. This requires that you transform the 3D coordinate projection of the 3D location you want the label to a 2D screen projection.

To generically manage 2D labels connected to a Point3D in JavaFX 3D you need to do a transform along the following:

Point3D coordinates = node.localToScene(javafx.geometry.Point3D.ZERO);
SubScene oldSubScene = NodeHelper.getSubScene(node);
coordinates = SceneUtils.subSceneToScene(oldSubScene, coordinates);
double x = coordinates.getX();
double y = coordinates.getY();
label.getTransforms().setAll(new Translate(x, y));

Where the node is some actual 3D object already in the 3D subscene. For my applications I simply use a Sphere of an extremely small size it cannot be seen. If you were to follow James_D's example, you could translate the sphere(s) to the same locations that you translated the original axis labels. The label is a standard JavaFX 2D label that you add to your scene... typically through a StackPane such that the labels are floating on top of the 3D subscene.

Now whenever the camera moves/rotates, this causes this transform to be called which slides the label on the 2D layer. Without direct access to the underlying GL or DD calls this is pretty much the only way to do something like this in JavaFX 3D but it works pretty well.

Here is a video example of it working. Here is an open source example of implementing a simple version of floating 2D labels. (Warning, I'm the contributing author for the sample, not trying to promote the library.)

Birdasaur
  • 736
  • 7
  • 10