-
-
Notifications
You must be signed in to change notification settings - Fork 94
Enhance plot plugin with configurable chart options and sequential image loading #196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ | |
| import java.io.OutputStreamWriter; | ||
| import java.io.PrintStream; | ||
| import java.nio.charset.Charset; | ||
| import java.text.DecimalFormat; | ||
| import java.text.NumberFormat; | ||
| import java.text.SimpleDateFormat; | ||
| import java.util.ArrayList; | ||
|
|
@@ -260,6 +261,30 @@ | |
| @SuppressWarnings("visibilitymodifier") | ||
| public String yaxisMaximum; | ||
|
|
||
| /** | ||
| * Custom chart width. When empty or null, DEFAULT_WIDTH (750) is used. | ||
| */ | ||
| @SuppressWarnings("visibilitymodifier") | ||
| public String chartWidth; | ||
|
|
||
| /** | ||
| * Custom chart height. When empty or null, DEFAULT_HEIGHT (450) is used. | ||
| */ | ||
| @SuppressWarnings("visibilitymodifier") | ||
| public String chartHeight; | ||
|
|
||
| /** | ||
| * Whether to skip data points with zero or NaN Y values. | ||
| */ | ||
| @SuppressWarnings("visibilitymodifier") | ||
| public boolean skipZeroValues; | ||
|
|
||
| /** | ||
| * Whether to use decimal format on Y-axis to avoid scientific notation. | ||
| */ | ||
| @SuppressWarnings("visibilitymodifier") | ||
| public boolean useDecimalFormat; | ||
|
|
||
| static class Label implements Comparable<Label> { | ||
| private final Integer buildNum; | ||
| private final String buildDate; | ||
|
|
@@ -348,7 +373,11 @@ | |
| boolean logarithmic, | ||
| String yaxisMinimum, | ||
| String yaxisMaximum, | ||
| String description) { | ||
| String description, | ||
| String chartWidth, | ||
| String chartHeight, | ||
| boolean skipZeroValues, | ||
| boolean useDecimalFormat) { | ||
| this.title = title; | ||
| this.yaxis = yaxis; | ||
| this.group = group; | ||
|
|
@@ -362,6 +391,10 @@ | |
| this.yaxisMinimum = yaxisMinimum; | ||
| this.yaxisMaximum = yaxisMaximum; | ||
| this.description = description; | ||
| this.chartWidth = chartWidth; | ||
| this.chartHeight = chartHeight; | ||
| this.skipZeroValues = skipZeroValues; | ||
| this.useDecimalFormat = useDecimalFormat; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -376,261 +409,316 @@ | |
| String csvFileName, | ||
| String style, | ||
| boolean useDescr) { | ||
| this(title, yaxis, group, numBuilds, csvFileName, style, useDescr, false, false, false, null, null, null); | ||
| this( | ||
| title, | ||
| yaxis, | ||
| group, | ||
| numBuilds, | ||
| csvFileName, | ||
| style, | ||
| useDescr, | ||
| false, | ||
| false, | ||
| false, | ||
| null, | ||
| null, | ||
| null, | ||
| null, | ||
| null, | ||
| false, | ||
| false); | ||
| } | ||
|
|
||
| // needed for serialization | ||
| public Plot() {} | ||
|
|
||
| public boolean getKeepRecords() { | ||
| return keepRecords; | ||
| } | ||
|
|
||
| public boolean getExclZero() { | ||
| return exclZero; | ||
| } | ||
|
|
||
| public boolean isLogarithmic() { | ||
| return logarithmic; | ||
| } | ||
|
|
||
| public boolean hasYaxisMinimum() { | ||
| return (getYaxisMinimum() != null); | ||
| } | ||
|
|
||
| public Double getYaxisMinimum() { | ||
| return getDoubleFromString(yaxisMinimum); | ||
| } | ||
|
|
||
| public boolean hasYaxisMaximum() { | ||
| return (getYaxisMaximum() != null); | ||
| } | ||
|
|
||
| public Double getYaxisMaximum() { | ||
| return getDoubleFromString(yaxisMaximum); | ||
| } | ||
|
|
||
| public String getChartWidth() { | ||
| return chartWidth; | ||
| } | ||
|
|
||
| public String getChartHeight() { | ||
| return chartHeight; | ||
| } | ||
|
|
||
| public boolean getSkipZeroValues() { | ||
| return skipZeroValues; | ||
| } | ||
|
|
||
| public boolean getUseDecimalFormat() { | ||
| return useDecimalFormat; | ||
| } | ||
|
|
||
| public int getEffectiveWidth() { | ||
| if (!StringUtils.isEmpty(chartWidth)) { | ||
| try { | ||
| int w = Integer.parseInt(chartWidth.trim()); | ||
| if (w > 0) return w; | ||
| } catch (NumberFormatException ignored) { | ||
| } | ||
| } | ||
| return DEFAULT_WIDTH; | ||
| } | ||
|
|
||
| public int getEffectiveHeight() { | ||
| if (!StringUtils.isEmpty(chartHeight)) { | ||
| try { | ||
| int h = Integer.parseInt(chartHeight.trim()); | ||
| if (h > 0) return h; | ||
| } catch (NumberFormatException ignored) { | ||
| } | ||
| } | ||
| return DEFAULT_HEIGHT; | ||
| } | ||
|
|
||
| public Double getDoubleFromString(String input) { | ||
| Double result = null; | ||
| if (!StringUtils.isEmpty(input)) { | ||
| try { | ||
| result = Double.parseDouble(input); | ||
| } catch (NumberFormatException nfe) { | ||
| LOGGER.log( | ||
| Level.INFO, | ||
| "Failed to parse double from '" + input + "'. Not a problem, result already set", | ||
| nfe); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| public int compareTo(Plot o) { | ||
| if (title == null) { | ||
| return o == null || o.getTitle() == null ? 0 : -1; | ||
| } | ||
| if (o == null || o.getTitle() == null) { | ||
| return 1; | ||
| } | ||
| return title.compareTo(o.getTitle()); | ||
| } | ||
|
|
||
| public boolean equals(Object o) { | ||
| return o instanceof Plot && this.compareTo((Plot) o) == 0; | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return this.title.hashCode(); | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return "TITLE(" + getTitle() + "),YAXIS(" + yaxis + "),NUMSERIES(" | ||
| + CollectionUtils.size(series) + "),GROUP(" + group | ||
| + "),NUMBUILDS(" + numBuilds + "),RIGHTBUILDNUM(" | ||
| + getRightBuildNum() + "),HASLEGEND(" + hasLegend() | ||
| + "),ISLOGARITHMIC(" + isLogarithmic() + "),YAXISMINIMUM(" | ||
| + yaxisMinimum + "),YAXISMAXIMUM(" + yaxisMaximum | ||
| + "),FILENAME(" + getCsvFileName() + "),DESCRIPTION(" | ||
| + getDescription() + ")"; | ||
| } | ||
|
|
||
| public String getYaxis() { | ||
| return yaxis; | ||
| } | ||
|
|
||
| public List<Series> getSeries() { | ||
| return series; | ||
| } | ||
|
|
||
| public String getGroup() { | ||
| return group; | ||
| } | ||
|
|
||
| public String getCsvFileName() { | ||
| if (StringUtils.isBlank(csvFileName) && project != null) { | ||
| try { | ||
| csvFileName = File.createTempFile("plot-", ".csv", project.getRootDir()) | ||
| .getName(); | ||
| LOGGER.log(Level.WARNING, "Loading " + csvFileName); | ||
| } catch (IOException e) { | ||
| LOGGER.log(Level.SEVERE, "Unable to create temporary CSV file.", e); | ||
| } | ||
| } | ||
| return csvFileName; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the title for the plot from the "title" parameter in the given | ||
| * StaplerRequest. | ||
| */ | ||
| private void setTitle(StaplerRequest2 req) { | ||
| urlTitle = req.getParameter("title"); | ||
| } | ||
|
|
||
| private String getURLTitle() { | ||
| return urlTitle != null ? urlTitle : title; | ||
| } | ||
|
|
||
| public String getTitle() { | ||
| return title; | ||
| } | ||
|
|
||
| private void setStyle(StaplerRequest2 req) { | ||
| urlStyle = req.getParameter("style"); | ||
| } | ||
|
|
||
| private String getUrlStyle() { | ||
| return urlStyle != null ? urlStyle : (style != null ? style : ""); | ||
| } | ||
|
|
||
| private void setUseDescr(StaplerRequest2 req) { | ||
| String u = req.getParameter("usedescr"); | ||
| if (u == null) { | ||
| urlUseDescr = null; | ||
| } else { | ||
| urlUseDescr = "on".equalsIgnoreCase(u) || "true".equalsIgnoreCase(u); | ||
| } | ||
| } | ||
|
|
||
| private boolean getUrlUseDescr() { | ||
| return urlUseDescr != null ? urlUseDescr : useDescr; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the "hasLegend" parameter in the given StaplerRequest. If the | ||
| * parameter doesn't exist then a default is used. | ||
| */ | ||
| private void setHasLegend(StaplerRequest2 req) { | ||
| String legend = req.getParameter("haslegend"); | ||
| hasLegend = legend == null || "on".equalsIgnoreCase(legend) || "true".equalsIgnoreCase(legend); | ||
| } | ||
|
|
||
| public boolean hasLegend() { | ||
| return hasLegend; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the number of builds to plot from the "numbuilds" parameter in the | ||
| * given StaplerRequest. If the parameter doesn't exist or isn't an integer | ||
| * then a default is used. | ||
| */ | ||
| private void setNumBuilds(StaplerRequest2 req) { | ||
| urlNumBuilds = req.getParameter("numbuilds"); | ||
| if (urlNumBuilds != null) { | ||
| try { | ||
| // simply try and parse the string to see if it's a valid | ||
| // number, throw away the result. | ||
| Integer.parseInt(urlNumBuilds); | ||
| } catch (NumberFormatException nfe) { | ||
| urlNumBuilds = null; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public String getURLNumBuilds() { | ||
| return urlNumBuilds != null ? urlNumBuilds : numBuilds; | ||
| } | ||
|
|
||
| public String getNumBuilds() { | ||
| return numBuilds; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the description of the plot from the "description" parameter in the | ||
| * given StaplerRequest. If the parameter doesn't exist or isn't an string | ||
| * then a default is used. | ||
| */ | ||
| private void setDescription(StaplerRequest2 req) { | ||
| description = req.getParameter("description"); | ||
| } | ||
|
|
||
| public String getDescription() { | ||
| return description; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the right-most build number shown on the plot from the | ||
| * "rightbuildnum" parameter in the given StaplerRequest. If the parameter | ||
| * doesn't exist or isn't an integer then a default is used. | ||
| */ | ||
| private void setRightBuildNum(StaplerRequest2 req) { | ||
| String build = req.getParameter("rightbuildnum"); | ||
| if (StringUtils.isBlank(build)) { | ||
| rightBuildNum = Integer.MAX_VALUE; | ||
| } else { | ||
| try { | ||
| rightBuildNum = Integer.parseInt(build); | ||
| } catch (NumberFormatException nfe) { | ||
| LOGGER.log(Level.SEVERE, "Exception converting to integer", nfe); | ||
| rightBuildNum = Integer.MAX_VALUE; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private int getRightBuildNum() { | ||
| return rightBuildNum; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the plot width from the "width" parameter in the given | ||
| * StaplerRequest. If the parameter doesn't exist or isn't an integer then a | ||
| * default is used. | ||
| */ | ||
| private void setWidth(StaplerRequest2 req) { | ||
| String w = req.getParameter("width"); | ||
| if (w == null) { | ||
| width = DEFAULT_WIDTH; | ||
| width = getEffectiveWidth(); | ||
| } else { | ||
| try { | ||
| width = Integer.parseInt(w); | ||
| } catch (NumberFormatException nfe) { | ||
| LOGGER.log(Level.SEVERE, "Exception converting to integer", nfe); | ||
| width = DEFAULT_WIDTH; | ||
| width = getEffectiveWidth(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private int getWidth() { | ||
| return width; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the plot height from the "height" parameter in the given | ||
| * StaplerRequest. If the parameter doesn't exist or isn't an integer then a | ||
| * default is used. | ||
| */ | ||
| private void setHeight(StaplerRequest2 req) { | ||
| String h = req.getParameter("height"); | ||
| if (h == null) { | ||
| height = DEFAULT_HEIGHT; | ||
| height = getEffectiveHeight(); | ||
| } else { | ||
| try { | ||
| height = Integer.parseInt(h); | ||
| } catch (NumberFormatException nfe) { | ||
| LOGGER.log(Level.SEVERE, "Exception converting to integer", nfe); | ||
| height = DEFAULT_HEIGHT; | ||
| height = getEffectiveHeight(); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -753,127 +841,140 @@ | |
| continue; | ||
| } | ||
|
|
||
| if (skipZeroValues) { | ||
| try { | ||
| double y = Double.parseDouble(point.getYvalue()); | ||
| if (y == 0.0 || Double.isNaN(y)) continue; | ||
| } catch (NumberFormatException ignored) { | ||
| continue; | ||
| } | ||
| } | ||
|
|
||
|
Comment on lines
+844
to
+852
|
||
| rawPlotData.add(new String[] { | ||
| point.getYvalue(), | ||
| point.getLabel(), | ||
| run.getNumber() + "", // convert to a string | ||
| run.getTimestamp().getTimeInMillis() + "", | ||
| point.getUrl() | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // save the updated plot data to disk | ||
| savePlotData(); | ||
| } | ||
|
|
||
| /** | ||
| * Generates the plot and stores it in the plot instance variable. | ||
| * | ||
| * @param forceGenerate if true, force the plot to be re-generated even if the on-disk | ||
| * data hasn't changed | ||
| */ | ||
| private void generatePlot(boolean forceGenerate) { | ||
| // LOGGER.info("Determining if we should generate plot " + | ||
| // getCsvFileName()); | ||
| File csvFile = new File(project.getRootDir(), getCsvFileName()); | ||
| if (csvFile.lastModified() == csvLastModification && plot != null && !forceGenerate) { | ||
| // data hasn't changed so don't regenerate the plot | ||
| return; | ||
| } | ||
| if (rawPlotData == null || csvFile.lastModified() > csvLastModification) { | ||
| // data has changed or has not been loaded so load it now | ||
| loadPlotData(); | ||
| } | ||
| // LOGGER.info("Generating plot " + getCsvFileName()); | ||
| csvLastModification = csvFile.lastModified(); | ||
| PlotCategoryDataset dataset = new PlotCategoryDataset(); | ||
| for (String[] record : rawPlotData) { | ||
| // record: series y-value, series label, build number, build date, | ||
| // url | ||
| int buildNum; | ||
| try { | ||
| buildNum = Integer.parseInt(record[2]); | ||
| if (!reportBuild(buildNum) || buildNum > getRightBuildNum()) { | ||
| continue; // skip this record | ||
| } | ||
| } catch (NumberFormatException nfe) { | ||
| LOGGER.log(Level.SEVERE, "Exception converting to integer", nfe); | ||
| continue; // skip this record all together | ||
| } | ||
| Number value; | ||
| try { | ||
| value = Integer.parseInt(record[0]); | ||
| } catch (NumberFormatException nfe) { | ||
| try { | ||
| value = Double.parseDouble(record[0]); | ||
| } catch (NumberFormatException nfe2) { | ||
| LOGGER.log(Level.SEVERE, "Exception converting to number", nfe2); | ||
| continue; // skip this record all together | ||
| } | ||
| } | ||
| Label columnXLabel = getUrlUseDescr() | ||
| ? new Label(record[2], record[3], descriptionForBuild(buildNum)) | ||
| : new Label(record[2], record[3]); | ||
| String url = null; | ||
| if (record.length >= 5) { | ||
| url = record[4]; | ||
| } | ||
| String rowSeries = record[1]; | ||
| dataset.setValue(value, url, rowSeries, columnXLabel); | ||
| } | ||
|
|
||
| String builds = getURLNumBuilds(); | ||
| int buildsNumber; | ||
| if (StringUtils.isBlank(builds)) { | ||
| buildsNumber = Integer.MAX_VALUE; | ||
| } else { | ||
| try { | ||
| buildsNumber = Integer.parseInt(builds); | ||
| } catch (NumberFormatException nfe) { | ||
| LOGGER.log(Level.SEVERE, "Exception converting to integer", nfe); | ||
| buildsNumber = Integer.MAX_VALUE; | ||
| } | ||
| } | ||
|
|
||
| dataset.clipDataset(buildsNumber); | ||
| plot = createChart(dataset); | ||
| CategoryPlot categoryPlot = (CategoryPlot) plot.getPlot(); | ||
| categoryPlot.setDomainGridlinePaint(Color.black); | ||
| categoryPlot.setRangeGridlinePaint(Color.black); | ||
| categoryPlot.setDrawingSupplier(Plot.SUPPLIER); | ||
| CategoryAxis domainAxis = new ShiftedCategoryAxis(Messages.Plot_Build()); | ||
| categoryPlot.setDomainAxis(domainAxis); | ||
| domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_90); | ||
| domainAxis.setLowerMargin(0.0); | ||
| domainAxis.setUpperMargin(0.03); | ||
| domainAxis.setCategoryMargin(0.0); | ||
| for (Object category : dataset.getColumnKeys()) { | ||
| Label label = (Label) category; | ||
| if (label.text != null) { | ||
| domainAxis.addCategoryLabelToolTip(label, label.numDateString()); | ||
| } else { | ||
| domainAxis.addCategoryLabelToolTip(label, descriptionForBuild(label.buildNum)); | ||
| } | ||
| } | ||
| // Replace the range axis by a logarithmic axis if the option is | ||
| // selected | ||
| if (isLogarithmic()) { | ||
| LogarithmicAxis logAxis = new LogarithmicAxis(getYaxis()); | ||
| categoryPlot.setRangeAxis(logAxis); | ||
| } | ||
|
|
||
| // optionally exclude zero as default y-axis value | ||
| ValueAxis rangeAxis = categoryPlot.getRangeAxis(); | ||
| if ((rangeAxis != null) && (rangeAxis instanceof NumberAxis)) { | ||
| if (hasYaxisMinimum()) { | ||
| rangeAxis.setLowerBound(getYaxisMinimum()); | ||
| } | ||
| if (hasYaxisMaximum()) { | ||
| rangeAxis.setUpperBound(getYaxisMaximum()); | ||
| } | ||
| ((NumberAxis) rangeAxis).setAutoRangeIncludesZero(!getExclZero()); | ||
|
|
||
| if (useDecimalFormat) { | ||
| ((NumberAxis) rangeAxis).setNumberFormatOverride(new DecimalFormat("#.##")); | ||
| } | ||
| } | ||
|
Comment on lines
+975
to
978
|
||
|
|
||
| AbstractCategoryItemRenderer renderer = (AbstractCategoryItemRenderer) categoryPlot.getRenderer(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing
<changelist>to-SNAPSHOT-20260302removes Maven's snapshot semantics (Maven only treats versions ending in-SNAPSHOTas snapshots). This can break CI/release tooling and artifact publication expectations for Jenkins plugins. Keep-SNAPSHOTas the suffix (e.g.,-20260302-SNAPSHOTif a date is required) or avoid encoding build dates in the version entirely.