Skip to content

Commit

Permalink
made legend fully styleable
Browse files Browse the repository at this point in the history
  • Loading branch information
ennerf committed Aug 7, 2023
1 parent 3320e7a commit 2a5d513
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 133 deletions.
58 changes: 6 additions & 52 deletions chartfx-chart/src/main/java/io/fair_acc/chartfx/Chart.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,6 @@ public abstract class Chart extends Region implements EventSource {
*/
protected final ChartLayoutAnimator animator = new ChartLayoutAnimator(this);

/**
* When true the chart will display a legend if the chart implementation supports a legend.
*/
private final StyleableBooleanProperty legendVisible = CSS.createBooleanProperty(this, "legendVisible", true, state.onAction(ChartBits.ChartLegend));

protected final ObservableList<Axis> axesList = FXCollections.observableList(new NoDuplicatesList<>());
private final Map<ChartPlugin, Group> pluginGroups = new HashMap<>();
private final ObservableList<ChartPlugin> plugins = FXCollections.observableList(new LinkedList<>());
Expand Down Expand Up @@ -172,31 +167,14 @@ public abstract class Chart extends Region implements EventSource {

protected final TitleLabel titleLabel = StyleUtil.addStyles(new TitleLabel(), "chart-title");

/**
* The side of the chart where the legend should be displayed default value Side.BOTTOM
*/
private final StyleableObjectProperty<Side> legendSide = CSS.createObjectProperty(this, "legendSide", Side.BOTTOM, false,
StyleConverter.getEnumConverter(Side.class), (oldVal, newVal) -> {
AssertUtils.notNull("Side must not be null", newVal);

final Legend legend = getLegend();
if (legend == null) {
return newVal;
}
ChartPane.setSide(legend.getNode(), newVal);
legend.setVertical(newVal.isVertical());

return newVal;
}, state.onAction(ChartBits.ChartLayout));

/**
* The node to display as the Legend. Subclasses can set a node here to be displayed on a side as the legend. If no
* legend is wanted then this can be set to null
*/
private final ObjectProperty<Legend> legend = new SimpleObjectProperty<>(this, "legend", new DefaultLegend()) {
private Legend oldLegend = get();
{
getTitleLegendPane().addSide(getLegendSide(), oldLegend.getNode());
getTitleLegendPane().addSide(oldLegend.getSide(), oldLegend.getNode());
}

@Override
Expand All @@ -208,12 +186,11 @@ protected void invalidated() {
}

if (newLegend != null) {
newLegend.getNode().setVisible(isLegendVisible());
getTitleLegendPane().addSide(getLegendSide(), newLegend.getNode());
getTitleLegendPane().addSide(newLegend.getSide(), newLegend.getNode());
}
super.set(newLegend);
oldLegend = newLegend;
updateLegend(getDatasets(), getRenderers());
fireInvalidated(ChartBits.ChartLegend);
}
};

Expand Down Expand Up @@ -320,14 +297,6 @@ public Chart(Axis... axes) {
menuPane.setTop(getToolBar());

getTitleLegendPane().getChildren().add(titleLabel);

legendVisibleProperty().addListener((ch, old, visible) -> {
if (getLegend() == null) {
return;
}
getLegend().getNode().setVisible(visible);
getLegend().getNode().setManaged(visible);
});
}

@Override
Expand Down Expand Up @@ -435,10 +404,6 @@ public final Legend getLegend() {
return legend.getValue();
}

public final Side getLegendSide() {
return legendSide.get();
}

public final ChartPane getMeasurementPane() {
return measurementPane;
}
Expand Down Expand Up @@ -508,10 +473,6 @@ public final boolean isAnimated() {
return animated.get();
}

public final boolean isLegendVisible() {
return legendVisible.getValue();
}

/**
* @return true: if chart is being visible in Scene/Window
*/
Expand Down Expand Up @@ -678,12 +639,9 @@ public final ObjectProperty<Legend> legendProperty() {
return legend;
}

public final ObjectProperty<Side> legendSideProperty() {
return legendSide;
}

@Deprecated // TODO: used in tests/examples. Should be replaced with getLegend().setVisible(value)?
public final BooleanProperty legendVisibleProperty() {
return legendVisible;
return getLegend().getNode().visibleProperty();
}

public final void setAnimated(final boolean value) {
Expand All @@ -694,12 +652,8 @@ public final void setLegend(final Legend value) {
legend.set(value);
}

public final void setLegendSide(final Side value) {
legendSide.set(value);
}

public final void setLegendVisible(final boolean value) {
legendVisible.set(value);
getLegend().getNode().setVisible(value);
}

public final void setTitle(final String value) {
Expand Down
3 changes: 2 additions & 1 deletion chartfx-chart/src/main/java/io/fair_acc/chartfx/XYChart.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ public XYChart(final Axis... axes) {
gridRenderer.getHorizontalMajorGrid().changeCounterProperty(),
gridRenderer.getHorizontalMinorGrid().changeCounterProperty(),
gridRenderer.getVerticalMajorGrid().changeCounterProperty(),
gridRenderer.getVerticalMinorGrid().changeCounterProperty());
gridRenderer.getVerticalMinorGrid().changeCounterProperty(),
gridRenderer.drawOnTopProperty());

this.setAnimated(false);
getRenderers().addListener(this::rendererChanged);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
import java.util.List;

import io.fair_acc.chartfx.renderer.Renderer;
import io.fair_acc.chartfx.ui.geometry.Side;
import io.fair_acc.dataset.DataSet;
import javafx.scene.Node;

public interface Legend {

Node getNode();

Side getSide();

void setSide(Side side);

boolean isVertical();

void setVertical(boolean value);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package io.fair_acc.chartfx.legend.spi;

import java.lang.ref.PhantomReference;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import io.fair_acc.chartfx.ui.css.CssPropertyFactory;
import io.fair_acc.chartfx.ui.css.StyleUtil;
import io.fair_acc.chartfx.ui.geometry.Side;
import io.fair_acc.chartfx.utils.FXUtils;
import io.fair_acc.chartfx.utils.PropUtil;
import io.fair_acc.dataset.events.ChartBits;
import io.fair_acc.dataset.events.StateListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.css.*;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
Expand All @@ -28,74 +30,40 @@
import io.fair_acc.chartfx.renderer.Renderer;
import io.fair_acc.chartfx.utils.StyleParser;
import io.fair_acc.dataset.DataSet;
import io.fair_acc.dataset.event.EventListener;
import io.fair_acc.dataset.event.UpdateEvent;
import io.fair_acc.dataset.event.UpdatedMetaDataEvent;

/**
* A chart legend that displays a list of items with symbols in a box
*
* @author rstein
*/
public class DefaultLegend extends FlowPane implements Legend {
// TODO: transform static integers to styleable property fields
private static final int GAP = 5;
private static final int SYMBOL_WIDTH = 20;
private static final int SYMBOL_HEIGHT = 20;
private static final PseudoClass disabledClass = PseudoClass.getPseudoClass("disabled");

// -------------- PRIVATE FIELDS ------------------------------------------

private final ListChangeListener<LegendItem> itemsListener = c -> {
getChildren().setAll(getItems());
if (isVisible()) {
requestLayout();
}
};

// -------------- PUBLIC PROPERTIES ----------------------------------------
/**
* The legend items should be laid out vertically in columns rather than horizontally in rows
*/
private final BooleanProperty vertical = new SimpleBooleanProperty(this, "vertical", false) {
@Override
protected void invalidated() {
setOrientation(get() ? Orientation.VERTICAL : Orientation.HORIZONTAL);
}
};

StyleableObjectProperty<Side> side = CSS.createSideProperty(this, Side.BOTTOM);
StyleableDoubleProperty symbolWidth = CSS.createDoubleProperty(this, "symbolWidth", 20);
StyleableDoubleProperty symbolHeight = CSS.createDoubleProperty(this, "symbolHeight", 20);

/**
* The legend items to display in this legend
*/
private final ObjectProperty<ObservableList<LegendItem>> items = new SimpleObjectProperty<>(
this, "items") {
private ObservableList<LegendItem> oldItems = null;

@Override
protected void invalidated() {
if (oldItems != null) {
oldItems.removeListener(itemsListener);
}

final ObservableList<LegendItem> newItems = get();
if (newItems == null) {
getChildren().clear();
} else {
newItems.addListener(itemsListener);
getChildren().setAll(newItems);
}
oldItems = get();
if (isVisible()) {
requestLayout();
}
}
};
private final ObservableList<LegendItem> items = FXCollections.observableArrayList();

public DefaultLegend() {
super(GAP, GAP);
setItems(FXCollections.observableArrayList());
getStyleClass().setAll("chart-legend");
setAlignment(Pos.CENTER);
managedProperty().bind(visibleProperty().and(Bindings.size(items).isNotEqualTo(0)));
items.addListener((ListChangeListener<LegendItem>) c -> getChildren().setAll(items));
PropUtil.runOnChange(this::applyCss, sideProperty());

// TODO:
// (1) The legend does not have a reference to the chart, so for now do a hack
// and try to get it out of the hierarchy.
// (2) The items are currently created with a fixed size before the styling phase,
// so live-updates w/ CSSFX dont work properly without re-instantiating the chart.
PropUtil.runOnChange(() -> FXUtils.tryGetChartParent(this)
.ifPresent(chart -> chart.fireInvalidated(ChartBits.ChartLegend)),
symbolHeight, symbolWidth);
}

@Override
Expand All @@ -111,11 +79,18 @@ protected double computePrefWidth(final double forHeight) {
}

public final ObservableList<LegendItem> getItems() {
return items.get();
return items;
}

public final void setItems(List<LegendItem> items) {
// TODO: remove after changing unit tests
this.items.setAll(items);
}

public LegendItem getNewLegendItem(final Renderer renderer, final DataSet series, final int seriesIndex) {
final Canvas symbol = renderer.drawLegendSymbol(series, seriesIndex, SYMBOL_WIDTH, SYMBOL_HEIGHT);
final Canvas symbol = renderer.drawLegendSymbol(series, seriesIndex,
(int) Math.round(getSymbolWidth()),
(int) Math.round(getSymbolHeight()));
var item = new LegendItem(series.getName(), symbol);
item.setOnMouseClicked(event -> series.setVisible(!series.isVisible()));
Runnable updateCss = () -> item.pseudoClassStateChanged(disabledClass, !series.isVisible());
Expand Down Expand Up @@ -143,15 +118,7 @@ public Node getNode() {
*/
@Override
public final boolean isVertical() {
return verticalProperty().get();
}

public final ObjectProperty<ObservableList<LegendItem>> itemsProperty() {
return items;
}

public final void setItems(final ObservableList<LegendItem> value) {
itemsProperty().set(value);
return getOrientation() == Orientation.VERTICAL;
}

/*
Expand All @@ -160,8 +127,8 @@ public final void setItems(final ObservableList<LegendItem> value) {
* @see io.fair_acc.chartfx.legend.Legend#setVertical(boolean)
*/
@Override
public final void setVertical(final boolean value) {
verticalProperty().set(value);
public final void setVertical(final boolean vertical) {
setOrientation(vertical ? Orientation.VERTICAL : Orientation.HORIZONTAL);
}

/*
Expand Down Expand Up @@ -239,19 +206,60 @@ public void updateLegend(final List<DataSet> dataSets, final List<Renderer> rend
}
}

public final BooleanProperty verticalProperty() {
return vertical;
public Side getSide() {
return side.get();
}

public StyleableObjectProperty<Side> sideProperty() {
return side;
}

public void setSide(Side side) {
this.side.set(side);
}

public double getSymbolWidth() {
return symbolWidth.get();
}

public StyleableDoubleProperty symbolWidthProperty() {
return symbolWidth;
}

public void setSymbolWidth(double symbolWidth) {
this.symbolWidth.set(symbolWidth);
}

public double getSymbolHeight() {
return symbolHeight.get();
}

public StyleableDoubleProperty symbolHeightProperty() {
return symbolHeight;
}

public void setSymbolHeight(double symbolHeight) {
this.symbolHeight.set(symbolHeight);
}

@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}

public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return CSS.getCssMetaData();
}

private static final CssPropertyFactory<DefaultLegend> CSS = new CssPropertyFactory<>(FlowPane.getClassCssMetaData());

/**
* A item to be displayed on a Legend
*/
public static class LegendItem extends Label {
public LegendItem(final String text, final Node symbol) {
StyleUtil.addStyles(this, "chart-legend-item");
setText(text);
getStyleClass().add("chart-legend-item");
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.LEFT);
setSymbol(symbol);
}

Expand Down
Loading

0 comments on commit 2a5d513

Please sign in to comment.