In our JavaFX project we need a line chart (one Data Serie) with different color between the Data Points which depends on the following rule: (t=time)
If Value t(x) < t(x+1) => Color should be green
If Value t(x) > t(x+1) => Color should be red
If Value t(x) = t(x+1) => Color should be grey
I can easily change the color of the entire line chart using CSS, but I have no clue how to solve this problem!
Here is a crude example of how to connect different Series. Comments in the code.
import java.util.Random;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.stage.Stage;
public class LineChartSample extends Application {
#Override public void start(Stage stage) {
stage.setTitle("Line Chart Sample");
//defining the axes
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("Number of Month");
//creating the chart
final LineChart<Number,Number> lineChart =
new LineChart<Number,Number>(xAxis,yAxis);
lineChart.setTitle("Stock Monitoring, 2010");
//defining a series
XYChart.Series series1 = new XYChart.Series();
XYChart.Series series2 = new XYChart.Series();
XYChart.Series series3 = new XYChart.Series();
series1.setName("t <= 10");
series2.setName("10 <= t <= 20");
series3.setName("t >= 20");
//populating the series with data
Random random = new Random();
int whenIEquals10 = -1;//Used to store the value to start second series. This will be the last value in the first series.
int whenIEquals20 = -1;//Used to store the value to start third series. This will be the last value in the second series.
for(int i = 1; i <= 30; i++)
{
if(i <= 10)//range of first series
{
int randomNumber = random.nextInt(50) + 1;//Populate graph with random numbers.
series1.getData().add(new XYChart.Data(i, randomNumber));
if(i == 10)//The first series ends at 10, so save this value to start the second series.
{
whenIEquals10 = randomNumber;
}
}
if(i >= 10 && i <= 20)//Range of second series.
{
int randomNumber = random.nextInt(50) + 1;
if(i == 10)//Start this second series with the last value from the first series.
{
randomNumber = whenIEquals10;
}
series2.getData().add(new XYChart.Data(i, randomNumber));
if(i == 20)//The second series ends at 20, so save this value to start the third series.
{
whenIEquals20 = randomNumber;
}
}
if(i >= 20)//Range of thired series.
{
int randomNumber = random.nextInt(50) + 1;
if(i == 20)//Start this third series with the last value from the second series.
{
randomNumber = whenIEquals20;
}
series3.getData().add(new XYChart.Data(i, randomNumber));
}
}
Scene scene = new Scene(lineChart,800,600);
lineChart.getData().addAll(series1, series2, series3);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Related
I have a barchat with 5 bars, now I'm trying to move each bar from left to right based on high value (more like a bar chart race from flourish # https://app.flourish.studio) not the same but the idea is the same.
To check small numbers against big numbers and big numbers against small numbers I'm using random integers.
For example, If barE is greater then all bars and less then barA, it should move to number 2 and replace bar B. I'm using the "if statement" to try and accomplish this. The problem is, only one transition is happening which is the first one. When random numbers change every 3 seconds the correct transition does not happen. Does anyone know how can I correct this problem?
import java.util.Calendar;
import java.util.TimeZone;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class App extends Application {
private ScheduledExecutorService scheduledExecutorService;
final static String austria = "Austria", brazil = "Brazil", france = "France", england = "England", belgium = "Belgium";
private IntegerProperty secondA, secondB , secondC, secondD, secondE;
private Text secondAText, secondBText , secondCText, secondDText, secondEText;
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("Realtime Bar Chart Demo");
//defining the axes
final CategoryAxis xAxis = new CategoryAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setAnimated(false);
yAxis.setAnimated(false);
//creating the bar chart with two axis
final BarChart<String,Number> bc = new BarChart<>(xAxis,yAxis);
bc.setAnimated(false);
bc.setTitle("Country Summary");
xAxis.setLabel("Country");
yAxis.setLabel("Value");
//defining a series to display data
XYChart.Series<String, Number> seriesA = new XYChart.Series<>();
Data<String, Number> dataA = new XYChart.Data<>(austria,0);
seriesA.getData().add(dataA);
seriesA.setName("Austra");
secondA = new SimpleIntegerProperty(0);
secondAText = new Text("");
secondA.addListener((ChangeListener<Number>) (observable, oldValue, newValue) -> {
dataA.setYValue(newValue);
secondAText.setText(String.valueOf(newValue));
});
XYChart.Series<String, Number> seriesB = new XYChart.Series<>();
Data<String, Number> dataB = new XYChart.Data<>(brazil,0);
seriesB.getData().add(dataB);
seriesB.setName("Brazil");
secondB = new SimpleIntegerProperty(0);
secondB.bind(secondA.add(0));
secondBText = new Text("");
secondB.addListener((ChangeListener<Number>) (observable, oldValue, newValue) -> {
dataB.setYValue(newValue);
secondBText.setText(String.valueOf(newValue));
});
XYChart.Series<String, Number> seriesC = new XYChart.Series<>();
Data<String, Number> dataC = new XYChart.Data<>(france,0);
seriesC.getData().add(dataC);
seriesC.setName("France");
secondC = new SimpleIntegerProperty(0);
secondC.bind(secondA.add(0));
secondCText = new Text("");
secondC.addListener((ChangeListener<Number>) (observable, oldValue, newValue) -> {
dataC.setYValue(newValue);
secondCText.setText(String.valueOf(newValue));
});
XYChart.Series<String, Number> seriesD = new XYChart.Series<>();
Data<String, Number> dataD = new XYChart.Data<>(england,0);
seriesD.getData().add(dataD);
seriesD.setName("England");
secondD = new SimpleIntegerProperty(0);
secondD.bind(secondA.add(0));
secondDText = new Text("");
secondD.addListener((ChangeListener<Number>) (observable, oldValue, newValue) -> {
dataD.setYValue(newValue);
secondDText.setText(String.valueOf(newValue));
});
XYChart.Series<String, Number> seriesE = new XYChart.Series<>();
Data<String, Number> dataE = new XYChart.Data<>(belgium,0);
seriesE.getData().add(dataE);
seriesE.setName("Belgium");
secondE = new SimpleIntegerProperty(0);
secondE.bind(secondA.add(0));
secondEText = new Text("");
secondE.addListener((ChangeListener<Number>) (observable, oldValue, newValue) -> {
dataE.setYValue(newValue);
secondEText.setText(String.valueOf(newValue));
});
// add series to chart
bc.getData().add(seriesA);
bc.getData().add(seriesB);
bc.getData().add(seriesC);
bc.getData().add(seriesD);
bc.getData().add(seriesE);
displayLabelForData(dataA, secondAText);
displayLabelForData(dataB, secondBText);
displayLabelForData(dataC, secondCText);
displayLabelForData(dataD, secondDText);
displayLabelForData(dataE, secondEText);
// setup scene
Scene scene = new Scene(bc, 800, 600);
primaryStage.setScene(scene);
// show the stage
primaryStage.show();
// setup a scheduled executor to periodically put data into the chart
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
// input data onto graph per second scheduledExecutorService.scheduleAtFixedRate(() -> {
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
double posA = dataA.getNode().localToScene(dataA.getNode().getBoundsInLocal()).getMinX();
double posB = dataB.getNode().localToScene(dataB.getNode().getBoundsInLocal()).getMinX();
double posC = dataC.getNode().localToScene(dataC.getNode().getBoundsInLocal()).getMinX();
double posD = dataD.getNode().localToScene(dataD.getNode().getBoundsInLocal()).getMinX();
double posE = dataE.getNode().localToScene(dataE.getNode().getBoundsInLocal()).getMinX();
TranslateTransition ttA = new TranslateTransition(Duration.millis(2000), dataA.getNode());
TranslateTransition ttB = new TranslateTransition(Duration.millis(2000), dataB.getNode());
TranslateTransition ttC = new TranslateTransition(Duration.millis(2000), dataC.getNode());
TranslateTransition ttD = new TranslateTransition(Duration.millis(2000), dataD.getNode());
TranslateTransition ttE = new TranslateTransition(Duration.millis(2000), dataE.getNode());
//Genarate random numbers
Integer randomB = ThreadLocalRandom.current().nextInt(60);
Integer randomC = ThreadLocalRandom.current().nextInt(60);
Integer randomD = ThreadLocalRandom.current().nextInt(60);
Integer randomE = ThreadLocalRandom.current().nextInt(60);
int intSecondB = secondB.bind(secondA.add(randomB));
int intSecondC = secondC.bind(secondA.add(randomC));
int intSecondD = secondD.bind(secondA.add(randomD));
int intSecondE = secondE.bind(secondA.add(randomE));
I'm not sure if the problem is with the below if statements or the code itself. To avoid the code for being more long, I only included if statements that compare only seriesB(barB) against other bars.
//using if statement to swich each bar based on value
if (intSecondB >= intSecondA && intSecondB >= intSecondB && intSecondB >= intSecondC && intSecondB >= intSecondD && intSecondB >= intSecondE) {
double diffBA = posB - posA;
ttA.setByX(diffBA);
ttB.setByX(-diffBA);
ttA.setCycleCount(1);
ttA.setAutoReverse(true);
ttA.play();
ttB.setCycleCount(1);
ttB.setAutoReverse(true);
ttB.play();
}
if (intSecondB < intSecondA && intSecondB >= intSecondC && intSecondB >= intSecondD && intSecondB >= intSecondE) {
System.out.println("keep seriesB(barB) at its position");
}
if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB >= intSecondD && intSecondB >= intSecondE) {
double diffCB = posC - posB;
ttB.setByX(diffCB);
ttC.setByX(-diffCB);
ttB.setCycleCount(1);
ttB.setAutoReverse(true);
ttB.play();
ttC.setCycleCount(1);
ttC.setAutoReverse(true);
ttC.play();
}
if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB < intSecondD && intSecondB >= intSecondE) {
double diffDB = posD - posB;
ttB.setByX(diffDB);
ttD.setByX(-diffDB);
ttB.setCycleCount(1);
ttB.setAutoReverse(true);
ttB.play();
ttD.setCycleCount(1);
ttD.setAutoReverse(true);
ttD.play();
}
if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB < intSecondD && intSecondB < intSecondE) {
double diffEB = posE - posB;
ttB.setByX(diffEB);
ttE.setByX(-diffEB);
ttB.setCycleCount(1);
ttB.setAutoReverse(true);
ttB.play();
ttE.setCycleCount(1);
ttE.setAutoReverse(true);
ttE.play();
}
if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB < intSecondD && intSecondB < intSecondE) {
double diffFB = posF - posB;
ttB.setByX(diffFB);
ttF.setByX(-diffFB);
ttB.setCycleCount(1);
ttB.setAutoReverse(true);
ttB.play();
ttF.setCycleCount(1);
ttF.setAutoReverse(true);
ttF.play();
}
if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB < intSecondD && intSecondB < intSecondE) {
double diffGB = posG - posB;
ttB.setByX(diffGB);
ttG.setByX(-diffGB);
ttB.setCycleCount(1);
ttB.setAutoReverse(true);
ttB.play();
ttG.setCycleCount(1);
ttG.setAutoReverse(true);
ttG.play();
}
if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB < intSecondD && intSecondB < intSecondE) {
double diffHB = posH - posB;
ttB.setByX(diffHB);
ttH.setByX(-diffHB);
ttB.setCycleCount(1);
ttB.setAutoReverse(true);
ttB.play();
ttH.setCycleCount(1);
ttH.setAutoReverse(true);
ttH.play();
}
if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB < intSecondD && intSecondB < intSecondE) {
double diffIB = posI - posB;
ttB.setByX(diffIB);
ttI.setByX(-diffIB);
ttB.setCycleCount(1);
ttB.setAutoReverse(true);
ttB.play();
ttI.setCycleCount(1);
ttI.setAutoReverse(true);
ttI.play();
}
if (intSecondB < intSecondA && intSecondB < intSecondC && intSecondB < intSecondD && intSecondB < intSecondE) {
double diffJB = posJ - posB;
ttB.setByX(diffJB);
ttJ.setByX(-diffJB);
ttB.setCycleCount(1);
ttB.setAutoReverse(true);
ttB.play();
ttJ.setCycleCount(1);
ttJ.setAutoReverse(true);
ttJ.play();
}
// Update the chart
Platform.runLater(() -> {
secondA.set( cal.get(Calendar.SECOND));
});
}, 0, 3, TimeUnit.SECONDS);
}
#Override
public void stop() throws Exception {
super.stop();
scheduledExecutorService.shutdownNow();
}
private void displayLabelForData(XYChart.Data<String, Number> data, Text text) {
final Node node = data.getNode();
((Group) node.getParent()).getChildren().add(text);
node.boundsInParentProperty().addListener((ChangeListener<Bounds>) (ov, oldBounds, bounds) -> {
text.setLayoutX(
Math.round( bounds.getMinX() + bounds.getWidth() / 2 - text.prefWidth(-1) / 2));
text.setLayoutY(Math.round( bounds.getMinY() - text.prefHeight(-1) * 0.5));
});
}
public static void main(String[] args) {
launch(args);
}
}
Any help will be appreciated!
Here is one strategy:
Create an ObservableList for the data
Create a SortedList from the underlying list
Register a listener with the SortedList, and when the data change, create an animation:
a. For each bar, find its current position and the position of the bar in the index corresponding to its new order
b. Use those positions to animate the translateX property of the bar
c. Animate the yValue property of the XYChart.Data in the same animation
d. At the end of the animation, reset the chart data to the new sorted data
There are a couple of little "gotchas" here: you need to turn off autoRanging on the CategoryAxis (else it will ignore changes to the order of the bars) and reset the categories using the new ordering when you update the data.
Here is an example. I created a class just to hold the data, without any chart API:
public static class CountryValue {
private final String country ;
private final double value ;
public CountryValue(String country, double value) {
super();
this.country = country;
this.value = value;
}
public String getCountry() {
return country;
}
public double getValue() {
return value;
}
}
and a simple data model to hold a list of these:
public static class Model {
private final ObservableList<CountryValue> values ;
public Model(CountryValue... countryValues) {
values = FXCollections.observableArrayList(countryValues) ;
}
public ObservableList<CountryValue> getValues() {
return values ;
}
}
Then the key parts look like:
Model model = new Model() ;
SortedList<CountryValue> sortedData = new SortedList<>(
model.getValues(),
Comparator.comparingDouble(CountryValue::getValue).reversed());
ObservableList<XYChart.Data<String, Number>> chartData = FXCollections.observableArrayList();
CategoryAxis countryAxis = new CategoryAxis();
countryAxis.setAutoRanging(false);
populateChartData(sortedData, chartData, countryAxis);
sortedData.addListener((Change<? extends CountryValue> c) -> {
Timeline timeline = new Timeline() ;
for (int newIndex = 0 ; newIndex < sortedData.size() ; newIndex++) {
CountryValue cv = sortedData.get(newIndex);
int currentIndex = indexByCountry(cv.getCountry(), chartData);
Data<String, Number> data = chartData.get(currentIndex);
double currentX = data.getNode().getBoundsInParent().getCenterX();
double targetX = chartData.get(newIndex).getNode().getBoundsInParent().getCenterX();
DoubleProperty translateXProperty = data.getNode().translateXProperty();
KeyValue kvx1 = new KeyValue(translateXProperty, 0);
KeyValue kvx2 = new KeyValue(translateXProperty, targetX - currentX);
ObjectProperty<Number> yValueProperty = data.YValueProperty();
KeyValue kvy1 = new KeyValue(yValueProperty, data.getYValue());
KeyValue kvy2 = new KeyValue(yValueProperty, cv.getValue());
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, kvx1),
new KeyFrame(Duration.ZERO, kvy1),
new KeyFrame(animationDuration, kvx2),
new KeyFrame(animationDuration, kvy2)
);
}
timeline.setOnFinished(e -> populateChartData(sortedData, chartData, countryAxis));
timeline.play();
});
The utility populateChartData() method updates both the category axis and the data:
private void populateChartData(ObservableList<CountryValue> source,
ObservableList<XYChart.Data<String, Number>> chartData,
CategoryAxis countryAxis) {
countryAxis.getCategories().setAll(
source.stream()
.map(CountryValue::getCountry)
.collect(Collectors.toList())
);
chartData.setAll(
source.stream()
.map(cv -> new XYChart.Data<String, Number>(cv.getCountry(), cv.getValue()))
.collect(Collectors.toList())
);
}
Here's a complete example. The animation is a little "jerky"; I think because the y-axis scale changes in an unpredictable way. You could manage this yourself by turning off autoranging on the y-axis, computing the max y value from the new data, and animating the y-axis range as well as the bars. Also note that it's important no updates to the data happen while the animation is running (else you'll end up with multiple animations running at once). Here that's simply managed by timing, but a more robust solution would check for that and either throttle updates or just end the current animation before starting a new one.
import java.util.Comparator;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.scene.Scene;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Duration;
/**
* JavaFX App
*/
public class FlourishChart extends Application {
private final Duration animationDuration = Duration.millis(250);
#Override
public void start(Stage stage) {
Model model = new Model() ;
Simulator simulator = new Simulator(model);
SortedList<CountryValue> sortedData = new SortedList<>(
model.getValues(),
Comparator.comparingDouble(CountryValue::getValue).reversed());
ObservableList<XYChart.Data<String, Number>> chartData = FXCollections.observableArrayList();
CategoryAxis countryAxis = new CategoryAxis();
countryAxis.setAutoRanging(false);
populateChartData(sortedData, chartData, countryAxis);
BarChart<String, Number> chart = new BarChart<>(countryAxis, new NumberAxis());
// turn off default animation:
chart.setAnimated(false);
Series<String, Number> series = new Series<>(chartData);
chart.getData().add(series);
// when sorted data change, animate bar chart nodes
// at end of animation, update chart data with new data
sortedData.addListener((Change<? extends CountryValue> c) -> {
Timeline timeline = new Timeline() ;
for (int newIndex = 0 ; newIndex < sortedData.size() ; newIndex++) {
CountryValue cv = sortedData.get(newIndex);
int currentIndex = indexByCountry(cv.getCountry(), chartData);
Data<String, Number> data = chartData.get(currentIndex);
double currentX = data.getNode().getBoundsInParent().getCenterX();
double targetX = chartData.get(newIndex).getNode().getBoundsInParent().getCenterX();
DoubleProperty translateXProperty = data.getNode().translateXProperty();
KeyValue kvx1 = new KeyValue(translateXProperty, 0);
KeyValue kvx2 = new KeyValue(translateXProperty, targetX - currentX);
ObjectProperty<Number> yValueProperty = data.YValueProperty();
KeyValue kvy1 = new KeyValue(yValueProperty, data.getYValue());
KeyValue kvy2 = new KeyValue(yValueProperty, cv.getValue());
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, kvx1),
new KeyFrame(Duration.ZERO, kvy1),
new KeyFrame(animationDuration, kvx2),
new KeyFrame(animationDuration, kvy2)
);
}
timeline.setOnFinished(e -> populateChartData(sortedData, chartData, countryAxis));
timeline.play();
});
BorderPane root = new BorderPane(chart);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
new Thread(simulator).start();
}
private int indexByCountry(String country, ObservableList<Data<String, Number>> chartData) {
for (int index = 0 ; index < chartData.size(); index++) {
if (chartData.get(index).getXValue().equals(country))
return index ;
}
return -1 ;
}
private void populateChartData(ObservableList<CountryValue> source,
ObservableList<XYChart.Data<String, Number>> chartData,
CategoryAxis countryAxis) {
countryAxis.getCategories().setAll(
source.stream()
.map(CountryValue::getCountry)
.collect(Collectors.toList())
);
chartData.setAll(
source.stream()
.map(cv -> new XYChart.Data<String, Number>(cv.getCountry(), cv.getValue()))
.collect(Collectors.toList())
);
}
public static class Model {
private final ObservableList<CountryValue> values ;
public Model(CountryValue... countryValues) {
values = FXCollections.observableArrayList(countryValues) ;
}
public ObservableList<CountryValue> getValues() {
return values ;
}
}
// replace with record when they are standard in Java:
public static class CountryValue {
private final String country ;
private final double value ;
public CountryValue(String country, double value) {
super();
this.country = country;
this.value = value;
}
public String getCountry() {
return country;
}
public double getValue() {
return value;
}
}
// Not really relevant to problem; just simulates changing data in model
public class Simulator implements Runnable {
private final Model model ;
private final Random rng = new Random();
public Simulator(Model model) {
this.model = model ;
createData();
}
#Override
public void run() {
for (int i = 0 ; i < 10 ; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Platform.runLater(this::createData);
}
}
private void createData() {
model.getValues().setAll(
Stream.of("Austria", "Brazil", "France", "England", "Belgium")
.map(country -> new CountryValue(country, 50 * rng.nextDouble() + 50))
.collect(Collectors.toList())
);
}
}
public static void main(String[] args) {
launch();
}
}
I am creating a 3D plot in JavaFX using the same technique discussed in this answer, with a MeshView and a PhongMaterial to provide the colours. However, only the top side of the material is coloured, and if the user rotates the camera to view from below, it's impossible to determine the shape of the plot because it is all black.
My questions:
Is there any way to set the material of the reverse side of the mesh?
If not, is there a good approach to "faking" it? I would imagine creating a new mesh upside-down in exactly the same position would cause rendering issues; is the best approach to do that but apply a very small offset so that the two meshes are not exactly on top of each other?
Edit: I have included some example code below, which is cut down from my real code but contains enough to illustrate the problem. By default it displays the top of the mesh, which is coloured red in this example. If you change the line that reads new Rotate(-30, Rotate.X_AXIS) so that the angle becomes +30 rather than -30, it will rotate the camera to show the underside of the mesh, which you will see appears black.
package test;
import javafx.application.Application;
import javafx.scene.DepthTest;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.image.Image;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.CullFace;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class TestApp extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
TestPlot tp = new TestPlot();
tp.setPrefSize(600, 400);
Scene scene = new Scene(tp);
stage.setScene(scene);
stage.show();
}
class TestPlot extends Region {
private final PerspectiveCamera camera = new PerspectiveCamera(true);
private double[][] data = new double[500][500];
private final StackPane root = new StackPane();
private final SubScene subscene;
public TestPlot() {
subscene = new SubScene(root, 1, 1, true, SceneAntialiasing.BALANCED);
subscene.setCamera(camera);
getChildren().add(subscene);
widthProperty().addListener((obs, oldVal, newVal) -> refreshPlot());
heightProperty().addListener((obs, oldVal, newVal) -> refreshPlot());
refreshPlot();
}
private void refreshPlot() {
// Set the subscene bounds to match the plot bounds, in case the plot was
// resized
subscene.setHeight(this.getHeight());
subscene.setWidth(this.getWidth());
// Clear any existing stuff
root.getChildren().clear();
root.setStyle("-fx-background-color: rgba(0, 0, 0, 0);");
root.getChildren().add(camera);
camera.getTransforms().clear();
int xDataPoints = data.length;
int zDataPoints = data[0].length;
// Create data mesh
TriangleMesh mesh = new TriangleMesh();
for (int x = 0; x < xDataPoints; x++) {
for (int z = 0; z < zDataPoints; z++) {
// Invert the data as JavaFX meshes are positive-down, whereas we expect
// the plot to be positive-up
mesh.getPoints().addAll(x, (float) (-data[x][z]), z);
}
}
// Create faces from data mesh
for (int x = 0; x < xDataPoints - 1; x++) {
for (int z = 0; z < zDataPoints - 1; z++) {
int tl = x * zDataPoints + z; // top-left
int bl = x * zDataPoints + z + 1; // bottom-left
int tr = (x + 1) * zDataPoints + z; // top-right
int br = (x + 1) * zDataPoints + z + 1; // bottom-right
int offset = (x * (zDataPoints - 1) + z) * 8 / 2; // div 2 because we have u AND v in the list
// working
mesh.getFaces().addAll(bl, offset + 1, tl, offset + 0, tr, offset + 2);
mesh.getFaces().addAll(tr, offset + 2, br, offset + 3, bl, offset + 1);
}
}
// Create data mesh texture map
for (float x = 0; x < xDataPoints - 1; x++) {
for (float z = 0; z < zDataPoints - 1; z++) {
float x0 = x / xDataPoints;
float z0 = z / zDataPoints;
float x1 = (x + 1) / xDataPoints;
float z1 = (z + 1) / zDataPoints;
mesh.getTexCoords().addAll( //
x0, z0, // 0, top-left
x0, z1, // 1, bottom-left
x1, z1, // 2, top-right
x1, z1 // 3, bottom-right
);
}
}
// Create texture material
Image diffuseMap = createTexture(data);
PhongMaterial material = new PhongMaterial();
material.setDiffuseMap(diffuseMap);
// Create & add mesh view
MeshView meshView = new MeshView(mesh);
meshView.setTranslateZ(-zDataPoints);
meshView.setMaterial(material);
meshView.setCullFace(CullFace.NONE);
meshView.setDrawMode(DrawMode.FILL);
meshView.setDepthTest(DepthTest.ENABLE);
root.getChildren().addAll(meshView);
double biggestAxisSize = xDataPoints;
double z = -(0.5 * biggestAxisSize) / Math.tan(0.5 * Math.toRadians(camera.getFieldOfView()));
camera.getTransforms().addAll(
new Translate(0, 0, -zDataPoints / 3.0),
new Rotate(-30, Rotate.X_AXIS),
new Translate(0, 0.5, z)
);
camera.setFarClip(biggestAxisSize * 200.0);
}
private Image createTexture(double[][] data) {
int width = data.length;
int height = data[0].length;
WritableImage wr = new WritableImage(width, height);
PixelWriter pw = wr.getPixelWriter();
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
pw.setColor(x, y, Color.RED);
}
}
return wr;
}
}
}
Modify the cullFace property of your MeshView:
meshView.setCullFace(CullFace.NONE);
Also you need to add ambient light to the scene. The normals of the surface are automatiacally determined and the scalar product used with won't be positive, if the normal is facing away from the light source...
root.getChildren().add(new AmbientLight(Color.WHITE));
I need to add to my pane a Square, A Circle and a rectangle in this order everytime i click, so at the firs click i will add the square , at the second one the circle, the third one the rectangle and the fourth one again the square and so on.
r1.setOnMouseClicked((T) -> {
if (T.getClickCount() == 1 || (T.getClickCount()%2 != 0 && T.getClickCount() % 3 != 0))
{
Rectangle r = new Rectangle(T.getSceneX(), T.getSceneY(), 50, 50);
root.getChildren().add(r);
}
else if (T.getClickCount()%2 == 0 && T.getClickCount() % 3 != 0) {
Circle c1 = new Circle(T.getSceneX(), T.getSceneY(), 50);
root.getChildren().add(c1);
}
else if (T.getClickCount()%2 != 0 && T.getClickCount() % 3 == 0) {
Rectangle r3 = new Rectangle(T.getSceneX(), T.getSceneY(), 40, 70);
root.getChildren().add(r3);
}
});
I have done this example using your above code:
import javafx.scene.layout.*;
import javafx.scene.*;
import javafx.stage.*;
import javafx.scene.shape.*;
import javafx.application.Application;
import javafx.scene.paint.Color;
public class Test1 extends Application{
private int count = 0;
#Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
Pane canvas = new Pane();
canvas.setStyle("-fx-background-color: black;");
canvas.setPrefSize(350,250);
canvas.setOnMouseClicked((t) -> {
count ++;
System.out.println("".format("%s %s %s", count, t.getSceneX(), t.getSceneY()));
if (count == 1 || (count%2 != 0 && count % 3 != 0))
{
Rectangle r = new Rectangle(50, 50, Color.RED);
r.relocate(t.getSceneX(),t.getSceneY());
canvas.getChildren().add(r);
}
else if (count%2 == 0 && count % 3 != 0) {
Circle c1 = new Circle(50,Color.GREEN);
c1.relocate(t.getSceneX(), t.getSceneY());
canvas.getChildren().add(c1);
}
else if (count%2 != 0 && count % 3 == 0) {
Rectangle r3 = new Rectangle(40, 70, Color.PINK);
r3.relocate(t.getSceneX(), t.getSceneY());
canvas.getChildren().add(r3);
}
});
root.getChildren().addAll(canvas);
}
public static void main(String[] args) {
launch(args);
}
}
I am trying to color different region of a polar chart with different colors. e.g coloring the region between the angle 20 and 60 and between the radii 2 and 4.
How can I do this? I was thinking of using a shape annotation and from there drawing an arc, but it seems there is no shape annotation for polar plots.
Any ideas?
Thank you
import java.awt.Color;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTick;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.PolarPlot;
import org.jfree.chart.renderer.DefaultPolarItemRenderer;
import org.jfree.chart.renderer.PolarItemRenderer;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.ui.TextAnchor;
public class test2 extends JFrame {
private static final String title = "Archimedes' Spiral";
public test2(String title) {
super(title);
JFreeChart chart = createChart(createDataset());
ChartPanel panel = new ChartPanel(chart);
panel.setPreferredSize(new Dimension(500, 500));
panel.setMouseZoomable(false);
this.add(panel);
}
private static XYDataset createDataset() {
XYSeriesCollection result = new XYSeriesCollection();
XYSeries series = new XYSeries(title);
XYSeries ser = new XYSeries("test");
for (int t = 0; t <= 1 * 360; t++) {
series.add(90 - t, t);
}
for (int t = 0; t <= 1 * 120; t++) {
ser.add(90 - t, 40);
ser.add(90 - t, 120);
}
result.addSeries(series);
result.addSeries(ser);
return result;
}
private static JFreeChart createChart(XYDataset dataset) {
ValueAxis radiusAxis = new NumberAxis();
radiusAxis.setTickLabelsVisible(false);
PolarItemRenderer renderer = new DefaultPolarItemRenderer();
PolarPlot plot = new PolarPlot(dataset, radiusAxis, renderer) {
#Override
protected List refreshAngleTicks() {
List<NumberTick> ticks = new ArrayList<NumberTick>();
int delta = (int) this.getAngleTickUnit().getSize();
for (int t = 0; t < 360; t += delta) {
int tp = (360 + 90 - t) % 360;
NumberTick tick = new NumberTick(
Double.valueOf(t), String.valueOf(tp),
TextAnchor.CENTER, TextAnchor.CENTER, 0.0);
ticks.add(tick);
}
return ticks;
}
};
plot.setBackgroundPaint(new Color(0x00f0f0f0));
plot.setRadiusGridlinePaint(Color.gray);
plot.addCornerTextItem("r(θ) = θ; 0 < θ < 6π");
DefaultPolarItemRenderer ren = new DefaultPolarItemRenderer();
ren.setSeriesFilled(0, true);
ren.setSeriesFilled(1, true);
plot.setRenderer(ren);
JFreeChart chart = new JFreeChart(
title, JFreeChart.DEFAULT_TITLE_FONT, plot, true);
chart.setBackgroundPaint(Color.white);
return chart;
}
public static void main(String[] args) {
test2 demo = new test2(title);
demo.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
demo.pack();
demo.setLocationRelativeTo(null);
demo.setVisible(true);
}
}
The DefaultPolarItemRenderer typically used in a PolarPlot has the method setSeriesFilled(), which controls whether a series is filled. The renderer specifies the AlphaComposite.SRC_OVER mode with a value of 50%, so overlapping fills look especially nice.
Addendum: To create the chart seen below, start with this example and reduce the data set's domain from 6π to 2π in createDataset():
for (int t = 0; t <= 1 * 360; t++) { ...
Then make the series filled in createChart():
...
DefaultPolarItemRenderer renderer = new DefaultPolarItemRenderer();
renderer.setSeriesFilled(0, true);
...
I am trying to create a filled series over each region of a polar plot. But the colors are not filled out right. Here is an image of what I get: http://i1122.photobucket.com/albums/l539/jpo2/polar-1.gif
Here is the code I have to loop through a given section is as follows:
if (i < 8) {
for(int r = 0; r< 20; r+=(NumberAxis) plot.getAxis()).getTickUnit().getSize()){
for(int theta = 0; theta <= 180; theta+=30){
XYSeries series = new XYSeries(i + "π/8 " + "< θ < 2π+" + i + "π/8");
for (int e = theta; e < theta+30; e++) {
series.add(90-e-i*45, r);
series.add(90-e-i*45, r- ((NumberAxis) plot.getAxis()).getTickUnit().getSize());
}
result.addSeries(series);
setFilled(result);
i++;
}
}
}
private void setFilled(XYDataset dataset) {
for (int i = 0; i < dataset.getSeriesCount(); i++) {
renderers.setSeriesFilled(i, true);
}
}
This is a modification of #trashgod's draw method at JFreechart Loop through polar chart sectors
Please help.
Full code:
import java.awt.Color;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTick;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.event.ChartProgressEvent;
import org.jfree.chart.event.ChartProgressListener;
import org.jfree.chart.plot.PolarPlot;
import org.jfree.chart.renderer.DefaultPolarItemRenderer;
import org.jfree.chart.renderer.PolarItemRenderer;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.ui.TextAnchor;
public class tests extends JFrame implements ChartProgressListener {
private static final String title = "Archimedes' Spirals";
private XYSeriesCollection result = new XYSeriesCollection();
private DefaultPolarItemRenderer renderers = new DefaultPolarItemRenderer();
private int i;
public tests(String title) {
super(title);
JFreeChart chart = createChart(result);
ChartPanel panel = new ChartPanel(chart);
panel.setPreferredSize(new Dimension(500, 500));
panel.setMouseZoomable(false);
this.add(panel);
}
private JFreeChart createChart(XYDataset dataset) {
ValueAxis radiusAxis = new NumberAxis();
radiusAxis.setTickLabelsVisible(false);
PolarItemRenderer renderer = new DefaultPolarItemRenderer();
PolarPlot plot = new PolarPlot(dataset, radiusAxis, renderer) {
#Override
protected List refreshAngleTicks() {
List<NumberTick> ticks = new ArrayList<NumberTick>();
int delta = (int) this.getAngleTickUnit().getSize();
for (int t = 0; t < 360; t += delta) {
int tp = (360 + 90 - t) % 360;
NumberTick tick = new NumberTick(
Double.valueOf(t), String.valueOf(tp),
TextAnchor.CENTER, TextAnchor.CENTER, 0.0);
ticks.add(tick);
}
return ticks;
}
};
plot.setBackgroundPaint(new Color(0x00f0f0f0));
plot.setRadiusGridlinePaint(Color.gray);
plot.addCornerTextItem("r(θ) = θ; 0 < θ < 2π; +iπ/8");
setFilled(dataset);
plot.setRenderer(renderers);
JFreeChart chart = new JFreeChart(
title, JFreeChart.DEFAULT_TITLE_FONT, plot, true);
chart.setBackgroundPaint(Color.white);
chart.addProgressListener(this);
return chart;
}
public static void main(String[] args) {
tests demo = new tests(title);
demo.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
demo.pack();
demo.setLocationRelativeTo(null);
demo.setVisible(true);
}
#Override
public void chartProgress(ChartProgressEvent e) {
if (e.getType() == ChartProgressEvent.DRAWING_FINISHED) {
System.out.println(e);
JFreeChart chart = e.getChart();
draw();
}
}
public void draw() {
if (i < 4) {
for (int g = 0; g < 30; g += 5) {
for (int h = 0; h < 180; h += 45) {
XYSeries series = new XYSeries(i + "π/8 " + "< θ < 2π+" + i + "π/8");
for (int t = h; t <= h + 45; t++) {
series.add(90 - t, g);
series.add(90 - t, g + 5);
}
result.addSeries(series);
setFilled(result);
i++;
}
}
}
}
private void setFilled(XYDataset dataset) {
for (int i = 0; i < dataset.getSeriesCount(); i++) {
renderers.setSeriesFilled(i, true);
}
}
}
Here's a simplified version the uses PolarPlot directly, without any transformation. It might be easier to experiment with.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import javax.swing.JFrame;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.PolarPlot;
import org.jfree.chart.renderer.DefaultPolarItemRenderer;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
/** #see http://stackoverflow.com/questions/6669734 */
public class PolarArcs {
private static final String title = "PolarArcs";
private static final double PI2 = 90d; // π/2 radians = 90°
private void display() {
JFrame f = new JFrame(title);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ChartPanel panel = new ChartPanel(createChart(createDataset()));
panel.setPreferredSize(new Dimension(400, 400));
f.add(panel);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
private JFreeChart createChart(XYDataset dataset) {
JFreeChart chart = ChartFactory.createPolarChart(
title, dataset, true, false, false);
PolarPlot plot = (PolarPlot) chart.getPlot();
plot.setBackgroundPaint(Color.white);
plot.setAngleGridlinesVisible(false);
plot.setRadiusGridlinesVisible(false);
DefaultPolarItemRenderer r = (DefaultPolarItemRenderer) plot.getRenderer();
for (int i = 0; i < dataset.getSeriesCount(); i++ ) {
r.setSeriesFilled(i, true);
}
NumberAxis rangeAxis = (NumberAxis) plot.getAxis();
rangeAxis.setTickLabelsVisible(false);
return chart;
}
private XYDataset createDataset() {
XYSeriesCollection result = new XYSeriesCollection();
for (int r = 8; r > 0; r--) {
XYSeries series = new XYSeries(title + String.valueOf(r));
for (int t = (int) -PI2; t <= PI2; t++) {
series.add(t, r);
}
result.addSeries(series);
}
return result;
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
new PolarArcs().display();
}
});
}
}