Skip to content

Commit

Permalink
Fix pie chart legends
Browse files Browse the repository at this point in the history
Fixes qupath#1062 - at least when using modena.css as a basis.

Also addresses two issues with the pie charts shown when training a pixel/object classifier:
* the legend should now be correct even when more than 8 classes are used
* there is no longer any need to write a temporary css file for styling
  • Loading branch information
petebankhead committed Oct 4, 2022
1 parent 9242648 commit 0efbe4e
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 95 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ This is a work-in-progress.
* Training a new object classifier with the same settings and annotations can give a different result when an image is reopened (https://github.com/qupath/qupath/issues/1016)
* It isn't possible to run cell detection on channels with " in the name (https://github.com/qupath/qupath/issues/1022)
* Fix occasional "One of the arguments' values is out of range" exception with Delaunay triangulation
* The colors used in pie chart legends were sometimes incorrect (https://github.com/qupath/qupath/issues/1062)

### Changes through Bio-Formats 6.10.1
* Bio-Formats 6.10.1 brings several important new features to QuPath, including:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1316,6 +1316,7 @@ private void initialize() {
* Training proportions (pie chart)
*/
pieChart = new PieChart();
pieChart.setAnimated(false);

pieChart.setLabelsVisible(false);
pieChart.setLegendVisible(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ private void initialize() {
pane.add(btnLive, 0, row++, pane.getColumnCount(), 1);

pieChart = new PieChart();
pieChart.setAnimated(false);

// var hierarchy = viewer.getHierarchy();
// Map<PathClass, List<PathObject>> map = hierarchy == null ? Collections.emptyMap() : PathClassificationLabellingHelper.getClassificationMap(hierarchy, false);
Expand Down
107 changes: 56 additions & 51 deletions qupath-gui-fx/src/main/java/qupath/lib/gui/charts/ChartTools.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
Expand All @@ -47,6 +49,7 @@
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.Labeled;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
Expand Down Expand Up @@ -286,77 +289,79 @@ else if (plotArea.getClip() instanceof Rectangle) {
public static <T> void setPieChartData(PieChart chart, Map<T, ? extends Number> counts,
Function<T, String> stringFun, Function<T, Color> colorFun, boolean convertToPercentages, boolean includeTooltips) {

StringBuilder style = null;
if (colorFun != null)
style = new StringBuilder();

// Add the counts in case we need them
double sum = counts.values().stream().mapToDouble(i -> i.doubleValue()).sum();
var newData = new ArrayList<PieChart.Data>();
int ind = 0;
var tooltips = new HashMap<PieChart.Data, Tooltip>();

// Store a map of names & styles so we can update the legend
var legendStyleMap = new LinkedHashMap<String, String>();

// Keep a reference to previous data so we can remove it later
var previousData = new LinkedHashSet<>(chart.getData());

// Add each data point
for (Entry<T, ? extends Number> entry : counts.entrySet()) {

// Compute name
var item = entry.getKey();
String name;
if (stringFun != null)
name = stringFun.apply(item);
else
name = Objects.toString(item);

// Compute value
double value = entry.getValue().doubleValue();
if (convertToPercentages)
value = value / sum * 100.0;
var datum = new PieChart.Data(name, value);
newData.add(datum);

// Add to the chart immediately so that we can access the node
chart.getData().add(datum);
var node = datum.getNode();

if (style != null) {
var color = colorFun.apply(item);
if (color != null) {
String colorString;
// TODO: Use alpha?
// if (color.isOpaque())
colorString = String.format("rgb(%d, %d, %d)", (int)(color.getRed()*255), (int)(color.getGreen()*255), (int)(color.getBlue()*255));
// else
// colorString = String.format("rgba(%f, %f, %f, %f)", color.getRed(), color.getGreen(), color.getBlue(), 1.0-color.getOpacity());
style.append(String.format(".default-color%d.chart-pie { -fx-pie-color: %s; }", ind, colorString)).append("\n");
}
ind++;
// Set style if we have a color
String styleString = "";
var color = colorFun.apply(item);
if (color != null) {
String colorString =
String.format("rgb(%d, %d, %d)", (int)(color.getRed()*255), (int)(color.getGreen()*255), (int)(color.getBlue()*255));

// Warning! This assumes the use of modena.css, which styles using -fx-pie-color
styleString = String.format("-fx-pie-color: %s", colorString);
datum.getNode().setStyle(styleString);
}


// Store the style
var previousStyle = legendStyleMap.put(name, styleString);
if (previousStyle != null && !Objects.equals(styleString, previousStyle)) {
logger.warn("Multiple slices with the label '{}' but different colors - legend colors may be inconsistent!", name);
}

// Set the tooltip if needed
if (includeTooltips) {
var text = String.format("%s: %.1f%%", name, value);
tooltips.put(datum, new Tooltip(text));
String text;
if (convertToPercentages)
text = String.format("%s: %.1f%%", name, value);
else
text = String.format("%s: %.1f", name, value);
Tooltip.install(node, new Tooltip(text));
}
}

if (style != null) {
var styleString = style.toString();
var sheet = piechartStyleSheets.get(styleString);
sheet = null;
if (sheet == null) {
try {
var file = File.createTempFile("chart", ".css");
file.deleteOnExit();
var writer = new PrintWriter(file);
writer.println(styleString);
writer.close();
sheet = file.toURI().toURL().toString();
piechartStyleSheets.put(styleString, sheet);
} catch (IOException e) {
logger.error("Error creating temporary piechart stylesheet", e);
}
}
if (sheet != null)
chart.getStylesheets().setAll(sheet);
// Remove previous data, if needed
if (!previousData.isEmpty()) {
chart.getData().removeAll(previousData);
}

chart.setAnimated(false);
chart.getData().setAll(newData);

if (includeTooltips) {
for (var entry : tooltips.entrySet()) {
Tooltip.install(entry.getKey().getNode(), entry.getValue());

// Try to update the style for the legend
for (var item : chart.lookupAll(".chart-legend-item")) {
if (item instanceof Labeled) {
var label = (Labeled)item;
var style = legendStyleMap.getOrDefault(label.getText(), null);
if (style != null)
label.getGraphic().setStyle(style);
}
}

}


Expand Down
84 changes: 40 additions & 44 deletions qupath-gui-fx/src/main/java/qupath/lib/gui/charts/Charts.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.Window;
import qupath.lib.common.ColorTools;
Expand Down Expand Up @@ -340,7 +341,10 @@ public Stage show() {
public static class PieChartBuilder extends ChartBuilder<PieChartBuilder, PieChart> {

private Map<Object, Number> data = new LinkedHashMap<>();
private Map<Object, Function<Object, String>> stringFun = new LinkedHashMap<>();
private Function<Object, String> stringFun = null;

private boolean tooltips = true;
private boolean convertToPercentages = false;

private PieChartBuilder() {
this.legendVisible = true;
Expand All @@ -353,7 +357,9 @@ protected PieChartBuilder getThis() {

@Override
protected PieChart createNewChart() {
return new PieChart();
var pieChart = new PieChart();
pieChart.setAnimated(false); // Don't animate by default
return pieChart;
}

@Override
Expand All @@ -374,6 +380,26 @@ public PieChartBuilder data(Map<?, ? extends Number> data) {
return this;
}

/**
* Request that pie chart values are converted to percentages for tooltips.
* @param doConvert
* @return
*/
public PieChartBuilder convertToPercentages(boolean doConvert) {
this.convertToPercentages = doConvert;
return this;
}

/**
* Request tooltips to be shown when the cursor hovers over the pie chart.
* @param showTooltips
* @return
*/
public PieChartBuilder tooltips(boolean showTooltips) {
this.tooltips = showTooltips;
return this;
}

/**
* Add a slice to the pie.
* @param name object the slice represents
Expand All @@ -388,48 +414,18 @@ public PieChartBuilder addSlice(Object name, Number value) {
@Override
protected void updateChart(PieChart chart) {
super.updateChart(chart);
var dataMap = new LinkedHashMap<Object, PieChart.Data>();
double sum = 0;
for (var entry : data.entrySet()) {
var key = entry.getKey();
double val = entry.getValue().doubleValue();
sum += val;

String str;
Function<Object, String> fun = stringFun.get(key);
if (fun == null)
str = key == null ? "No key" : key.toString();
else
str = fun.apply(key);

var d = new PieChart.Data(str, val);
dataMap.put(key, d);
}
chart.getData().setAll(dataMap.values());
for (var entry : dataMap.entrySet()) {
var key = entry.getKey();
var data = entry.getValue();
var str = data.getName();
var node = data.getNode();
var val = this.data.get(key).doubleValue();

String tip = String.format("%s (%.1f %%)", str, val/sum*100.0);
Tooltip.install(node, new Tooltip(tip));

// Set to a meaningful color... if we have one
Integer rgb = null;
if (key instanceof PathClass)
rgb = ((PathClass)key).getColor();
else if (key instanceof PathObject)
rgb = ColorToolsFX.getDisplayedColorARGB((PathObject)key);
if (rgb != null) {
node.setStyle(String.format("-fx-background-color: rgb(%d, %d, %d)",
ColorTools.red(rgb),
ColorTools.green(rgb),
ColorTools.blue(rgb)));
}

}
ChartTools.setPieChartData(chart, data,
stringFun,
PieChartBuilder::colorExtractor, convertToPercentages, tooltips);
}

static Color colorExtractor(Object key) {
Integer rgb = null;
if (key instanceof PathClass)
rgb = ((PathClass)key).getColor();
else if (key instanceof PathObject)
rgb = ColorToolsFX.getDisplayedColorARGB((PathObject)key);
return rgb == null ? null : ColorToolsFX.getCachedColor(rgb);
}


Expand Down

0 comments on commit 0efbe4e

Please sign in to comment.