5

Background:

Am trying to create a Visual representation of realtime stock movement like the 2 attached images (Green indicating positive, Amber negative & value at left bottom Day's Low, value at right bottom Day's High) within a TableView in JavaFx. The movement will be controlled by the realtime data streaming. So the slide marker needs to be able to slide both ways with color changing if it croses Day's Open value

Research:

I always refer to this wonderful -> Link but in this case seek guidance as am not sure if its feasible with Progress Bar or Slider.

Any pointers on this can be achieved will be much appreciated.

Positive Value

Negative Value

iCoder
  • 1,406
  • 6
  • 16
  • 35
  • not sure whether you can achieve this via `ProgressBar`, but I am sure that you can make one yourself using `Line` shape – Muzib Feb 10 '18 at 17:32
  • I think you might be able to do this just with a slider. Use CSS with a linear gradient (something like `transparent 0%, transparent x%, green x%, green y%, transparent y%, transparent 100%`). Use an inline style for the CSS and update it when the value changes. – James_D Feb 10 '18 at 17:34

2 Answers2

3

A possible solution is to pack two ProgressBars into an HBox.
The following should not be regarded a full implementation, but as a demonstration of the idea:

import javafx.application.Application;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.When;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import src.tests.BiDiProgressBar.Indicator;

public class BiDiProgressBar extends Application {

    @Override public void start(Stage stage) {

        final HBox indicator = new Indicator(60);
        stage.setScene(new Scene(indicator));
        stage.show();

        //run test
        Thread th = new Thread(new TestTask((Indicator)indicator));
        th.setDaemon(true);
        th.start();
    }


    class Indicator extends HBox{

        //properties representing indicator min max, mid and current values
        DoubleProperty minValue = new SimpleDoubleProperty(0);
        DoubleProperty maxValue = new SimpleDoubleProperty(100);
        DoubleProperty nominalValue = new SimpleDoubleProperty(50);
        DoubleProperty currentlValue;

        //properties representing full, neg and pos ranges
        DoubleProperty rangeValue = new SimpleDoubleProperty();
        DoubleProperty posRangeValue = new SimpleDoubleProperty();
        DoubleProperty negRangeValue = new SimpleDoubleProperty();

        private ProgressBar negProgBar, posProgBar;
        private static final double indicatorWidth = 200; //in pixles

        Indicator(double nomlValue){

            super(0);
            setNominalValue(nomlValue);
            rangeValue.bind(maxValue.subtract(minValue));
            negRangeValue.bind(nominalValue.subtract(minValue));
            posRangeValue.bind(rangeValue.subtract(negRangeValue));
            currentlValue = new SimpleDoubleProperty(nominalValue.get());

            negProgBar = new ProgressBar();
            negProgBar.rotateProperty().set(180); //rotate to progress from left to right

            posProgBar = new ProgressBar();
            setProgBarWidth();
            setProgBarValue();
            getChildren().addAll(negProgBar, posProgBar);

            getStylesheets().add(getClass().getResource("css/indicator.css").toExternalForm());
            negProgBar.getStyleClass().add("neg-bar");
            posProgBar.getStyleClass().add("pos-bar");
        }

        private void setNominalValue(double nominalValue) {
            nominalValue = withInRange(nominalValue);
            this.nominalValue.set(nominalValue);
        }

        private void setProgBarWidth() {
            double width = (indicatorWidth * nominalValue.get())/rangeValue.get();
            negProgBar.setPrefWidth(width);
            posProgBar.setPrefWidth(indicatorWidth-width);
        }

        private void setProgBarValue() {

            final DoubleBinding posBinding = new When(currentlValue.greaterThan(nominalValue))
                        .then((currentlValue.subtract(nominalValue)).divide(posRangeValue))
                        .otherwise(0.);
            final DoubleBinding negBinding = new When(currentlValue.lessThan(nominalValue))
                    .then((nominalValue.subtract(currentlValue)).divide(negRangeValue))
                    .otherwise(0.);
            posProgBar.progressProperty().bind(posBinding);
            negProgBar.progressProperty().bind(negBinding);
        }

        private double withInRange(double value) {
            value = (value < minValue.get()) ? minValue.get() : value;
            value = (value > maxValue.get()) ? maxValue.get() : value;
            return value ;
        }

        public double getMin() {return minValue.get();}

        public double getMax() {return maxValue.get();}

        public double getNominal() {return nominalValue.get();}

        public void setProgress(double value) {
            currentlValue.set(withInRange(value));
        }
    }

    public static void main(String[] args) { launch(args);}
}

class TestTask extends Task<Void>{

    private Indicator indicator;
    public TestTask(Indicator indicator) {
        this.indicator = indicator;
    }

    @Override
    protected Void call() throws Exception {

        for(double value = indicator.getNominal();
                value < (indicator.getMax() +5) ; //exceed max
                value++)    {
            indicator.setProgress(value);
            delay(100);
        }

        for(double value = indicator.getMax();
                value > (indicator.getMin() -5 ) ;  value--)    {
            indicator.setProgress(value);
            delay(100);
        }

        return null;
    }

    private void delay(long millis) {

        try {
            Thread.sleep(millis);
        } catch (InterruptedException ex) { ex.printStackTrace();   }
    }
}

The result looks like this:

enter image description here

The point where the two ProgressBar meet represents the day start value. In the final implementation only one progress bar should be active at any given time.

indicator.css

.root { -fx-background-color: black; -fx-padding: 15; }
.progress-bar > .track {
  -fx-text-box-border: white;
  -fx-control-inner-background: lightgrey;
  -fx-border-radius: 0px ;
  -fx-background-radius: 0px ;
}

.pos-bar  { -fx-accent: green; }
.neg-bar  { -fx-accent: red; }
c0der
  • 18,467
  • 6
  • 33
  • 65
2

You can do this fairly simply with either a slider or progress bar, using CSS to change the background color of the track. For a progress bar, just make the bar transparent.

The basic idea is to use a linear "gradient". If the starting value is x% and the current value y%, with y > x, you need color stops at

(default 0%, default x%, green x%, green y%, default y%, default 100%)

where default is the default background color. (Similarly with red replacing green and x and y switched if y < x.)

Here's a SSCCE:

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.Slider;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class BidirectionalSlider extends Application {

    @Override
    public void start(Stage primaryStage) {
        double startingValue = 0.5 ;
        Slider slider = new Slider(0, 1, startingValue);

        slider.styleProperty().bind(Bindings.createStringBinding(() -> {

            double min = slider.getMin();
            double max = slider.getMax();
            double value = slider.getValue() ;

            return createSliderStyle(startingValue, min, max, value);

        }, slider.valueProperty()));

        ProgressBar progressBar1 = new ProgressBar(0.1);
        ProgressBar progressBar2 = new ProgressBar(0.9);

        progressBar1.styleProperty().bind(Bindings.createStringBinding(() -> 
            createSliderStyle(startingValue, 0.0, 1.0, progressBar1.getProgress()), 
            progressBar1.progressProperty()));

        progressBar2.styleProperty().bind(Bindings.createStringBinding(() -> 
            createSliderStyle(startingValue, 0.0, 1.0, progressBar2.getProgress()), 
            progressBar2.progressProperty()));

        VBox root = new VBox(5, slider, progressBar1, progressBar2);
        root.setAlignment(Pos.CENTER);

        Scene scene = new Scene(root, 400, 400);
        scene.getStylesheets().add("style.css");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private String createSliderStyle(double startingValue, double min, double max, double value) {
        StringBuilder gradient = new StringBuilder("-slider-track-color: ");
        String defaultBG = "derive(-fx-control-inner-background, -5%) " ;
        gradient.append("linear-gradient(to right, ").append(defaultBG).append("0%, ") ;

        double valuePercent = 100.0 * (value - min) / (max - min);

        double startingValuePercent = startingValue * 100.0;


        if (valuePercent > startingValuePercent) {
            gradient.append(defaultBG).append(startingValuePercent).append("%, ");
            gradient.append("green ").append(startingValuePercent).append("%, ");
            gradient.append("green ").append(valuePercent).append("%, ");
            gradient.append(defaultBG).append(valuePercent).append("%, ");
            gradient.append(defaultBG).append("100%); ");
        } else {
            gradient.append(defaultBG).append(valuePercent).append("%, ");
            gradient.append("red ").append(valuePercent).append("%, ");
            gradient.append("red ").append(startingValuePercent).append("%, ");
            gradient.append(defaultBG).append(startingValuePercent).append("%, ");
            gradient.append(defaultBG).append("100%); ");               
        }
        return gradient.toString() ;
    }

    public static void main(String[] args) {
        launch(args);
    }
}

and the base CSS file (style.css):

.slider, .progress-bar {
    -slider-track-color: derive(-fx-control-inner-background, -5%) ;
}

.slider .track, .progress-bar .track {
    -fx-background-color: 
        -fx-shadow-highlight-color,
        linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border),
        -slider-track-color ;
}

.progress-bar .bar {
    -fx-background-color: transparent ;
}

enter image description here enter image description here

James_D
  • 201,275
  • 16
  • 291
  • 322
  • If you actually prefer a progress bar, you could do the same CSS trick with the progress bar's track (use 0.0 for `min`, 1.0 for `max`, and the progress for `value`), and just make the bar itself transparent. – James_D Feb 10 '18 at 22:03
  • Sir, thank you. Prefer the slider approach. A clarification, is it possible to change the circle in the slider to a tear drop as seen in the picture I posted initially? – iCoder Feb 11 '18 at 05:13
  • Found this link which shows how to change the icon picture - https://stackoverflow.com/questions/13457549/javafx-2-0-how-can-i-change-the-slider-icon-in-a-slider-to-an-image – iCoder Feb 13 '18 at 03:49