I have a horizontal line drawn on a chart. When hovering over it the cursor changes to CursorType.S_RESIZE. That indicates the user can start to drag. As the line is very thin you have to place the cursor very accurate. For a better user experience I would like to add a margin above and below the line to enter the draggable zone easier.
Is there a way to make the line “thicker” so the setOnMouseMoved() event fires already when approaching?
import javafx.application.Application;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.chart.Axis;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class DragLine extends Application {
public void start(Stage stage) {
ChartWithLine chartWithLine = new ChartWithLine(new NumberAxis(), new NumberAxis());
stage.setScene(new Scene(chartWithLine, 500, 400));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
class ChartWithLine<X, Y> extends LineChart {
public ChartWithLine(Axis axis, Axis axis2) {
super(axis, axis2);
line = new Line();
line.setOnMouseMoved(event -> line.setCursor(Cursor.S_RESIZE));
getPlotChildren().add(line);
}
private Line line;
public void layoutPlotChildren() {
super.layoutPlotChildren();
double yPos = getYAxis().getDisplayPosition(55);
line.setStartX(0);
line.setEndX(getBoundsInLocal().getWidth());
line.setStartY(yPos);
line.setEndY(yPos);
}
}
Here is my work around. I am plotting a second, thicker line at the same position where I plot the thin line. The thicker line is set to transparent so it is not visible. The dragging functionality is set to the thicker line. When it is dragged, both lines are plotted to the dragged location.
This solves the issue that it is hard to grab a thin line with the mouse. But I do not really like it for two reasons. First there is a second line I do not need at all actually. And second if you hover over the middle of the thicker line where the thin visible line is located, the mouse changes back to not draggable cursor. I would have to implement dragability for the thinner line now too to avoid this. But this is pretty much overkill.
Again, is there some kind of way to set the line thicker without making it look thicker?
import javafx.application.Application;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.chart.Axis;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.input.MouseEvent;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class DragLine extends Application {
public void start(Stage stage) {
ChartWithLine chartWithLine = new ChartWithLine(new NumberAxis(), new NumberAxis());
stage.setScene(new Scene(chartWithLine, 500, 400));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
//ChartWithLine
class ChartWithLine<X, Y> extends LineChart {
public ChartWithLine(Axis axis, Axis axis2) {
super(axis, axis2);
draggableLine = new DraggableLine(this);
getPlotChildren().addAll(draggableLine.lineToShow, draggableLine.lineToDrag);
}
private DraggableLine draggableLine;
public void layoutPlotChildren() {
super.layoutPlotChildren();
updateLinePlot();
}
//updateLinePlot called when line was dragged
public void updateLinePlot() {
//mouse position after drag
double yPos = draggableLine.mousePosY;
System.out.println("Line dragged to: " + getYAxis().getValueForDisplay(yPos));
//plot lines accordingly to new mouse position
Line line = draggableLine.lineToDrag;
line.setStartX(0);
line.setEndX(getBoundsInLocal().getWidth());
line.setStartY(yPos);
line.setEndY(yPos);
line = draggableLine.lineToShow;
line.setStartX(0);
line.setEndX(getBoundsInLocal().getWidth());
line.setStartY(yPos);
line.setEndY(yPos);
}
}
//DraggableLine
class DraggableLine {
public DraggableLine(ChartWithLine chart) {
this.chart = chart;
//lineToShow is thin line plotted visible on chart
lineToShow = new Line();
//lineToDrag is plotted at same position on chart as thin visible line.
lineToDrag = new Line();
//set transparent to make it not visible
lineToDrag.setStyle("-fx-stroke: transparent;");
//set line to drag stroke width very broad so it is easy to grab
lineToDrag.setStrokeWidth(20.0);
lineToDrag.setOnMouseMoved(this::mouseOver);
lineToDrag.setOnMouseDragged(event -> onMouseDragged(event.getY()));
lineToDrag.setOnMousePressed(this::onMousePressed);
lineToDrag.setOnMouseReleased(event -> onMouseReleased());
}
private ChartWithLine chart;
public Line lineToShow;
public Line lineToDrag;
boolean isDragging = false;
public double mousePosY = 55;
//change cursor
protected void mouseOver(MouseEvent event) {
if (isDragZone(event)) {
lineToDrag.setCursor(Cursor.S_RESIZE);
} else {
lineToDrag.setCursor(Cursor.DEFAULT);
}
}
//mouse pressed over draggable zone
void onMousePressed(MouseEvent event) {
if (isDragZone(event))
isDragging = true;
}
//mouse released
void onMouseReleased() {
isDragging = false;
}
//change values when mouse is dragging
void onMouseDragged(double y) {
if (!isDragging) return;
mousePosY = y;
chart.updateLinePlot();
}
//check if mouse is in draggable zone
protected boolean isDragZone(MouseEvent event) {
return event.getY() > (lineToDrag.getStartY()) || event.getY() < (lineToDrag.getStartY());
}
}
Related
I made this code that creates a scatter chart and allows me to change the color of a node on the plot when I click/select it.
package com.jpc.javafx.charttest;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class CreateChart extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
//-------Create Chart--------------
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
XYChart.Series<Number,Number> dataSeries1 = new XYChart.Series();
ScatterChart chart = new ScatterChart(xAxis,yAxis);
dataSeries1.getData().add(new XYChart.Data( 1, 567));
dataSeries1.getData().add(new XYChart.Data( 5, 612));
dataSeries1.getData().add(new XYChart.Data(10, 800));
chart.getData().add(dataSeries1);
//-----Select node and change color -----
for(final XYChart.Data<Number,Number> data : dataSeries1.getData()) {
data.getNode().setOnMouseClicked(e-> {
//dataSeries1.getNode().lookup(".chart-symbol").setStyle("-fx-background-color: red"); that does not work
data.getNode().setStyle("-fx-background-color: blue" );
});
}
VBox vbox = new VBox(chart);
Scene scene = new Scene(vbox, 400, 200);
primaryStage.setScene(scene);
primaryStage.setHeight(300);
primaryStage.setWidth(1200);
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
The problem is that when I select another point the previous one stays blue. So I need to reset all the nodes to the default color before I change the selected point's color.
I tried to add this:
dataSeries1.getNode().lookup(".chart-symbol").setStyle("-fx-background-color: red");
but I get:
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
To summarize your requirement:
a visual property of a chart-symbol should be marked on user interaction
there should be only one such marked symbol
Sounds like a kind of selection mechanism - which is not supported for chart symbols out of the box, application code must take care of it. The task is
keep track of the (last) selected symbol
guarantee that at any time only a single symbol is selected
keep the visual state of un/selected as needed
The most simple implementation for the logic (the first two bullets) would be to keep a reference to the current selected and update it on user interaction. An appropriate instrument for the latter would be a PseudoClass: can be defined in the css and de/activated along with the logic.
Code snippets (to be inserted into your example)
// Pseudo-class
private PseudoClass selected = PseudoClass.getPseudoClass("selected");
// selected logic
private Node selectedSymbol;
protected void setSelectedSymbol(Node symbol) {
if (selectedSymbol != null) {
selectedSymbol.pseudoClassStateChanged(selected, false);
}
selectedSymbol = symbol;
if (selectedSymbol != null) {
selectedSymbol.pseudoClassStateChanged(selected, true);
}
}
// event handler on every symbol
data.getNode().setOnXX(e -> setSelectedSymbol(data.getNode()));
css example, to be loaded via a style-sheet f.i.:
.chart-symbol:selected {
-fx-background-color: blue;
}
One thing you can do is loop through the data and change the color for the one clicked and set all the other to null
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class CreateChart extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
//-------Create Chart--------------
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
XYChart.Series<Number,Number> dataSeries1 = new XYChart.Series();
ScatterChart chart = new ScatterChart(xAxis,yAxis);
dataSeries1.getData().add(new XYChart.Data( 1, 567));
dataSeries1.getData().add(new XYChart.Data( 5, 612));
dataSeries1.getData().add(new XYChart.Data(10, 800));
chart.getData().add(dataSeries1);
//-----Select node and change color -----
for(final XYChart.Data<Number,Number> data : dataSeries1.getData()) {
data.getNode().setOnMouseClicked(e-> {
for(final XYChart.Data<Number,Number> data2 : dataSeries1.getData()) {
if(data == data2)
{
data2.getNode().setStyle("-fx-background-color: blue" );
}
else
{
data2.getNode().setStyle(null);
}
}
});
}
VBox vbox = new VBox(chart);
Scene scene = new Scene(vbox, 400, 200);
primaryStage.setScene(scene);
primaryStage.setHeight(300);
primaryStage.setWidth(1200);
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
I have a JavaFX application with a pane that contains rectangles. These rectangles can be moved by dragging the mouse.
When I drag a rectangle over another rectangle, I would like the second (background) rectangle to be highlighted. This works, see code below
private boolean moveInProgress;
private Point2D prevPos;
public void onMousePressed(MouseEvent event) {
setMouseTransparent(true);
Point2D point = new Point2D(event.getSceneX(), event.getSceneY());
if (!moveInProgress) {
moveInProgress = true;
prevPos = point;
LOG.debug("Mouse move started on location " + prevPos);
}
event.consume();
}
public void onMouseDragged(MouseEvent event) {
if (moveInProgress) {
Point2D point = new Point2D(event.getSceneX(), event.getSceneY());
this.toFront();
double[] translationVector = new double[2];
translationVector[0] = point.getX() - prevPos.getX();
translationVector[1] = point.getY() - prevPos.getY();
setTranslateX(getTranslateX() + translationVector[0]);
setTranslateY(getTranslateY() + translationVector[1]);
prevPos = point;
}
event.consume();
}
public void onMouseReleased(MouseEvent event) {
setMouseTransparent(false);
if (moveInProgress) {
moveInProgress = false;
}
event.consume();
}
public void onDragDetected(MouseEvent event) {
startFullDrag();
event.consume();
}
public void onMouseDragEntered(MouseDragEvent event) {
getStyleClass().add("drag-target");
event.consume();
}
public void onMouseDragExited(MouseDragEvent event) {
if (getStyleClass().contains("drag-target")) {
getStyleClass().remove("drag-target");
}
event.consume();
}
I would like to highlight the underlying rectangle when more than half of my dragging rectangle overlaps. In this picture, I would like to highlight the red rectangle, since the grey rectangle overlaps more than half of it.
The problem is that the MouseDragEntered and MouseDragExited events are fired based on my mouse position. When my mouse position is for example the black dot in the picture, my mouse events will only be fired when my mouse enters the red rectangle.
Can anyone give me some pointers how to highlight the red rectangle when during a drag action of the grey rectangle, more than half of it overlaps?
One approach is to have each rectangle observe the bounds of the rectangle that is being dragged. Then it's reasonably easy to do a computation using Shape.intersect (or by other means) to see if the rectangle is 50% covered by the rectangle being dragged. The tricky part here is adding the listeners to the rectangle being dragged and removing them again when the rectangle stops being dragged.
Here's a quick example. I think I have things set up a little differently from the way you have them set up, but you should be able to adapt this to your use case easily enough.
import java.util.Random;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.stage.Stage;
public class DraggingHighlightRectangles extends Application {
private final Random rng = new Random();
private final ObjectProperty<Rectangle> draggingRectangle = new SimpleObjectProperty<>();
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
pane.setMinSize(600, 600);
Button newRectButton = new Button("New Rectangle");
newRectButton.setOnAction(e -> pane.getChildren().add(createRectangle()));
BorderPane.setAlignment(newRectButton, Pos.CENTER);
BorderPane.setMargin(newRectButton, new Insets(5));
BorderPane root = new BorderPane(pane);
root.setBottom(newRectButton);
Scene scene = new Scene(root);
scene.getStylesheets().add("style.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private Rectangle createRectangle() {
Rectangle rect = new Rectangle(rng.nextInt(400)+100, rng.nextInt(500)+50, 100, 50);
rect.setFill(randomColor());
rect.getStyleClass().add("rect");
ChangeListener<Bounds> boundsListener = (obs, oldBounds, newBounds) -> {
double myArea = rect.getWidth() * rect.getHeight() ;
Shape intersection = Shape.intersect(draggingRectangle.get(), rect);
Bounds intersectionBounds = intersection.getBoundsInLocal();
double intersectionArea = intersectionBounds.getWidth() * intersectionBounds.getHeight() ;
rect.pseudoClassStateChanged(PseudoClass.getPseudoClass("highlight"), intersectionArea >= 0.5 * myArea);
};
draggingRectangle.addListener((obs, oldRect, newRect) -> {
if (oldRect != null) {
oldRect.boundsInLocalProperty().removeListener(boundsListener);
}
if (newRect != null && newRect != rect) {
newRect.boundsInLocalProperty().addListener(boundsListener);
}
rect.pseudoClassStateChanged(PseudoClass.getPseudoClass("highlight"), false);
});
class MouseLocation { double x, y ; }
MouseLocation mouseLocation = new MouseLocation();
rect.setOnMousePressed(e -> {
draggingRectangle.set(rect);
rect.toFront();
mouseLocation.x = e.getX() ;
mouseLocation.y = e.getY() ;
});
rect.setOnMouseDragged(e -> {
rect.setX(rect.getX() + e.getX() - mouseLocation.x);
rect.setY(rect.getY() + e.getY() - mouseLocation.y);
mouseLocation.x = e.getX() ;
mouseLocation.y = e.getY() ;
});
rect.setOnMouseReleased(e -> draggingRectangle.set(null));
return rect ;
}
private Color randomColor() {
return Color.rgb(rng.nextInt(256), rng.nextInt(256), rng.nextInt(256));
}
public static void main(String[] args) {
launch(args);
}
}
My stylesheet, style.css, just contains
.rect:highlight {
-fx-fill: yellow ;
}
Could someone help me? I Need draw a circle in javaFX. It should be Partially filled (bottom part). Top part should be transparent. Level of filled part I should able to change in runtime. Also, it could be circle filled with two colors
Thanks
You could just use a rectangle and a circle as its clipping shape. By moving the rectangle up and down while keeping the clipping in place you could simulate the filling level.
You could use a Linear Gradient to do this inside a method where you must each time indicate a color :
private void changeColor(String color){
if(color == null){
color = "transparent";
}
circle.setStyle("-fx-fill:linear-gradient( from 100.0% 100.0% to 100.0% 0.0%, rgb(77,102,204) 0.5," + color +" 0.5);");
}
Here's a demo :
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class Launcher extends Application{
private Pane root = new Pane();
private Scene scene;
private Circle circle = new Circle(200, 200, 100);
private Button btn = new Button("Change");
private boolean change = false;
#Override
public void start(Stage stage) throws Exception {
btn.setOnAction(evt->{
if(change){
change = !change;
changeColor("red");
}else{
change = !change;
changeColor("transparent");
}
});
changeColor("green");
root.getChildren().addAll(btn,circle);
scene = new Scene(root,400,400);
stage.setScene(scene);
stage.show();
}
private void changeColor(String color){
if(color == null || color.isEmpty()){
color = "transparent";
}
circle.setStyle("-fx-fill:linear-gradient( from 100.0% 100.0% to 100.0% 0.0%, rgb(77,102,204) 0.5," + color +" 0.5);");
}
public static void main(String[] args) {
launch(args);
}
}
It's just a simple method you can find better and more powerful example I think, good luck !
Context :
Hi !
I'm trying to create a little popup which display the value of slice when mouse hover, on my PieChart (with JavaFX).
I successed on my LineChart, AreaChart etc.. Thanks this post : JavaFX LineChart Hover Values (thank you so much Jewelsea for your help).
Problem (1/2) :
But with the PieChart, I have a problem : The popup is blinking oO
My code :
With syntactic color : https://bpaste.net/show/12838ad6b2e2
import java.util.ArrayList;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.chart.PieChart;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import com.alpha.client.view.nodes.stats.statsEngine.beans.ListRepere;
import com.alpha.client.view.nodes.stats.statsEngine.beans.OptionsChart;
import com.alpha.client.view.nodes.stats.statsEngine.beans.ValueStat;
/**
*
* #author Zombkey
*/
public class PieChartNode implements ChartNode {
//My personnal attributes
private ListRepere categories;
private ArrayList<ValueStat> values;
//The PieChart
private PieChart chart;
//The data of Chart, will be fill by a thread
private ObservableList<PieChart.Data> pieChartData;
//The node which contain chart and label
private Group group;
//The Label
private final Label caption;
public PieChartNode(ListRepere categories, ArrayList<ValueStat> values, OptionsChart optionsChart) {
this.categories = categories;
this.values = values;
//New Group
group = new Group();
//I must use a StackPane to place Label hover Chart
StackPane pane = new StackPane();
group.getChildren().add(pane);
//Init' PieChart
pieChartData = FXCollections.observableArrayList();
chart = new PieChart(pieChartData);
chart.setStartAngle(180.0);
//Add chart to StackPane
pane.getChildren().add(chart);
//Init Popup(Label)
caption = new Label("");
caption.setVisible(false);
caption.getStyleClass().addAll("chart-line-symbol", "chart-series-line");
caption.setStyle("-fx-font-size: 12; -fx-font-weight: bold;");
caption.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
//Add Label to StackPane
pane.getChildren().add(caption);
}
#Override
public Node getNodeGraph() {
return (Node) group;
}
#Override
public Task initTaskFormat() {
Task<Void> task = new Task<Void>() {
#Override
protected Void call() throws Exception {
//i and sizeOfallElements are just use for ProgressBar
int i = 0;
int sizeOfallElements = values.size();
updateProgress(i, sizeOfallElements);
//For Each ValueStat (a Personnal pojo Class), I must create a slice
for (ValueStat v : values) {
//Create the PieChart.Data and add it to ObservableList
PieChart.Data dataTemp = new PieChart.Data(v.getCategorie().getStringName(), v.getDoubleValue());
pieChartData.add(dataTemp);
//HERE, the interessante code !
//At the same way that the LineChart, I add Event when mouse entered and mouse exited.
//When mouse entered (on the slice of PieChart)
dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_ENTERED,
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
System.out.println("MOUSE_ENTERED : "+dataTemp.getName());
//I display Label
caption.setVisible(true);
//I move Label near the mouse cursor
caption.setTranslateX(e.getX());
caption.setTranslateY(e.getY());
//I hide the mouse cursor
dataTemp.getNode().setCursor(Cursor.NONE);
//I change text of Label
caption.setText(String.valueOf(dataTemp.getPieValue()) + "\n" + dataTemp.getName());
//I try to change the frame color of Label
caption.getStyleClass().add(dataTemp.getNode().getStyleClass().get(2));
}
});
//When mouse exited (the slice of PieChart)
dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_EXITED,
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
System.out.println("MOUSE_EXITED : "+dataTemp.getName());
//I Hide Label
caption.setVisible(false);
//I show the mouse cursor
dataTemp.getNode().setCursor(Cursor.DEFAULT);
}
});
//Update progress
updateProgress(i++, sizeOfallElements);
}
return null;
}
};
return task;
}
}
Problem (2/2) :
The problem is that the events (MOUSE_ENTERED and MOUSE_EXITED) are emitted, too often instead of once.
Ex :
I just put in, then put off, my mouse hover a slice.
Here the result on console :
MOUSE_ENTERED : BC
MOUSE_EXITED : BC
MOUSE_ENTERED : BC
MOUSE_EXITED : BC
MOUSE_ENTERED : BC
MOUSE_EXITED : BC
MOUSE_ENTERED : BC
MOUSE_EXITED : BC
Anyone know why the event bug ?
Thanks : )
It not the blinking effect caused by label?
When you shows the label, it means that you exited the node which is listened. This causes hiding the label. When label disappears, it fires the mouse entered event on the node, it shows the label etc.
Not tested, just an idea.
EDIT:
If I am right, try to avoid putting label under the mouse pointer:
caption.setTranslateX(e.getX()+10);
caption.setTranslateY(e.getY()+10);
For example (10 is a magic number, depends on insets etc.)
Thanks all for your help.
#maskacovnik to find the problem, #James_D to find a cool solution, and #ItachiUchiha to put my image on my post : D
Now, my new code.
import java.util.ArrayList;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.chart.PieChart;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import com.alpha.client.view.nodes.stats.statsEngine.beans.ListRepere;
import com.alpha.client.view.nodes.stats.statsEngine.beans.OptionsChart;
import com.alpha.client.view.nodes.stats.statsEngine.beans.ValueStat;
public class PieChartNode implements ChartNode {
//My personnal attributes
private ListRepere categories;
private ArrayList<ValueStat> values;
//The PieChart
private PieChart chart;
//The data of Chart, will be fill by a thread
private ObservableList<PieChart.Data> pieChartData;
//The node which contain chart and label
private Group group;
//The Label
private final Label caption;
public PieChartNode(ListRepere categories, ArrayList<ValueStat> values, OptionsChart optionsChart) {
this.categories = categories;
this.values = values;
//New Group
group = new Group();
//I must use a StackPane to place Label hover Chart
StackPane pane = new StackPane();
group.getChildren().add(pane);
//Init' PieChart
pieChartData = FXCollections.observableArrayList();
chart = new PieChart(pieChartData);
chart.setStartAngle(180.0);
//Add chart to StackPane
pane.getChildren().add(chart);
//Init Popup(Label)
caption = new Label("");
caption.setVisible(false);
caption.getStyleClass().addAll("chart-line-symbol", "chart-series-line");
caption.setStyle("-fx-font-size: 12; -fx-font-weight: bold;");
caption.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
//Add Label to StackPane
pane.getChildren().add(caption);
}
#Override
public Node getNodeGraph() {
return (Node) group;
}
#Override
public Task initTaskFormat() {
Task<Void> task = new Task<Void>() {
#Override
protected Void call() throws Exception {
//i and sizeOfallElements are just use for ProgressBar
int i = 0;
int sizeOfallElements = values.size();
updateProgress(i, sizeOfallElements);
//For Each ValueStat (a Personnal pojo Class), I must create a slice
for (ValueStat v : values) {
//Create the PieChart.Data and add it to ObservableList
PieChart.Data dataTemp = new PieChart.Data(v.getCategorie().getStringName(), v.getDoubleValue());
pieChartData.add(dataTemp);
//At the same way that the LineChart, I add Event when mouse entered and mouse exited.
//When mouse entered (on the slice of PieChart)
dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_ENTERED,
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
//Set Label ignores the mouse
caption.setMouseTransparent(true);
//I move Label near the mouse cursor, with a offset !
caption.setTranslateX(e.getX());
caption.setTranslateY(e.getY()+20);
//I change text of Label
caption.setText(String.valueOf(dataTemp.getPieValue()) + "\n" + dataTemp.getName());
//Change the color of popup, to adapt it to slice
if(caption.getStyleClass().size() == 4){
caption.getStyleClass().remove(3);
}
caption.getStyleClass().add(dataTemp.getNode().getStyleClass().get(2));
//I display Label
caption.setVisible(true);
}
});
//Need to add a event when the mouse move hover the slice
//If I don't the popup stay blocked on edges of the slice.
dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_MOVED,
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
//Keep Label near the mouse
caption.setTranslateX(e.getX());
caption.setTranslateY(e.getY()+20);
}
});
//When mouse exited (the slice of PieChart)
dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_EXITED,
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
//I Hide Label
caption.setVisible(false);
}
});
//Update progress
updateProgress(i++, sizeOfallElements);
}
return null;
}
};
return task;
}
}
Here the result :
I had the same problem but also wanted to make sure that the popup can extend beyond the chart, i.e. that it does not get cut off when the text does not fit in the chart. Here's a solution using a Tooltip instead of a Label:
public class ChartHoverUtil<T> {
public static void setupPieChartHovering(PieChart chart) {
new ChartHoverUtil<PieChart.Data>(
data -> String.format("Value = ", data.getPieValue()),
data -> data.getNode())
.setupHovering(chart.getData());
}
private final Tooltip tooltip = new Tooltip();
private final SimpleBooleanProperty adjustingTooltip = new SimpleBooleanProperty(false);
private final Function<T, String> textProvider;
private final Function<T, Node> nodeProvider;
private EventHandler<MouseEvent> moveHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
if (tooltip.isShowing()) {
setLabelPosition(e);
}
}
};
private EventHandler<MouseEvent> enterHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
adjustingTooltip.set(true);
Node chartNode = (Node) e.getSource();
tooltip.show(chartNode, e.getScreenX(), e.getScreenY());
setLabelPosition(e);
ObservableBooleanValue stillHovering = chartNode.hoverProperty().or(adjustingTooltip);
stillHovering.addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean nowHovering) {
if (!nowHovering) {
stillHovering.removeListener(this);
tooltip.hide();
}
}
});
T chartData = (T) chartNode.getUserData();
String txt = textProvider.apply(chartData);
tooltip.setText(txt);
adjustingTooltip.set(false);
}
};
public ChartHoverUtil(Function<T, String> textProvider, Function<T, Node> getNode) {
this.textProvider = textProvider;
this.nodeProvider = getNode;
tooltip.addEventFilter(MouseEvent.MOUSE_MOVED, moveHandler);
}
public void setupHovering(Collection<T> data) {
for (T chartData : data) {
Node node = nodeProvider.apply(chartData);
node.setUserData(chartData);
setupNodeHovering(node);
}
}
private void setupNodeHovering(Node node) {
node.addEventFilter(MouseEvent.MOUSE_MOVED, moveHandler);
node.addEventHandler(MouseEvent.MOUSE_ENTERED, enterHandler);
// Do not use MOUSE_EXIT handler because it is triggered immediately when showing the tooltip
}
private void setLabelPosition(MouseEvent e) {
adjustingTooltip.set(true);
tooltip.setAnchorX(e.getScreenX());
tooltip.setAnchorY(e.getScreenY() + 20);
adjustingTooltip.set(false);
}
}
I am trying to plot javafx bar graph using line chart. Each bar line is drawn using a vertical line drawn with two points and line symbol removed. There could be many series(Bar line) in my application but want to show only two legends only.
Currently legends were shown as many series been added. Somehow i am able to show only two legends and hided others. But now problem exist with spaces used by hided legends.
My current code is as below:-
package graph;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Tooltip;
import javafx.stage.Stage;
import com.sun.javafx.charts.Legend;
public class BarGraphUsingLineChart extends Application {
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
final MyLineChart<Number,Number> lineChart =
new MyLineChart<Number,Number>(xAxis,yAxis);
private boolean valid=true;
private boolean invalid=true;
#Override public void start(Stage stage) {
stage.setTitle("Bar Chart Using Lines");
xAxis.setLabel("Month");
lineChart.setTitle("BAR CHART DEMO");
ObservableList<XYChart.Series<Number,Number>> graphData = FXCollections.observableArrayList();
for(int i=1; i<=10;i++)
{
if(i%2==0)
{
graphData.add(drawBarline(i*10, i*5, true));
}
else{
graphData.add(drawBarline(i*10, i*5, false));
}
}
// Dont show symbol of line charts
lineChart.setCreateSymbols(false);
Scene scene = new Scene(lineChart,800,600);
lineChart.setData(graphData);
stage.setScene(scene);
stage.getScene().getStylesheets().add("/graph/BarChart.css");
updateStyleSheet();
stage.show();
}
private XYChart.Series<Number, Number> drawBarline(Number xAxis, Number yAxis, boolean valid)
{
XYChart.Series<Number, Number> channel_Series = new XYChart.Series<Number, Number>();
channel_Series.getData().add(new XYChart.Data<Number, Number>(yAxis, xAxis ));
channel_Series.getData().add(new XYChart.Data<Number, Number>(yAxis, 0.0 ));
if(valid) {
channel_Series.setName("Valid");
}
else
{
channel_Series.setName("Invalid");
}
return channel_Series;
}
private void updateStyleSheet()
{
for(Node symbol : lineChart.lookupAll(".chart-legend-item")){
if(valid)
{
((Legend)symbol.getParent()).getItems().get(0).setText("Valid");
valid=false;
}
else if(invalid){
((Legend)symbol.getParent()).getItems().get(1).setText("Invalid");
invalid=false;
}
else
{
symbol.setVisible(false);
}
}
// Beloc code removes all the legends
//lineChart.setLegendVisible(false);
for (XYChart.Series<Number, Number> s : lineChart.getData()) {
if(("Valid").equals(s.getName()))
{
s.getNode().setStyle("-fx-stroke: #0000FF; ");
}
else
{
s.getNode().setStyle("-fx-stroke: #FF0000; ");
}
for (XYChart.Data<Number, Number> d : s.getData()) {
Tooltip.install(d.getNode(), new Tooltip("Frequency: "+
d.getXValue()+ " THz, Power: "+
d.getYValue().doubleValue()+" unit"));
}
}
}
public static void main(String[] args) {
launch(args);
}
}
BarChart.css contains are as below:-
.default-color0.chart-legend-item-symbol{
-fx-background-color: #0000FF;
}
.default-color1.chart-legend-item-symbol{
-fx-background-color: #FF0000;
}
Please help me to remove legends or shrink the components where legends are been added. Thanks alot
Since you are already dealing with Legend, you can work with its items, removing those you don't need, so the legend shows only two items.
Using streams, you can mark the first two items as "Valid"/"Invalid" and the rest as "Remove", for instance, and finally you just remove these last items.
private void updateStyleSheet() {
Legend legend = (Legend)lineChart.lookup(".chart-legend");
AtomicInteger count = new AtomicInteger();
legend.getItems().forEach(item->{
if(count.get()==0){
item.setText("Valid");
} else if(count.get()==1){
item.setText("Invalid");
} else {
item.setText("Remove");
}
count.getAndIncrement();
});
legend.getItems().removeIf(item->item.getText().equals("Remove"));
...
}