ai.timefold.solver.benchmark.impl.report.BarChart Maven / Gradle / Ivy
package ai.timefold.solver.benchmark.impl.report;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
public record BarChart>(String id, String title, String xLabel, String yLabel,
List categories, List> datasets, boolean timeOnY)
implements
Chart {
public BarChart {
id = Chart.makeIdUnique(id);
}
@SuppressWarnings("unused") // Used by FreeMarker.
public BigDecimal yMin() {
return LineChart.min(getYValues());
}
private List getYValues() {
return datasets.stream()
.flatMap(d -> d.data().stream())
.filter(Objects::nonNull)
.toList();
}
@SuppressWarnings("unused") // Used by FreeMarker.
public BigDecimal yMax() {
return LineChart.max(getYValues());
}
@SuppressWarnings("unused") // Used by FreeMarker.
public BigDecimal yStepSize() {
return LineChart.stepSize(yMin(), yMax());
}
@SuppressWarnings("unused") // Used by FreeMarker.
public boolean yLogarithmic() {
if (timeOnY) { // Logarithmic time doesn't make sense.
return false;
}
return LineChart.useLogarithmicProblemScale(getYValues());
}
@Override
public void writeToFile(Path parentFolder) {
File file = new File(parentFolder.toFile(), id() + ".js");
file.getParentFile().mkdirs();
Configuration freeMarkerCfg = BenchmarkReport.createFreeMarkerConfiguration();
freeMarkerCfg.setClassForTemplateLoading(getClass(), "");
String templateFilename = "chart-bar.js.ftl";
Map model = new HashMap<>();
model.put("chart", this);
try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) {
Template template = freeMarkerCfg.getTemplate(templateFilename);
template.process(model, writer);
} catch (IOException e) {
throw new IllegalArgumentException("Can not read templateFilename (" + templateFilename
+ ") or write chart file (" + file + ").", e);
} catch (TemplateException e) {
throw new IllegalArgumentException("Can not process Freemarker templateFilename (" + templateFilename
+ ") to chart file (" + id + ").", e);
}
}
public static final class Builder> {
private final Map> data = new LinkedHashMap<>();
private final Set favoriteSet = new HashSet<>();
public Builder add(String dataset, String category, Y y) {
data.computeIfAbsent(dataset, k -> new TreeMap<>())
.put(category, y);
return this;
}
public Set keys() {
return data.keySet();
}
public Builder markFavorite(String dataset) {
favoriteSet.add(dataset);
return this;
}
public BarChart build(String fileName, String title, String xLabel, String yLabel, boolean timeOnY) {
// First find all categories across all data sets.
List categories = data.values().stream()
.flatMap(m -> m.keySet().stream())
.distinct()
.sorted(Comparable::compareTo)
.toList();
/*
* Now gather Y values for every such category, even if some are null.
* Specifying the data like this helps avoid Chart.js quirks during rendering.
*/
List> datasetList = new ArrayList<>(data.size());
for (String datasetLabel : data.keySet()) {
List datasetData = new ArrayList<>(categories.size());
for (String category : categories) {
Y yValue = data.get(datasetLabel).get(category);
datasetData.add(yValue);
}
datasetList.add(new Dataset<>(datasetLabel, datasetData, favoriteSet.contains(datasetLabel)));
}
return new BarChart<>(fileName, title, xLabel, yLabel, categories, datasetList, timeOnY);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy