Skip to content

Commit

Permalink
Merge pull request #338 from jenkinsci/metrics-trend
Browse files Browse the repository at this point in the history
Add metric and style customization of trend charts
  • Loading branch information
uhafner authored Feb 24, 2025
2 parents 055dece + ecebb0a commit 8a289c9
Show file tree
Hide file tree
Showing 20 changed files with 610 additions and 210 deletions.
4 changes: 4 additions & 0 deletions plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
</exclusion>
<exclusion>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import edu.hm.hafner.coverage.Metric;
import edu.hm.hafner.echarts.line.SeriesBuilder;

import java.util.HashMap;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

import io.jenkins.plugins.coverage.metrics.model.Baseline;
import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics;

/**
Expand All @@ -15,38 +15,10 @@
* @author Ullrich Hafner
*/
public class CoverageSeriesBuilder extends SeriesBuilder<CoverageStatistics> {
static final String LINE_COVERAGE = Metric.LINE.toTagName();
static final String BRANCH_COVERAGE = Metric.BRANCH.toTagName();
static final String MUTATION_COVERAGE = Metric.MUTATION.toTagName();
static final String TEST_STRENGTH = Metric.TEST_STRENGTH.toTagName();
static final String MCDC_PAIR_COVERAGE = Metric.MCDC_PAIR.toTagName();
static final String FUNCTION_CALL_COVERAGE = Metric.FUNCTION_CALL.toTagName();
static final String METHOD_COVERAGE = Metric.METHOD.toTagName();

@Override
protected Map<String, Double> computeSeries(final CoverageStatistics statistics) {
Map<String, Double> series = new HashMap<>();

add(statistics, Metric.LINE, LINE_COVERAGE, series);
add(statistics, Metric.BRANCH, BRANCH_COVERAGE, series);
add(statistics, Metric.MUTATION, MUTATION_COVERAGE, series);
add(statistics, Metric.TEST_STRENGTH, TEST_STRENGTH, series);
add(statistics, Metric.MCDC_PAIR, MCDC_PAIR_COVERAGE, series);
add(statistics, Metric.FUNCTION_CALL, FUNCTION_CALL_COVERAGE, series);

if (statistics.containsValue(Metric.MCDC_PAIR, Baseline.PROJECT)
|| statistics.containsValue(Metric.FUNCTION_CALL, Baseline.PROJECT)) {
// Method coverage is only relevant if MC/DC pair or function call coverage is available
add(statistics, Metric.METHOD, METHOD_COVERAGE, series);
}

return series;
}

private void add(final CoverageStatistics statistics, final Metric metric, final String chartId,
final Map<String, Double> series) {
if (statistics.containsValue(metric, Baseline.PROJECT)) {
series.put(chartId, statistics.roundValue(Baseline.PROJECT, metric));
}
return Arrays.stream(Metric.values())
.filter(statistics::containsValue)
.collect(Collectors.toMap(Metric::toTagName, statistics::roundValue));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
import edu.hm.hafner.echarts.BuildResult;
import edu.hm.hafner.echarts.ChartModelConfiguration;
import edu.hm.hafner.echarts.JacksonFacade;
import edu.hm.hafner.echarts.line.LineSeries.FilledMode;
import edu.hm.hafner.echarts.line.LineSeries;
import edu.hm.hafner.echarts.line.LinesChartModel;
import edu.hm.hafner.echarts.line.LinesDataSet;
import edu.hm.hafner.util.VisibleForTesting;

import java.util.List;
import java.util.Set;

import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics;
import io.jenkins.plugins.echarts.JenkinsPalette;
Expand All @@ -21,44 +24,49 @@
* @see JacksonFacade
*/
public class CoverageTrendChart extends TrendChart {
@VisibleForTesting
CoverageTrendChart() {
super(Set.of(), false);
}

/**
* Creates a new {@link CoverageTrendChart}.
*
* @param visibleMetrics
* the metrics to render in the trend chart
* @param useLines
* determines if the chart should use lines or filled areas
*/
public CoverageTrendChart(final Set<Metric> visibleMetrics, final boolean useLines) {
super(visibleMetrics, useLines);
}

@Override
public LinesChartModel create(final Iterable<BuildResult<CoverageStatistics>> results,
final ChartModelConfiguration configuration) {
var dataSet = new CoverageSeriesBuilder().createDataSet(configuration, results);

LinesChartModel model = new LinesChartModel(dataSet);
if (dataSet.isNotEmpty()) {
model.useContinuousRangeAxis();
model.setRangeMax(100);
model.setRangeMin(dataSet.getMinimumValue());
int colorIndex = 0;
for (Metric metric : List.of(Metric.MODULE, Metric.PACKAGE, Metric.FILE, Metric.CLASS, Metric.METHOD)) {
addSeriesIfAvailable(dataSet, model, metric, JenkinsPalette.chartColor(colorIndex++).normal());
}

var filledMode = computeFilledMode(dataSet);
addSeriesIfAvailable(dataSet, model, Metric.LINE, JenkinsPalette.GREEN.normal(), filledMode);
addSeriesIfAvailable(dataSet, model, Metric.BRANCH, JenkinsPalette.GREEN.dark(), filledMode);
addSeriesIfAvailable(dataSet, model, Metric.MUTATION, JenkinsPalette.GREEN.dark(), filledMode);
addSeriesIfAvailable(dataSet, model, Metric.TEST_STRENGTH, JenkinsPalette.GREEN.light(), filledMode);
addSeriesIfAvailable(dataSet, model, Metric.LINE, JenkinsPalette.GREEN.normal());
addSeriesIfAvailable(dataSet, model, Metric.BRANCH, JenkinsPalette.GREEN.dark());
addSeriesIfAvailable(dataSet, model, Metric.INSTRUCTION, JenkinsPalette.GREEN.light());

addSeriesIfAvailable(dataSet, model, Metric.MCDC_PAIR, JenkinsPalette.RED.light(), filledMode);
addSeriesIfAvailable(dataSet, model, Metric.METHOD, JenkinsPalette.RED.normal(), filledMode);
addSeriesIfAvailable(dataSet, model, Metric.FUNCTION_CALL, JenkinsPalette.RED.dark(), filledMode);
}
return model;
}
addSeriesIfAvailable(dataSet, model, Metric.MUTATION, JenkinsPalette.GREEN.dark());
addSeriesIfAvailable(dataSet, model, Metric.TEST_STRENGTH, JenkinsPalette.GREEN.light());

/**
* Returns the filled mode based on the contained coverage values. If the dataset contains MCDC or Function Call
* coverage, then the filled mode is set to LINES, otherwise FILLED.
*
* @param dataSet
* the dataset to check
*
* @return the filled mode
*/
private FilledMode computeFilledMode(final LinesDataSet dataSet) {
if (dataSet.containsSeries(CoverageSeriesBuilder.MCDC_PAIR_COVERAGE)
|| dataSet.containsSeries(CoverageSeriesBuilder.FUNCTION_CALL_COVERAGE)) {
return FilledMode.LINES;
addSeriesIfAvailable(dataSet, model, Metric.MCDC_PAIR, JenkinsPalette.RED.light());
addSeriesIfAvailable(dataSet, model, Metric.FUNCTION_CALL, JenkinsPalette.RED.dark());

model.useContinuousRangeAxis();
model.setRangeMax(100); // Restrict the range to 100%
model.setRangeMin(model.getSeries().stream().map(LineSeries::getData).flatMap(List::stream).mapToDouble(Number::doubleValue).min().orElse(0));
}
return FilledMode.FILLED;
return model;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,67 @@
import edu.hm.hafner.echarts.BuildResult;
import edu.hm.hafner.echarts.ChartModelConfiguration;
import edu.hm.hafner.echarts.JacksonFacade;
import edu.hm.hafner.echarts.line.LineSeries.FilledMode;
import edu.hm.hafner.echarts.line.LineSeries;
import edu.hm.hafner.echarts.line.LinesChartModel;
import edu.hm.hafner.echarts.line.LinesDataSet;

import java.util.List;
import java.util.Set;

import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics;
import io.jenkins.plugins.echarts.JenkinsPalette;

/**
* Builds the Java side model for a trend chart showing the metrics of a project. The number of builds
* to consider is controlled by a {@link ChartModelConfiguration} instance. The created model object can be serialized
* to JSON (e.g., using the {@link JacksonFacade}) and can be used 1:1 as ECharts configuration object in the
* corresponding JS file.
* Builds the Java side model for a trend chart showing the metrics of a project. The number of builds to consider is
* controlled by a {@link ChartModelConfiguration} instance. The created model object can be serialized to JSON (e.g.,
* using the {@link JacksonFacade}) and can be used 1:1 as ECharts configuration object in the corresponding JS file.
*
* @author Ullrich Hafner
* @see JacksonFacade
*/
public class MetricsTrendChart extends TrendChart {
/**
* Creates a new {@link MetricsTrendChart}.
*
* @param visibleMetrics
* the metrics to render in the trend chart
* @param useLines
* determines if the chart should use lines or filled areas
*/
public MetricsTrendChart(final Set<Metric> visibleMetrics, final boolean useLines) {
super(visibleMetrics, useLines);
}

@Override
public LinesChartModel create(final Iterable<BuildResult<CoverageStatistics>> results,
final ChartModelConfiguration configuration) {
LinesDataSet dataSet = new MetricSeriesBuilder().createDataSet(configuration, results);
LinesDataSet dataSet = new CoverageSeriesBuilder().createDataSet(configuration, results);

LinesChartModel model = new LinesChartModel(dataSet);
if (dataSet.isNotEmpty()) {
model.useContinuousRangeAxis();
model.setRangeMax(dataSet.getMaximumValue());
model.setRangeMin(dataSet.getMinimumValue());

int colorIndex = 0;
for (var tag : dataSet.getDataSetIds()) {
Metric metric = Metric.fromTag(tag);
addSeriesIfAvailable(dataSet, model, metric.getDisplayName(),
tag, JenkinsPalette.chartColor(colorIndex++).normal(),
FilledMode.LINES);
tag, JenkinsPalette.chartColor(colorIndex++).normal());
}

model.useContinuousRangeAxis();
// FIXME: once part of ECharts we should remove this code
model.setRangeMax(model.getSeries()
.stream()
.map(LineSeries::getData)
.flatMap(List::stream)
.mapToDouble(Number::doubleValue)
.max()
.orElse(0));
model.setRangeMin(model.getSeries()
.stream()
.map(LineSeries::getData)
.flatMap(List::stream)
.mapToDouble(Number::doubleValue)
.min()
.orElse(0));
}
return model;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
import edu.hm.hafner.echarts.line.LinesChartModel;
import edu.hm.hafner.echarts.line.LinesDataSet;

import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;

import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics;

/**
Expand All @@ -21,15 +25,25 @@
* @see JacksonFacade
*/
public abstract class TrendChart {
private static final Set<Metric> ALL_METRICS = Arrays.stream(Metric.values()).collect(Collectors.toSet());

/**
* Create a Trend Chart Instance that is either for Coverage or Metrics.
* Create a trend chart instance for coverage or software metrics.
*
* @param metrics if the instance should be the metrics
*
* @return the created Trend Chart Instance
*/
public static TrendChart createTrendChart(final boolean metrics) {
return metrics ? new MetricsTrendChart() : new CoverageTrendChart();
return metrics ? new MetricsTrendChart(ALL_METRICS, true) : new CoverageTrendChart(ALL_METRICS, false);
}

private final Set<Metric> visibleMetrics;
private final FilledMode filledMode;

TrendChart(final Set<Metric> visibleMetrics, final boolean useLines) {
this.visibleMetrics = visibleMetrics;
filledMode = useLines ? FilledMode.LINES : FilledMode.FILLED;
}

/**
Expand All @@ -47,8 +61,8 @@ public abstract LinesChartModel create(Iterable<BuildResult<CoverageStatistics>>
ChartModelConfiguration configuration);

void addSeriesIfAvailable(final LinesDataSet dataSet, final LinesChartModel model,
final String name, final String seriesId, final String color, final FilledMode filledMode) {
if (dataSet.containsSeries(seriesId)) {
final String name, final String seriesId, final String color) {
if (dataSet.containsSeries(seriesId) && isVisible(seriesId)) {
LineSeries branchSeries = new LineSeries(name,
color, StackedMode.SEPARATE_LINES, filledMode, dataSet.getSeries(seriesId));

Expand All @@ -57,13 +71,17 @@ void addSeriesIfAvailable(final LinesDataSet dataSet, final LinesChartModel mode
}

void addSeriesIfAvailable(final LinesDataSet dataSet, final LinesChartModel model,
final Metric metric, final String color, final FilledMode filledMode) {
final Metric metric, final String color) {
var tagName = metric.toTagName();
if (dataSet.containsSeries(tagName)) {
if (dataSet.containsSeries(tagName) && isVisible(tagName)) {
LineSeries branchSeries = new LineSeries(metric.getDisplayName(),
color, StackedMode.SEPARATE_LINES, filledMode, dataSet.getSeries(tagName));

model.addSeries(branchSeries);
}
}

private boolean isVisible(final String seriesId) {
return visibleMetrics.contains(Metric.fromTag(seriesId));
}
}
Loading

0 comments on commit 8a289c9

Please sign in to comment.