I am using a logarithmic scale for a graph and I want to be able to make the individual curves a little wider when hovered over and on top of all the other curves as well as the color key's value too. Below is a picture better illustrating what I want to do (with sensitive data redacted)
Is something like this even possible? And if so what direction should I move in to achieve this?
The first of your cases, which is applying the visual changes when the mouse hovers over the curve, is possible by modifying the Node that represents the Series on the chart, which is a Path. You can apply the changes to the stroke of the Path making it darker, wider and bringing in to the front when the mouse enters and reverting them when the mouse leaves
The second, which is applying the visual changes when hovering over the legend items is still possible but it's not as clean a solution, at least it's not in my implementation below. With a Node lookup you can get the items and cast them to either a Label or a LegendItem which expose the graphic and text. I chose Label to avoid using the internal API
See more here: It is a bad practice to use Sun's proprietary Java classes?
With String comparisons between the legend text and series names you can associate the two assuming each series has a name and that it is unique. If it isn't unique you could compare the stroke fills as well as the names
Important: These assumptions limit this approach and so if possible I'd avoid it
SSCCE:
public class DarkerSeriesOnHoverExample extends Application {
#SuppressWarnings("unchecked")
#Override
public void start(Stage primaryStage) throws Exception {
//LogarithmicNumberAxis source: https://stackoverflow.com/a/22424519/5556314
LineChart lineChart = new LineChart(new LogarithmicNumberAxis(1, 1000000), new NumberAxis(0, 2.25, 0.25));
lineChart.setCreateSymbols(false);
//Values guessed from the screen shot
ObservableList<XYChart.Data> seriesData = FXCollections.observableArrayList(new XYChart.Data(1, 2),
new XYChart.Data(10, 2), new XYChart.Data(100, 2), new XYChart.Data(1000, 1.85),
new XYChart.Data(10000, 1.50), new XYChart.Data(100000, 1.20), new XYChart.Data(1000000, 0.9));
ObservableList<XYChart.Data> series2Data = FXCollections.observableArrayList(new XYChart.Data(1, 2),
new XYChart.Data(10, 2), new XYChart.Data(100, 2), new XYChart.Data(1000, 1.60),
new XYChart.Data(10000, 1.25), new XYChart.Data(100000, 0.95), new XYChart.Data(1000000, 0.65));
ObservableList<XYChart.Data> series3Data = FXCollections.observableArrayList(new XYChart.Data(1, 2),
new XYChart.Data(10, 1.85), new XYChart.Data(100, 1.55), new XYChart.Data(1000, 1),
new XYChart.Data(10000, 0.65), new XYChart.Data(100000, 0.5), new XYChart.Data(1000000, 0.45));
ObservableList<XYChart.Series> displayedSeries = FXCollections.observableArrayList(
new XYChart.Series("Series 1", seriesData), new XYChart.Series("Series 2", series2Data),
new XYChart.Series("Series 3", series3Data));
lineChart.getData().addAll(displayedSeries);
Scene scene = new Scene(lineChart, 300, 300);
primaryStage.setScene(scene);
primaryStage.show();
darkenSeriesOnHover(displayedSeries); //Setup for hovering on series (cleaner)
darkenSeriesOnLegendHover(lineChart); //Setup both hovering on series and legend (messier)
}
private void darkenSeriesOnHover(List<XYChart.Series> seriesList){
for(XYChart.Series series : seriesList){
Node seriesNode = series.getNode();
//seriesNode will be null if this method is called before the scene CSS has been applied
if(seriesNode != null && seriesNode instanceof Path){
Path seriesPath = (Path) seriesNode;
Color initialStrokeColor = (Color)seriesPath.getStroke();
double initialStrokeWidth = seriesPath.getStrokeWidth();
seriesPath.setOnMouseEntered(event -> {
updatePath(seriesPath, initialStrokeColor.darker(), initialStrokeWidth*2, true);
});
seriesPath.setOnMouseExited(event -> {
//Reset
updatePath(seriesPath, initialStrokeColor, initialStrokeWidth, false);
});
}
}
}
private void darkenSeriesOnLegendHover(LineChart lineChart){
Set<Node> legendItems = lineChart.lookupAll("Label.chart-legend-item");
List<XYChart.Series> seriesList = lineChart.getData();
//Will be empty if this method is called before the scene CSS has been applied
if(legendItems.isEmpty()){ return; }
for(Node legendItem : legendItems){
Label legend = (Label) legendItem;
XYChart.Series matchingSeries = getMatchingSeriesByName(seriesList, legend.getText());
if(matchingSeries == null){ return; }
Node seriesNode = matchingSeries.getNode();
//seriesNode will be null if this method is called before the scene CSS has been applied
if(seriesNode != null && seriesNode instanceof Path){
Path seriesPath = (Path) seriesNode;
Color initialStrokeColor = (Color)seriesPath.getStroke();
double initialStrokeWidth = seriesPath.getStrokeWidth();
legendItem.setOnMouseEntered(event -> {
updatePath(seriesPath, initialStrokeColor.darker(), initialStrokeWidth*2, true);
});
legendItem.setOnMouseExited(event -> {
//Reset
updatePath(seriesPath, initialStrokeColor, initialStrokeWidth, false);
});
seriesPath.setOnMouseEntered(event -> {
updatePath(seriesPath, initialStrokeColor.darker(), initialStrokeWidth*2, true);
});
seriesPath.setOnMouseExited(event -> {
//Reset
updatePath(seriesPath, initialStrokeColor, initialStrokeWidth, false);
});
}
}
}
private void updatePath(Path seriesPath, Paint strokeColor, double strokeWidth, boolean toFront){
seriesPath.setStroke(strokeColor);
seriesPath.setStrokeWidth(strokeWidth);
if(!toFront){ return; }
seriesPath.toFront();
}
private XYChart.Series getMatchingSeriesByName(List<XYChart.Series> seriesList, String searchParam){
for (XYChart.Series series : seriesList){
if(series.getName().equals(searchParam)){
return series;
}
}
return null;
}
}
Output:
Before vs Hover
Related
I'm a bit of a javafx noob. I want to make it so this button updates the "GraphView" object called "viewGraph", but I don't know how to properly do this because I can't access outside variables in the setOnAction event. I would also want to make startVertice and endVertice outside of the event class as well but I don't know how to do that.
I would think the answer would be to somehow pass the variables in using parameters but I don't know how to do that.
I tried looking stuff up and trying random syntax stuff but I still dont know what to do. It seems simple though.
WeightedGraph<Campus> graph = new WeightedGraph<>(vertices, edges);
Label startCampus = new Label("Starting Campus: ");
Label endCampus = new Label(" Ending Campus: ");
TextField startText = new TextField();
TextField endText = new TextField();
Button displayShortestPathBtn = new Button("Display Shortest Path");
HBox input = new HBox();
input.getChildren().addAll(startCampus, startText, endCampus, endText, displayShortestPathBtn);
input.setPadding(new Insets(25, 0, 0, 0));
VBox vbox = new VBox();
GraphView viewGraph = new GraphView(graph);
vbox.getChildren().addAll(viewGraph, input);
vbox.setPadding(new Insets(50, 50, 25, 50));
displayShortestPathBtn.setOnAction((event) -> {
int startVertice = Integer.parseInt(startText.getText());
int endVertice = Integer.parseInt(endText.getText());
WeightedGraph<Campus>.ShortestPathTree shortestPathGraph = graph.getShortestPath(startVertice);
ArrayList<Integer> path = (ArrayList)shortestPathGraph.getPath(endVertice);
/*
What I want to do:
viewGraph = new GraphView(graph, path);
*/
});
primaryStage.setTitle("Final Exam");
primaryStage.setScene(new Scene(vbox));
primaryStage.show();
I just want to be able to access the outside variables but idk if that's possible or if I'm approaching the problem correctly in the first place.
I placed an HBox into the center of a BorderPane, that is the only direct Node on a DialogPane of my Dialog by using this code:
public GameDialog(Player[] players) {
setTitle("Spiel eintragen");
setWidth(WIDTH);
setHeight(HEIGHT);
createLeftBox();
createRightBox(players);
// Center
this.center = new HBox(leftBox, rightBox);
center.setStyle("-fx-border-color: #00ff00;");
center.getChildren()
.forEach(b -> HBox.setMargin(b, INSETS));
// Bottom
this.btnOk = new Button("ok");
this.btnCancel = new Button("abbrechen");
this.bottom = new HBox(btnOk, btnCancel);
bottom.getChildren()
.forEach(b -> HBox.setMargin(b, INSETS));
// Pane
this.pane = new BorderPane(center);
pane.setBottom(bottom);
pane.setPrefSize(WIDTH, HEIGHT);
getDialogPane().setPrefSize(WIDTH, HEIGHT);
getDialogPane().getChildren().add(pane);
...
}
private void createLeftBox() {
// Points spinner
List<Integer> points = Stream.iterate(0, Util::increment)
.limit(MAX_POINTS)
.collect(Collectors.toList());
this.spPoints = new Spinner<>(observableArrayList(points));
// Solo combobox
List<Solotype> soli = Arrays.asList(Solotype.values());
this.cbSolo = new ComboBox<>(observableArrayList(soli));
cbSolo.setOnAction(e -> changed());
// Bock checkbox
this.chBock = new CheckBox("Bock");
// Left box
leftBox = new VBox(spPoints, cbSolo, chBock);
leftBox.getChildren()
.forEach(n -> VBox.setMargin(n, INSETS));
leftBox.setStyle("-fx-border-color: #ff0000");
}
private void createRightBox(Player[] players) {
List<Player> playerlist = Arrays.asList(players);
// Right box
rightBox = new VBox();
List<Node> rightChildren = rightBox.getChildren();
this.playerBoxes = playerlist.stream()
.collect(toMap(p -> p,
p -> new HBox(4)));
this.players = playerlist.stream()
.collect(toMap(p -> p,
this::createToggleGroup));
playerBoxes.values()
.stream()
.peek(rightChildren::add)
.forEach(b -> VBox.setMargin(b, INSETS));
rightBox.setStyle("-fx-border-color: #ff0000");
}
My class GameDialog extends the javafx-class Dialog<R>. However the HBox in the center of the BorderPane doesn't fill all of the available space. It just looks like this:
I tried to setMinWidth, setMinHeight, setPrefSize, setFillWidth(true) and setFillHeight(true) on several of the H- and VBoxes. Nothing Changed. I just don't understand this behaviour. Any ideas?
I couldn't fully recreate your example (since some code is missing, I used TextField instead of those toggle groups), but had some success with this solution.
Instead of using:
getDialogPane().getChildren().add(pane);
use:
getDialogPane().setContent(pane);
And the controls should use all the available space.
(I also removed the setting of the preferred width and height, so the image was a little smaller. Setting the preferred size of the pane doesn't change anything any ways.)
FeathersPerSec fps = new FeathersPerSec();
public void start(Stage primaryStage)
{
double getFeathers = fps.getNumOfFeathers();
//double setFeathers = fps.setNumOfFeathers(1);
double click = 1;
GridPane pane = new GridPane();
Image image = new Image("dancingChicken.gif");
Button chickenClick = new Button();
Label feathers = new Label("FPS " + click);
chickenClick.setGraphic(new ImageView(image));
chickenClick.setOnAction((e) -> {
click++;
feathers = new Label("FPS" + click);
});
heres a pieceof my program, im trying to make a cookie clicker type game. except with a chicken and feathers. Im trying to make it so when the user clicks the image button the number of feathers will increment by one. Im not sure what I should put in the setOnAction. with my current set up I keep getting a "Local Variable feathers defined in an enclosing scope must be final or effectively final."
You need to update the text of the current label, instead of creating a new label (which isn't part of your scene graph).
Additionally, you can't change a local variable inside a lambda expression (local variables accessed in a lambda expression must be final or effectively final (meaning they can only be assigned a value once)). The simplest fix for this is to move click to an instance variable:
private FeathersPerSec fps = new FeathersPerSec();
private double click ;
public void start(Stage primaryStage)
{
double getFeathers = fps.getNumOfFeathers();
//double setFeathers = fps.setNumOfFeathers(1);
click = 1;
GridPane pane = new GridPane();
Image image = new Image("dancingChicken.gif");
Button chickenClick = new Button();
Label feathers = new Label("FPS " + click);
chickenClick.setGraphic(new ImageView(image));
chickenClick.setOnAction((e) -> {
click++;
feathers.setText("FPS" + click);
});
}
I've got a little problem with my JavaFX code. I'm sure you all know that you can get the input from a TextInputDialog with an Optional< String > and .showAndWait(). But what should I do when I have a custom dialog with multiple TextFields and a ChoiceBox? How do I get the results from all of them when clicking OK? I thought about a List<String> but I didn't manage to do it..
Code (Custom Dialog):
public class ImageEffectInputDialog extends Dialog {
private ButtonType apply = new ButtonType("Apply", ButtonBar.ButtonData.OK_DONE);
private ButtonType cancel = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
public ImageEffectInputDialog(String title) {
setTitle(title);
setHeaderText(null);
GridPane dPane = new GridPane();
Label offsetX = new Label("Offset X: ");
Label offsetY = new Label("Offset Y: ");
Label color = new Label("Shadow Color: ");
TextField offsetXText = new TextField();
TextField offsetYText = new TextField();
ChoiceBox<String> shadowColors = new ChoiceBox<>();
shadowColors.getItems().add(0, "Black");
shadowColors.getItems().add(1, "White");
dPane.setHgap(7D);
dPane.setVgap(8D);
GridPane.setConstraints(offsetX, 0, 0);
GridPane.setConstraints(offsetY, 0, 1);
GridPane.setConstraints(offsetXText, 1, 0);
GridPane.setConstraints(offsetYText, 1, 1);
GridPane.setConstraints(color, 0, 2);
GridPane.setConstraints(shadowColors, 1, 2);
dPane.getChildren().addAll(offsetX, offsetY, color, offsetXText, offsetYText, shadowColors);
getDialogPane().getButtonTypes().addAll(apply, cancel);
getDialogPane().setContent(dPane);
}
}
Code (where I want the results)
if(scrollPane.getContent() != null && scrollPane.getContent() instanceof ImageView) {
// ImageEffectUtil.addDropShadow((ImageView) scrollPane.getContent());
ImageEffectInputDialog drop = new ImageEffectInputDialog("Drop Shadow");
//Want the Results here..
}
I hope someone might be able to help.
First of all, in order to obtain different values of different types (generic solution) just define a new data structure, say Result, which contains fields like offsetX, offsetY and whatever else you need. Next, extend Dialog<Result> instead of just Dialog. Finally, in the constructor of your ImageEffectInputDialog you need to set result converter, as follows:
setResultConverter(button -> {
// here you can also check what button was pressed
// and return things accordingly
return new Result(offsetXText.getText(), offsetYText.getText());
});
Now wherever you need to use the dialog, you can do:
ImageEffectInputDialog dialog = new ImageEffectInputDialog("Title");
dialog.showAndWait().ifPresent(result -> {
// do something with result object, which is of type Result
});
I am having an issue with using a scrollpane in libgdx. It is going to be used for a chatwindow class. When you press enter the message will be added to the window and you will scroll to the latest posted message..However it doesn't. It misses one message and scrolls to the one before the latest message. Below I've posted the chatwindow class and the method that adds input to it. The textAreaholder is a table that holds everything. The chatField is where you input what you want to post to the chat. The chatarea is the textfield that then becomes added to the table. But as stated..it doesn't scroll properly, the error properly lies somewhere in the keyTyped method.
public ChatWindow(final Pipe<String> chatPipe) {
this.chatPipe = chatPipe;
messageFieldCounter = 0;
white = new BitmapFont(Gdx.files.internal("fonts/ChatWindowText.fnt"), false);
fontSize = white.getLineHeight();
white.scale(TEXT_SCALE);
final TextFilter filter = new TextFilter();
/* Making a textfield style */
textFieldStyle = new TextFieldStyle();
textFieldStyle.fontColor = Color.WHITE;
textFieldStyle.font = white;
textFieldStyle.focusedFontColor = Color.CYAN;
/*Area where all chat appears*/
textAreaHolder = new Table();
textAreaHolder.debug();
/*Applies the scrollpane to the chat area*/
scrollPane = new ScrollPane(textAreaHolder);
scrollPane.setForceScroll(false, true);
scrollPane.setFlickScroll(true);
scrollPane.setOverscroll(false, false);
/*Input chat*/
chatField = new TextField("", textFieldStyle);
chatField.setTextFieldFilter(filter);
/*Tries to make the textField react on enter?*/
chatField.setTextFieldListener(new TextFieldListener() {
#Override
public void keyTyped(final TextField textField, final char key) {
if (key == '\n' || key == '\r') {
if (messageFieldCounter <= 50) {
textAreaHolder.row();
StringBuilder message = new StringBuilder(); //Creates the message
message.append(chatField.getText()); //Appends the chatfield entry
TextArea chatArea = new TextArea(message.toString(), textFieldStyle); //Creates a chatArea with the message
chatArea.setHeight(fontSize + 1);
chatArea.setDisabled(true);
chatArea.setTextFieldFilter(filter);
textAreaHolder.add(chatArea).height(CHAT_INPUT_HEIGHT).width(CHAT_WIDTH);
scrollPane.scrollToCenter(0, 0, 0, 0);
//Scrolls to latest input
chatField.setText("");
//InputDecider.inputDecision(message.toString(), chatPipe); //TODO: Change the filter
//chatPipe.put(message.toString()); //TODO: testing
}
}
}
});
Problems could occur, because you're using scrollPane.scrollToCenter(float x, float y, float width, float height) with zero parameters:
scrollPane.scrollToCenter(0, 0, 0, 0);
scrollToCenter method requires that parameters to be correctly supplied. So, try to supply message bounds.
The second reason could be because you call scrollToCenter before table do layout itself. So, try overwrite table's layout method and call scrollToCenter after:
#Override
public void layout()
{
super.layout();
if (new_messages_added)
{
scrollPane.scrollToCenter(...)
}
}