The trigger for my experiment was a recent question - one cell in a row should visualize the relative proportion of values in several cell values in the same row. In fx, such a visualization is supported in StackedBarChart (degenerate to a single category and with yAxis being the category axis).
Unfortunately, using such a chart as cell graphics has weird effects when updating the item, depending on how we do the update:
scenario A: initialize the chart with the series and update the data in the series. The bars appear fine only on the very first showing, scrolling back and forth leaves random "gaps" inside
scenario B: create and set new series in each round. The bars seem to have the correct width, but their colors changes randomly on scrolling
Also, there are minor visual quirks f.i. can't find a way to restrict the height of the cell as needed.
The questions:
how to make it work correctly, or
what's wrong, which part of the rendering mechanism interfers?
The example:
import javafx.application.Application;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.StackedBarChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
/**
* from SO, how to show relative bars with colors of
* a related chart
*
* https://stackoverflow.com/a/28141421/203657
*
* That's a solution with manually calculating and
* filling a rectangle with base chart colors
*
* Here trying to use StackedBarChart .. problems as noted in cell doc.
* Extracted TableStackedBarChart for SO question.
*/
public class TableStackedBar extends Application {
public static void main(String[] args) { launch(args); }
#Override
public void start(Stage stage) {
ObservableList<Data> data = FXCollections.observableArrayList();
for (int i = 0; i<10; i++) data.add(new Data());
TableView<Data> tv = new TableView<>(data);
TableColumn<Data, Number> col1 = new TableColumn<>("num1");
TableColumn<Data, Number> col2 = new TableColumn<>("num2");
col1.setCellValueFactory((p)->{return p.getValue().num1;});
col2.setCellValueFactory((p)->{return p.getValue().num2;});
//make this column hold the entire Data object so we can access all fields
TableColumn<Data, Data> col3 = new TableColumn<>("bar");
col3.setPrefWidth(500);
col3.setCellValueFactory((p)->{return new ReadOnlyObjectWrapper<>(p.getValue());});
col3.setCellFactory(p -> new StackedBarChartCell(2000.));
tv.getColumns().addAll(col1,col2,col3);
tv.setFixedCellSize(50.);
Scene scene = new Scene(tv);
stage.setScene(scene);
stage.show();
}
/**
* TableCell that uses a StackedBarChart to visualize relation of
* data.
*
* Problems with updating items:
* - scenario A: updating the series leaves empty patches horizontally
* - scenario B: re-setting the series changes colors randomly
*
* Other problems
* - runs amok without fixedCellSize on tableView
* - can't max the height of the chart (so it's cut-off in the middle
*/
public static class StackedBarChartCell extends TableCell<Data, Data> {
NumberAxis xAxisHoriz = new NumberAxis();
CategoryAxis yAxisHoriz = new CategoryAxis();
StackedBarChart<Number, String> sbcHoriz = new StackedBarChart<>(xAxisHoriz, yAxisHoriz);
XYChart.Series<Number, String> series1Horiz = new XYChart.Series<>();
XYChart.Series<Number, String> series2Horiz = new XYChart.Series<>();
public StackedBarChartCell(double upperBound) {
yAxisHoriz.setTickLabelsVisible(false);
yAxisHoriz.setTickMarkVisible(false);
yAxisHoriz.setStyle("-fx-border-color: transparent transparent transparent transparent;");
xAxisHoriz.setTickLabelsVisible(false);
xAxisHoriz.setTickMarkVisible(false);
xAxisHoriz.setMinorTickVisible(false);
xAxisHoriz.setStyle("-fx-border-color: transparent transparent transparent transparent;");
xAxisHoriz.setAutoRanging(false);
xAxisHoriz.setUpperBound(upperBound);
xAxisHoriz.setLowerBound(0.);
sbcHoriz.setHorizontalGridLinesVisible(false);
sbcHoriz.setVerticalGridLinesVisible(false);
sbcHoriz.setLegendVisible(false);
sbcHoriz.setAnimated(false);
// scenario A: set series initially
sbcHoriz.getData().setAll(series1Horiz, series2Horiz);
sbcHoriz.setCategoryGap(0);
// no effect
sbcHoriz.setMaxHeight(20);
}
#Override
protected void updateItem(Data item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(sbcHoriz);
// scenario B: set new series
// uncomment for scenario A
// XYChart.Series<Number, String> series1Horiz = new XYChart.Series<>();
// XYChart.Series<Number, String> series2Horiz = new XYChart.Series<>();
// sbcHoriz.getData().setAll(series1Horiz, series2Horiz);
//---- end of scenario B
series1Horiz.getData().setAll(new XYChart.Data(item.num1.get(), "none"));
series2Horiz.getData().setAll(new XYChart.Data(item.num2.get(), "none"));
}
}
}
private static class Data{
private SimpleIntegerProperty num1 = new SimpleIntegerProperty((int)(Math.random()*1000));
private SimpleIntegerProperty num2 = new SimpleIntegerProperty((int)(Math.random()*1000));
public SimpleIntegerProperty num1Property(){return num1;}
public SimpleIntegerProperty num2Property(){return num2;}
}
}
Update: seems to be a regression in 8u40 - works for 8u20/25, not for 8u40b20. Reported as RT-39884
Here's just where I copied stuff into my CellFactory
col3.setCellFactory((TableColumn<Data, Data> param) -> {
return new TableCell<Data, Data>() {
#Override
protected void updateItem(Data item, boolean empty) {
super.updateItem(item, empty);
if (empty) setGraphic(null);
else {
super.updateItem(item, empty);
if (item == null || empty) {
setGraphic(null);
} else {
NumberAxis xAxisHoriz = new NumberAxis(0, 2000, 1000);
CategoryAxis yAxisHoriz = new CategoryAxis(FXCollections.observableArrayList(""));
XYChart.Series<Number, String> series1Horiz = new XYChart.Series<>();
XYChart.Series<Number, String> series2Horiz = new XYChart.Series<>();
StackedBarChart<Number, String> sbcHoriz = new StackedBarChart<>(xAxisHoriz, yAxisHoriz);
sbcHoriz.getData().setAll(series1Horiz, series2Horiz);
yAxisHoriz.setStyle("-fx-border-color: transparent transparent transparent transparent;"
+ "-fx-tick-labels-visible: false;"
+ "-fx-tick-mark-visible: false;"
+ "-fx-minor-tick-visible: false;"
+ "-fx-padding: 0 0 0 0;");
xAxisHoriz.setStyle("-fx-border-color: transparent transparent transparent transparent;"
+ "-fx-tick-labels-visible: false;"
+ "-fx-tick-mark-visible: false;"
+ "-fx-minor-tick-visible: false;"
+ "-fx-padding: 0 0 0 0;");
sbcHoriz.setHorizontalGridLinesVisible(false);
sbcHoriz.setVerticalGridLinesVisible(false);
sbcHoriz.setLegendVisible(false);
sbcHoriz.setAnimated(false);
xAxisHoriz.setMaxWidth(100);
sbcHoriz.setMaxWidth(100);
sbcHoriz.setPadding(Insets.EMPTY);
sbcHoriz.setCategoryGap(0);
setGraphic(sbcHoriz);
series1Horiz.getData().setAll(new XYChart.Data(item.num1.get(), ""));
series2Horiz.getData().setAll(new XYChart.Data(item.num2.get(), ""));
}
}
}
};
});
and also after I set this tv.setFixedCellSize(30);
I also had to change the column width to 200, I can't make the chart smaller.
Related
I would like to add such kind of bar chart to my application using JavaFX:
Essentially: A (potentially large, i.e. up to 50 entries) table. For each row there are several columns with information. One piece of information are percentages about win/draw/loss ratio, i.e. say three numbers 10%, 50%, 40%. I would like to display these three percentages graphically as a vertical bar, with three different colors. So that a user can get a visual impression of each of these percentages.
I have not found a simple or straight-forward method of doing that with JavaFX. There seems at least no control for that right now. I also could not find a control from ControlsFX that seemd suitable. What I am curently doing is having the information itself, and three columns for the percentages like this:
Option Win Draw Loss
============================
option1 10% 50% 40%
option2 20% 70% 10%
option3 ...
But that's just not so nice. How can I achieve the above mentioned graphical kind of display?
(added an image for better understanding; it's from the lichess.org where they do exactly that in html)
This uses a combination of trashgod's and James_D's ideas:
a TableView with a custom cell factory and graphic,
The graphic could just be three appropriately-styled labels in a single-row grid pane with column constraints set.
Other than that, it is a standard table view implementation.
Numbers in my example don't always add up to 100% due to rounding, so you may wish to do something about that, if so, I leave that up to you.
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.collections.*;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.text.NumberFormat;
import java.util.Arrays;
public class ChartTableApp extends Application {
private final ObservableList<Outcomes> outcomes = FXCollections.observableList(
Arrays.asList(
new Outcomes("Qxd5", 5722, 5722, 3646),
new Outcomes("Kf6", 2727, 2262, 1597),
new Outcomes("c6", 11, 1, 5),
new Outcomes("e6", 0, 1, 1)
)
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
stage.setScene(new Scene(createOutcomesTableView()));
stage.show();
}
private TableView<Outcomes> createOutcomesTableView() {
final TableView<Outcomes> outcomesTable = new TableView<>(outcomes);
TableColumn<Outcomes, String> moveCol = new TableColumn<>("Move");
moveCol.setCellValueFactory(o ->
new SimpleStringProperty(o.getValue().move())
);
TableColumn<Outcomes, Integer> totalCol = new TableColumn<>("Total");
totalCol.setCellValueFactory(o ->
new SimpleIntegerProperty(o.getValue().total()).asObject()
);
totalCol.setCellFactory(p ->
new IntegerCell()
);
totalCol.setStyle("-fx-alignment: BASELINE_RIGHT;");
TableColumn<Outcomes, Outcomes> outcomesCol = new TableColumn<>("Outcomes");
outcomesCol.setCellValueFactory(o ->
new SimpleObjectProperty<>(o.getValue())
);
outcomesCol.setCellFactory(p ->
new OutcomesCell()
);
//noinspection unchecked
outcomesTable.getColumns().addAll(
moveCol,
totalCol,
outcomesCol
);
outcomesTable.setPrefSize(450, 150);
return outcomesTable;
}
public record Outcomes(String move, int wins, int draws, int losses) {
public int total() { return wins + draws + losses; }
public double winPercent() { return percent(wins); }
public double drawPercent() { return percent(draws); }
public double lossPercent() { return percent(losses); }
private double percent(int value) { return value * 100.0 / total(); }
}
private static class OutcomesCell extends TableCell<Outcomes, Outcomes> {
OutcomesBar bar = new OutcomesBar();
#Override
protected void updateItem(Outcomes item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
bar.setOutcomes(item);
setGraphic(bar);
}
}
}
private static class OutcomesBar extends GridPane {
private final Label winsLabel = new Label();
private final Label drawsLabel = new Label();
private final Label lossesLabel = new Label();
private final ColumnConstraints winsColConstraints = new ColumnConstraints();
private final ColumnConstraints drawsColConstraints = new ColumnConstraints();
private final ColumnConstraints lossesColConstraints = new ColumnConstraints();
public OutcomesBar() {
winsLabel.setStyle("-fx-background-color : lightgray");
drawsLabel.setStyle("-fx-background-color : darkgray");
lossesLabel.setStyle("-fx-background-color : gray");
winsLabel.setAlignment(Pos.CENTER);
drawsLabel.setAlignment(Pos.CENTER);
lossesLabel.setAlignment(Pos.CENTER);
winsLabel.setMaxWidth(Double.MAX_VALUE);
drawsLabel.setMaxWidth(Double.MAX_VALUE);
lossesLabel.setMaxWidth(Double.MAX_VALUE);
addRow(0, winsLabel, drawsLabel, lossesLabel);
getColumnConstraints().addAll(
winsColConstraints,
drawsColConstraints,
lossesColConstraints
);
}
public void setOutcomes(Outcomes outcomes) {
winsLabel.setText((int) outcomes.winPercent() + "%");
drawsLabel.setText((int) outcomes.drawPercent() + "%");
lossesLabel.setText((int) outcomes.lossPercent() + "%");
winsColConstraints.setPercentWidth(outcomes.winPercent());
drawsColConstraints.setPercentWidth(outcomes.drawPercent());
lossesColConstraints.setPercentWidth(outcomes.lossPercent());
}
}
private static class IntegerCell extends TableCell<Outcomes, Integer> {
#Override
protected void updateItem(Integer item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
setText(
NumberFormat.getNumberInstance().format(
item
)
);
}
}
}
}
I need to write a custom pane that behaves like an infinite two-dimensional cartesian coordinate system. When first showing I want 0,0 to be in the center of the pane. The user should be able to navigate the pane by holding down the left mouse button and dragging. It needs to have the ability to zoom in and out. I also have to be able to place nodes at specific coordinates.
Of course I am aware that this is a very specific control and I am not asking anyone to give me step-by-step instructions or write it for me.
I am just new to the world of JFX custom controls and don't know how to approach this problem, especially the whole infinity thing.
This is not so difficult to achieve as you may think. Just start with a simple Pane. That already gives you the infinte coordinate system. The only difference from your requirement is that the point 0/0 is in the upper left corner and not in the middle. This can be fixed by applying a translate transform to the pane. Zooming and panning can then be achieved in a similar way by adding the corresponding mouse listeners to the Pane.
One approach is to render arbitrary content in a Canvas, as suggested here. The corresponding GraphicsContext gives you maximum control of the coordinates. As a concrete example, jfreechart renders charts using jfreechart-fx, whose ChartViewer holds a ChartCanvas that extends Canvas. Starting from this example, the variation below sets the domain axis to span an interval centered on zero after adding corresponding points to the three series. Use the mouse wheel or context menu to zoom; see this related answer for more on zooming and panning.
for (double t = -3; t <= 3; t += 0.5) {
series.add(t, Math.sin(t) + i);
}
…
xAxis.setRange(-Math.PI, Math.PI);
…
plot.setDomainPannable(true);
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.entity.ChartEntity;
import org.jfree.chart.entity.LegendItemEntity;
import org.jfree.chart.entity.XYItemEntity;
import org.jfree.chart.fx.ChartViewer;
import org.jfree.chart.fx.interaction.ChartMouseEventFX;
import org.jfree.chart.fx.interaction.ChartMouseListenerFX;
import org.jfree.chart.labels.StandardXYToolTipGenerator;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
/**
* #see https://stackoverflow.com/a/44967809/230513
* #see https://stackoverflow.com/a/43286042/230513
*/
public class VisibleTest extends Application {
#Override
public void start(Stage stage) {
XYSeriesCollection dataset = new XYSeriesCollection();
for (int i = 0; i < 3; i++) {
XYSeries series = new XYSeries("value" + i);
for (double t = -3; t <= 3; t += 0.5) {
series.add(t, Math.sin(t) + i);
}
dataset.addSeries(series);
}
NumberAxis xAxis = new NumberAxis("domain");
xAxis.setRange(-Math.PI, Math.PI);
NumberAxis yAxis = new NumberAxis("range");
XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(true, true);
renderer.setBaseToolTipGenerator(new StandardXYToolTipGenerator());
XYPlot plot = new XYPlot(dataset, xAxis, yAxis, renderer);
JFreeChart chart = new JFreeChart("Test", plot);
ChartViewer viewer = new ChartViewer(chart);
viewer.addChartMouseListener(new ChartMouseListenerFX() {
#Override
public void chartMouseClicked(ChartMouseEventFX e) {
ChartEntity ce = e.getEntity();
if (ce instanceof XYItemEntity) {
XYItemEntity item = (XYItemEntity) ce;
renderer.setSeriesVisible(item.getSeriesIndex(), false);
} else if (ce instanceof LegendItemEntity) {
LegendItemEntity item = (LegendItemEntity) ce;
Comparable key = item.getSeriesKey();
renderer.setSeriesVisible(dataset.getSeriesIndex(key), false);
} else {
for (int i = 0; i < dataset.getSeriesCount(); i++) {
renderer.setSeriesVisible(i, true);
}
}
}
#Override
public void chartMouseMoved(ChartMouseEventFX e) {}
});
stage.setScene(new Scene(viewer));
stage.setTitle("JFreeChartFX");
stage.setWidth(640);
stage.setHeight(480);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I am trying to resize a particular cell in TableView in a particular Row.
The code i use,
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
import javafx.util.Callback;
public class TableColors extends Application {
TableView<String> tView = new TableView<>();
#Override
public void start(Stage primaryStage) {
TableColumn<String, String> tcName = new TableColumn("name");
TableColumn<String, String> tcName1 = new TableColumn("name1");
ObservableList<String> items = FXCollections.observableArrayList("One", "Two", "Three", "Four");
tView.setItems(items);
tView.getColumns().add(tcName);
tView.getColumns().add(tcName1);
tView.setColumnResizePolicy((param) -> true);
Platform.runLater(() -> customResize());
tcName.setCellValueFactory((p) -> new SimpleStringProperty(p.getValue()));
tcName.setCellFactory(new Callback<TableColumn<String, String>, TableCell<String, String>>() {
#Override
public TableCell call(TableColumn p) {
return new TableCell<String, String>() {
#Override
public void updateItem(final String item, final boolean empty) {
super.updateItem(item, empty);//*don't forget!
if (item != null) {
setText(item);
setAlignment(Pos.CENTER);
} else {
setText(null);
}
}
};
}
});
Scene scene = new Scene(tView, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
private void customResize() {
try {
TableColumn<?, ?> col = tView.getColumns().get(0);
col.setPrefWidth(150);
} catch (Exception r) {
r.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
when i run this complete name column size is increased(as expected).
Instead i want to increase only the size of the cell which contains "Three".
Am not sure ,What code should i incorporate to make it work in this way.
How can i solve this?
you can try this logic
Let your cell DataHolder implement something like this
public class DataHolder{
boolean cellOverlaps = false; //tells the cell whether its string overlaps others
int rowPos = -1;// the vertical position
int columnPos =-1; //its column position
boolean startPoint = false;//whether its the start point
//in other words it begins from here hence its right border we care
String text = ""
}
So what you do basically, is in your updateIndex(), or updateItem() methods or your Commit() method of your cell, (use commit()) you calculate the width of the cell by Cell.prefWidth(-1) or Cell.getWidth(), after you get the space your text is occupying in your first cell, compare that to your width if its > then you trim the text and make it overlap. here you have something like
dataHolder.cellOverlaps = true;
dataHolder.startPoint = true;//other overlapping cell will have this false
dataHolder.rowPost = getIndex();
dataHolder.columnPos = getTableView().getColumns().indexof(getTableColumn());
so now you look for cells in row getIndex() with columnPos > the startPoint
and you change the styles of the cell for example the starting cell with have a transparent border right color
-fx-border-color: black black black transparent;
now the other overlaping celss will have the left transparent
-fx-border-color: black transparent black black;
and you set the text of the remaining string to the cell. if possible change the background of the TableRow so it matches your cell hence you do not get to see the divider line.
try it and see, wanted to drop a demo but do not have much time though.
Hope if helps
At work I created a TableView that needs to have specific cells flash from one color to the other simultaneously. This is relatively easy using Rectangles, FillTransitions, and a ParallelTransition as shown in the toy example below. After assigning the rectangle to the FillTransition, I set the TableCell's graphic to the rectangle. Then I just had to add/remove the FillTransition from the ParallelTransition depending on whether or not the cell should be blinking.
An area where I had a lot of difficulty, however, was in figuring out a way to scale the rectangle to the size of the TableCell containing it as a graphic. The problem I had was that the TableCell would always resize itself to have empty space between its boundaries and the boundaries of the rectangle.
I had to solve this in a very tedious and round-about way: I had to call setFixedCellSize to fix the table's cell height to whatever my rectangle's height was, reposition the rectangle up and to the left by trial and error through calling its setTranslateX/Y, and set the minwidth and minheight of the column to slightly less than whatever my rectangle's width and height was set to. It solved the problem, but I would've hoped for something a little less tedious and annoying.
I would have assumed this could be avoided by doing one or more of the following with the cell:
Calling setScaleShape(true)
Calling setContentDisplay(ContentDisplay.GRAPHIC_ONLY)
Setting the cell's CSS style to include "-fx-scale-shape: true"
Sadly, none of these had any noticeable effect...
My question is a three-parter:
Is there a better way to size a Shape assigned as a graphic for a Cell to fill the boundaries of the Cell?
Why would none of the three methods above have any effect in my case and what is their actual intended purpose? Do they only apply for a shape assigned with setShape() as opposed to setGraphic()?
Are there any legitimate reasons why JavaFx wouldn't support setting the preferred width or height of Nodes other than those that subclass Region? Autosizing seems like something that should be universal to all Nodes in the hierarchy, and it seems intuitive that any Parent node should be able to dictate the size of its children when necessary.
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javafx.animation.Animation;
import javafx.animation.FillTransition;
import javafx.animation.ParallelTransition;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class FlashingPriorityTable extends Application {
public static void main(String args[]) {
FlashingPriorityTable.launch();
}
#Override
public void start(Stage primaryStage) throws Exception {
// periodically add prioritized items to an observable list
final ObservableList<PItem> itemList = FXCollections.observableArrayList();
class ItemAdder {
private int state, count = 0; private final int states = 3;
public synchronized void addItem() {
state = count++ % states;
PItem item;
if(state == 0)
item = new PItem(Priority.LOW, count, "bob saget");
else if(state == 1)
item = new PItem(Priority.MEDIUM, count, "use the force");
else
item = new PItem(Priority.HIGH, count, "one of us is in deep trouble");
itemList.add(item);
}
};
final ItemAdder itemAdder = new ItemAdder();
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(
() -> itemAdder.addItem(),
0, // initial delay
1, // period
TimeUnit.SECONDS); // time unit
// set up a table view bound to the observable list
final TableColumn<PItem, Priority> priCol = new TableColumn<>("Priority");
priCol.setCellValueFactory(new PropertyValueFactory<PItem, Priority>("priority"));
priCol.setCellFactory((col) -> new PriorityCell()); // create a blinking cell
priCol.setMinWidth(50);
priCol.setMaxWidth(50);
final TableColumn<PItem, Integer> indexCol = new TableColumn<>("Index");
indexCol.setCellValueFactory(new PropertyValueFactory<PItem, Integer>("index"));
indexCol.setCellFactory((col) -> makeBorderedTextCell());
final TableColumn<PItem, String> descriptionCol = new TableColumn<>("Description");
descriptionCol.setCellValueFactory(new PropertyValueFactory<PItem, String>("description"));
descriptionCol.setCellFactory((col) -> makeBorderedTextCell());
descriptionCol.setMinWidth(300);
final TableView<PItem> table = new TableView<>(itemList);
table.getColumns().setAll(priCol, indexCol, descriptionCol);
table.setFixedCellSize(25);
// display the table view
final Scene scene = new Scene(table);
primaryStage.setScene(scene);
primaryStage.show();
}
// render a simple cell text and border
private <T> TableCell<PItem, T> makeBorderedTextCell() {
return new TableCell<PItem, T>() {
#Override protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if(item == null || empty) {
setText(null);
} else {
setBorder(new Border(new BorderStroke(Color.GREEN, BorderStrokeStyle.SOLID, null, null)));
setText(item.toString());
}
}
};
}
/* for cells labeled as high priority, render an animation that blinks (also include a border) */
public static class PriorityCell extends TableCell<PItem, Priority> {
private static final ParallelTransition pt = new ParallelTransition();
private final Rectangle rect = new Rectangle(49.5, 24);
private final FillTransition animation = new FillTransition(Duration.millis(100), rect);
public PriorityCell() {
rect.setTranslateX(-2.75);
rect.setTranslateY(-2.7);
animation.setCycleCount(Animation.INDEFINITE); animation.setAutoReverse(true); }
#Override
protected void updateItem(Priority priority, boolean empty) {
super.updateItem(priority, empty);
if(priority == null || empty) {
setGraphic(null);
return;
}
setGraphic(rect);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setBorder(new Border(new BorderStroke(Color.GREEN, BorderStrokeStyle.SOLID, null, null)));
if(priority == Priority.HIGH) {
if(!pt.getChildren().contains(animation)) {
animation.setFromValue(Color.BLACK);
animation.setToValue(priority.getColor());
animation.setShape(rect);
pt.getChildren().add(animation);
pt.stop(); pt.play();
}
} else {
if(pt.getChildren().contains(animation)) {
pt.getChildren().remove(animation);
pt.stop(); pt.play();
}
rect.setFill(priority.getColor());
}
}
}
/* an item that has a priority assigned to it */
public static class PItem {
private ObjectProperty<Priority> priority = new SimpleObjectProperty<>();
private IntegerProperty index = new SimpleIntegerProperty();
private StringProperty description = new SimpleStringProperty();
public PItem(Priority priority, Integer index, String description) {
setPriority(priority); setIndex(index); setDescription(description);
}
public void setPriority(Priority priority_) { priority.set(priority_); }
public Priority getPriority() { return priority.get(); }
public void setIndex(int index_) { index.set(index_); }
public Integer getIndex() { return index.get(); }
public void setDescription(String description_) { description.set(description_); }
public String getDescription() { return description.get(); }
}
/* a priority */
public enum Priority {
HIGH(Color.RED), MEDIUM(Color.ORANGE), LOW(Color.BLUE);
private final Color color;
private Priority(Color color) { this.color = color; }
public Color getColor() { return color; }
}
}
Regarding:
the TableCell would always resize itself to have empty space between its boundaries and the boundaries of the rectangle.
This is because the cell has by default 2 px of padding, according to modena.css:
.table-cell {
-fx-padding: 0.166667em; /* 2px, plus border adds 1px */
-fx-cell-size: 2.0em; /* 24 */
}
One easy way to get rid of this empty space is just override it:
#Override
protected void updateItem(Priority priority, boolean empty) {
super.updateItem(priority, empty);
...
setGraphic(rect);
setStyle("-fx-padding: 0;");
...
}
The next problem you also mention is autosizing. According to JavaDoc, for Node.isResizable():
If this method returns true, then the parent will resize the node (ideally within its size range) by calling node.resize(width,height) during the layout pass. All Regions, Controls, and WebView are resizable classes which depend on their parents resizing them during layout once all sizing and CSS styling information has been applied.
If this method returns false, then the parent cannot resize it during layout (resize() is a no-op) and it should return its layoutBounds for minimum, preferred, and maximum sizes. Group, Text, and all Shapes are not resizable and hence depend on the application to establish their sizing by setting appropriate properties (e.g. width/height for Rectangle, text on Text, and so on). Non-resizable nodes may still be relocated during layout.
Clearly, a Rectangle is not resizable, but this doesn't mean you can't resize it: if the layout doesn't do it for you, you'll need to take care of it.
So one easy solution may be binding the dimensions of the rectangle to those of the cell (minus 2 pixels for the cell borders):
private final Rectangle rect = new Rectangle();
#Override
protected void updateItem(Priority priority, boolean empty) {
super.updateItem(priority, empty);
if(priority == null || empty) {
setGraphic(null);
return;
}
setGraphic(rect);
setStyle("-fx-padding: 0;");
rect.widthProperty().bind(widthProperty().subtract(2));
rect.heightProperty().bind(heightProperty().subtract(2));
...
}
Note that you won't need to translate the rectangle, and it won't be necessary to fix the size of the cell nor the width of the column, unless you want to give it a fixed size.
Note also that setShape() is intended to change the cell shape, that by default is already a rectangle.
This may answer your first two questions. For the third one, sometimes you wish the nodes were always resizable... but if that were the case we will have the opposite problem, trying to keep them constrained...
In the java doc it says for setMouseTransparent that is affects all children as well as the parent.
How can it be made so only the parent's transparent areas (can see other nodes below it but not responding to mouse events) are transparent to mouse events so that the nodes below it may receive them.
This happens when stacking two XYCharts in the same pane. Only the last one added can receive events.
Set pickOnBounds for the relevant nodes to false, then clicking on transparent areas in a node won't register a click with that node.
Defines how the picking computation is done for this node when triggered by a MouseEvent or a contains function call. If pickOnBounds is true, then picking is computed by intersecting with the bounds of this node, else picking is computed by intersecting with the geometric shape of this node.
Sample Output
This sample is actually far more complicated than is necessary to demonstrate the pickOnBounds function - but I just did something this complicated so that it shows what happens "when stacking two XYCharts in the same pane" as mentioned in the poster's question.
In the sample below two line charts are stacked on top of each other and the mouse is moved over the data line in one chart which has a glow function attached to it's mouseenter event. The mouse is then moved off of the first line chart data and the glow is removed from it. The mouse is then placed over the second line chart data of an underlying stacked chart and the glow is added to that linechart in the underlying stacked chart.
This sample was developed using Java8 and the coloring and behaviour described is what I exeperienced running the program on Mac OS X and Java 8b91.
Sample Code
The code below is just for demonstrating that pickOnBounds does work for allowing you to pass mouse events through transparent regions stacked on top of opaque node shapes. It is not a recommended code practice to follow for styling lines in charts (you are better off using style sheets than lookups for that), it is also not necessary that you use a line chart stack to get multiple series on a single chart - it was only necessary or simpler to do these things to demonstrate the pick on bounds concept application for this answer.
Note the recursive call to set the pickOnBounds property for the charts after the charts have been shown on a stage and all of their requisite nodes created.
Sample code is an adaption of JavaFX 2 XYChart.Series and setOnMouseEntered:
import javafx.application.Application;
import javafx.collections.*;
import javafx.event.EventHandler;
import javafx.scene.*;
import javafx.scene.chart.*;
import javafx.scene.effect.Glow;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
public class LineChartSample extends Application {
#SuppressWarnings("unchecked")
#Override public void start(Stage stage) {
// initialize data
ObservableList<XYChart.Data> data = FXCollections.observableArrayList(
new XYChart.Data(1, 23),new XYChart.Data(2, 14),new XYChart.Data(3, 15),new XYChart.Data(4, 24),new XYChart.Data(5, 34),new XYChart.Data(6, 36),new XYChart.Data(7, 22),new XYChart.Data(8, 45),new XYChart.Data(9, 43),new XYChart.Data(10, 17),new XYChart.Data(11, 29),new XYChart.Data(12, 25)
);
ObservableList<XYChart.Data> reversedData = FXCollections.observableArrayList(
new XYChart.Data(1, 25), new XYChart.Data(2, 29), new XYChart.Data(3, 17), new XYChart.Data(4, 43), new XYChart.Data(5, 45), new XYChart.Data(6, 22), new XYChart.Data(7, 36), new XYChart.Data(8, 34), new XYChart.Data(9, 24), new XYChart.Data(10, 15), new XYChart.Data(11, 14), new XYChart.Data(12, 23)
);
// create charts
final LineChart<Number, Number> lineChart = createChart(data);
final LineChart<Number, Number> reverseLineChart = createChart(reversedData);
StackPane layout = new StackPane();
layout.getChildren().setAll(
lineChart,
reverseLineChart
);
// show the scene.
Scene scene = new Scene(layout, 800, 600);
stage.setScene(scene);
stage.show();
// make one line chart line green so it is easy to see which is which.
reverseLineChart.lookup(".default-color0.chart-series-line").setStyle("-fx-stroke: forestgreen;");
// turn off pick on bounds for the charts so that clicks only register when you click on shapes.
turnOffPickOnBoundsFor(lineChart);
turnOffPickOnBoundsFor(reverseLineChart);
// add a glow when you mouse over the lines in the line chart so that you can see that they are chosen.
addGlowOnMouseOverData(lineChart);
addGlowOnMouseOverData(reverseLineChart);
}
#SuppressWarnings("unchecked")
private void turnOffPickOnBoundsFor(Node n) {
n.setPickOnBounds(false);
if (n instanceof Parent) {
for (Node c: ((Parent) n).getChildrenUnmodifiable()) {
turnOffPickOnBoundsFor(c);
}
}
}
private void addGlowOnMouseOverData(LineChart<Number, Number> lineChart) {
// make the first series in the chart glow when you mouse over it.
Node n = lineChart.lookup(".chart-series-line.series0");
if (n != null && n instanceof Path) {
final Path path = (Path) n;
final Glow glow = new Glow(.8);
path.setEffect(null);
path.setOnMouseEntered(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent e) {
path.setEffect(glow);
}
});
path.setOnMouseExited(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent e) {
path.setEffect(null);
}
});
}
}
private LineChart<Number, Number> createChart(ObservableList<XYChart.Data> data) {
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("Number of Month");
final LineChart<Number, Number> lineChart = new LineChart<>(xAxis, yAxis);
lineChart.setTitle("Stock Monitoring, 2010");
XYChart.Series series = new XYChart.Series(data);
series.setName("My portfolio");
series.getData().addAll();
lineChart.getData().add(series);
lineChart.setCreateSymbols(false);
lineChart.setLegendVisible(false);
return lineChart;
}
public static void main(String[] args) { launch(args); }
}
Instead of doing this:
// turn off pick on bounds for the charts so that clicks only register when you click on shapes.
turnOffPickOnBoundsFor(lineChart);
turnOffPickOnBoundsFor(reverseLineChart);
do this:
// turn off pick on bounds for the charts so that clicks only register when you click on shapes.
turnOffPickOnBoundsFor(reverseLineChart, false);
with the folling methods.
private boolean turnOffPickOnBoundsFor(Node n, boolean plotContent) {
boolean result = false;
boolean plotContentFound = false;
n.setPickOnBounds(false);
if(!plotContent){
if(containsStyle(n)){
plotContentFound = true;
result=true;
}
if (n instanceof Parent) {
for (Node c : ((Parent) n).getChildrenUnmodifiable()) {
if(turnOffPickOnBoundsFor(c,plotContentFound)){
result = true;
}
}
}
n.setMouseTransparent(!result);
}
return result;
}
private boolean containsStyle(Node node){
boolean result = false;
for (String object : node.getStyleClass()) {
if(object.equals("plot-content")){
result = true;
break;
}
}
return result;
}
Also you will need to make the chart in front(reverseLineChart) transparent.
The code posted in jewelsea answer does not work. To make it work I implemented the changes proposed is user1638436 answer and Julia Grabovska comment. Here is a working version for the sake of future readers:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.effect.Glow;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
public class LineChartSample extends Application {
#SuppressWarnings("unchecked")
#Override public void start(Stage stage) {
// initialize data
ObservableList<XYChart.Data> data = FXCollections.observableArrayList(
new XYChart.Data(1, 23),new XYChart.Data(2, 14),new XYChart.Data(3, 15),new XYChart.Data(4, 24),new XYChart.Data(5, 34),new XYChart.Data(6, 36),new XYChart.Data(7, 22),new XYChart.Data(8, 45),new XYChart.Data(9, 43),new XYChart.Data(10, 17),new XYChart.Data(11, 29),new XYChart.Data(12, 25)
);
ObservableList<XYChart.Data> reversedData = FXCollections.observableArrayList(
new XYChart.Data(1, 25), new XYChart.Data(2, 29), new XYChart.Data(3, 17), new XYChart.Data(4, 43), new XYChart.Data(5, 45), new XYChart.Data(6, 22), new XYChart.Data(7, 36), new XYChart.Data(8, 34), new XYChart.Data(9, 24), new XYChart.Data(10, 15), new XYChart.Data(11, 14), new XYChart.Data(12, 23)
);
// create charts
final LineChart<Number, Number> bottomLineChart = createChart(data);
final LineChart<Number, Number> topLineChart = createChart(reversedData);
//add css to make top chart line transparent as pointed out by Julia Grabovska
//and user1638436, as well as make line green
topLineChart.getStylesheets().add(getClass().getResource("LineChartSample.css").toExternalForm());
StackPane layout = new StackPane(bottomLineChart, topLineChart);
// show the scene.
Scene scene = new Scene(layout, 800, 600);
stage.setScene(scene);
stage.show();
// turn off pick on bounds for the charts so that clicks only register when you click on shapes.
turnOffPickOnBoundsFor(topLineChart, false); //taken from user1638436 answer
// add a glow when you mouse over the lines in the line chart so that you can see that they are chosen.
addGlowOnMouseOverData(bottomLineChart);
addGlowOnMouseOverData(topLineChart);
}
//taken from user1638436 answer (https://stackoverflow.com/a/18104172/3992939)
private boolean turnOffPickOnBoundsFor(Node n, boolean plotContent) {
boolean result = false;
boolean plotContentFound = false;
n.setPickOnBounds(false);
if(!plotContent){
if(containsPlotContent(n)){
plotContentFound = true;
result=true;
}
if (n instanceof Parent) {
for (Node c : ((Parent) n).getChildrenUnmodifiable()) {
if(turnOffPickOnBoundsFor(c,plotContentFound)){
result = true;
}
}
}
n.setMouseTransparent(!result);
}
return result;
}
private boolean containsPlotContent(Node node){
boolean result = false;
for (String object : node.getStyleClass()) {
if(object.equals("plot-content")){
result = true;
break;
}
}
return result;
}
private void addGlowOnMouseOverData(LineChart<Number, Number> lineChart) {
// make the first series in the chart glow when you mouse over it.
Node n = lineChart.lookup(".chart-series-line.series0");
if ((n != null) && (n instanceof Path)) {
final Path path = (Path) n;
final Glow glow = new Glow(.8);
path.setEffect(null);
path.setOnMouseEntered(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent e) {
path.setEffect(glow);
}
});
path.setOnMouseExited(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent e) {
path.setEffect(null);
}
});
}
}
private LineChart<Number, Number> createChart(ObservableList<XYChart.Data> data) {
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("Number of Month");
final LineChart<Number, Number> lineChart = new LineChart<>(xAxis, yAxis);
lineChart.setTitle("Stock Monitoring, 2010");
XYChart.Series series = new XYChart.Series(data);
series.setName("My portfolio");
series.getData().addAll();
lineChart.getData().add(series);
lineChart.setCreateSymbols(false);
lineChart.setLegendVisible(false);
return lineChart;
}
public static void main(String[] args) { launch(args); }
}
LineChartSample.css:
.chart-plot-background {
-fx-background-color:transparent;
}
.default-color0.chart-series-line{
-fx-stroke: forestgreen;
}
A simpler version of turnOffPickOnBoundsFor method:
private boolean turnOffPickOnBoundsFor(Node n) {
n.setPickOnBounds(false);
boolean isContainPlotContent = containsPlotContent(n);
if (! isContainPlotContent && (n instanceof Parent) ) {
for (Node c : ((Parent) n).getChildrenUnmodifiable()) {
if(turnOffPickOnBoundsFor(c)){
isContainPlotContent = true;
}
}
}
n.setMouseTransparent(!isContainPlotContent);
return isContainPlotContent;
}
Based on jewelsea answer setting top pane background color of the pane to null and topPane.setPickOnBounds(false); works fine:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class PropagateEvents extends Application {
private double x, y;
#Override public void start(Stage primaryStage) throws Exception {
StackPane root = new StackPane(getBottomPane(), getTopPane());
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private Pane getBottomPane() {
Pane pane = new Pane();
pane.setStyle("-fx-background-color : yellow;");
pane.setPrefSize(250,200);
pane.setOnMouseClicked(e-> System.out.println("Bottom pane recieved click event"));
return pane;
}
private Pane getTopPane() {
Label label = new Label();
label.setPrefSize(20,10);
label.setStyle("-fx-background-color:red;");
label.layoutXProperty().setValue(30); label.layoutYProperty().setValue(30);
addDragSupport(label);
Pane pane = new Pane(label);
// NULL color setPickOnBounds do the trick
pane.setPickOnBounds(false);
pane.setStyle("-fx-background-color: null; ");
return pane;
}
//drag support for red label
private void addDragSupport(Node node) {
node.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
x = node.getLayoutX() - mouseEvent.getSceneX();
y = node.getLayoutY() - mouseEvent.getSceneY();
node.setCursor(Cursor.MOVE);
}
});
node.setOnMouseReleased(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
node.setCursor(Cursor.HAND);
}
});
node.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
node.setLayoutX(mouseEvent.getSceneX() + x);
node.setLayoutY(mouseEvent.getSceneY() + y);
}
});
node.setOnMouseEntered(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
node.setCursor(Cursor.HAND);
}
});
}
public static void main (String[] args) {launch(null); }
}