draw better dashboard images

Scaling big plots to small thumbnails results in bad images that barely
show any details.
We solve this by calling gnuplot a second time to generate the
thumbnails. They don't have any labels and are rendered in the required
size, so that not scaling is necessary.
Thumbnails have to be requested explicitly, because it can be expensive
to compute them.
This commit is contained in:
2018-05-01 07:54:10 +02:00
parent f573436219
commit bfcbd0a451
9 changed files with 109 additions and 96 deletions

View File

@@ -42,6 +42,8 @@ public class PlotSettings {
private PlotType plotType; private PlotType plotType;
private boolean generateThumbnail;
public String getQuery() { public String getQuery() {
return query; return query;
} }
@@ -210,4 +212,12 @@ public class PlotSettings {
public PlotType getPlotType() { public PlotType getPlotType() {
return plotType; return plotType;
} }
public void setGenerateThumbnail(final boolean generateThumbnail) {
this.generateThumbnail = generateThumbnail;
}
public boolean isGenerateThumbnail() {
return generateThumbnail;
}
} }

View File

@@ -13,8 +13,6 @@ public class GnuplotFileGenerator {
appendfln(result, "set terminal %s noenhanced size %d,%d", settings.getTerminal(), settings.getWidth(), appendfln(result, "set terminal %s noenhanced size %d,%d", settings.getTerminal(), settings.getWidth(),
settings.getHeight()); settings.getHeight());
appendfln(result, "set datafile separator \"%s\"", settings.getDatafileSeparator()); appendfln(result, "set datafile separator \"%s\"", settings.getDatafileSeparator());
settings.getAggregate().addGnuplotDefinitions(result, settings.getDatafileSeparator(), dataSeries); settings.getAggregate().addGnuplotDefinitions(result, settings.getDatafileSeparator(), dataSeries);
@@ -25,9 +23,9 @@ public class GnuplotFileGenerator {
if (xAxis.isxDataTime()) { if (xAxis.isxDataTime()) {
appendfln(result, "set xdata time"); appendfln(result, "set xdata time");
} }
appendfln(result, "set xtics nomirror rotate by %d", xAxis.getRotateXAxisLabel());
appendfln(result, "set format x \"%s\"", xAxis.getFormatX()); appendfln(result, "set format x \"%s\"", xAxis.getFormatX());
appendfln(result, "set xlabel \"%s\"", xAxis.getXlabel()); appendfln(result, "set xlabel \"%s\"", xAxis.getXlabel());
appendfln(result, "set xtics nomirror rotate by %d", xAxis.getRotateXAxisLabel());
appendfln(result, "set xrange [\"%s\":\"%s\"]", xAxis.getFrom(), xAxis.getTo()); appendfln(result, "set xrange [\"%s\":\"%s\"]", xAxis.getFrom(), xAxis.getTo());
appendfln(result, "set x2label \"Percentile\""); appendfln(result, "set x2label \"Percentile\"");
@@ -56,14 +54,27 @@ public class GnuplotFileGenerator {
// marker lines that show which area will be zoomed // marker lines that show which area will be zoomed
final long minDate = Long.parseLong(settings.getxAxisSettings().getFrom()); final long minDate = Long.parseLong(settings.getxAxisSettings().getFrom());
final long maxDate = Long.parseLong(settings.getxAxisSettings().getTo()); final long maxDate = Long.parseLong(settings.getxAxisSettings().getTo());
appendfln(result, "set arrow from "+(minDate + (maxDate-minDate)*0.25)+","+graphOffset+" rto graph 0,1 lt 3 lc rgb \"#EEEEEE\" nohead"); appendfln(result, "set arrow from " + (minDate + (maxDate - minDate) * 0.25) + "," + graphOffset
appendfln(result, "set arrow from "+(minDate + (maxDate-minDate)*0.75)+","+graphOffset+" rto graph 0,1 lc rgb \"#EEEEEE\" nohead"); + " rto graph 0,1 lt 3 lc rgb \"#EEEEEE\" nohead");
appendfln(result, "set arrow from " + (minDate + (maxDate - minDate) * 0.75) + "," + graphOffset
+ " rto graph 0,1 lc rgb \"#EEEEEE\" nohead");
if (settings.isKeyOutside()) { if (settings.isKeyOutside()) {
appendfln(result, "set key outside"); appendfln(result, "set key outside");
} }
appendfln(result, "set key font \",10\""); appendfln(result, "set key font \",10\"");
if (!settings.isRenderLabels()) {
appendfln(result, "set format x \"\"", xAxis.getFormatX());
appendfln(result, "set xlabel \"\"", xAxis.getXlabel());
appendfln(result, "set x2label \"\"");
appendln(result, "set format x2 \"\"");
appendfln(result, "set ylabel \"\"", settings.getYlabel());
appendln(result, "set format y \"\"");
appendln(result, "set nokey");
}
appendf(result, "plot "); appendf(result, "plot ");
for (final DataSeries dataSerie : dataSeries) { for (final DataSeries dataSerie : dataSeries) {
@@ -78,7 +89,6 @@ public class GnuplotFileGenerator {
builder.append(String.format(format + "\n", args)); builder.append(String.format(format + "\n", args));
} }
private void appendln(final StringBuilder builder, final String string) { private void appendln(final StringBuilder builder, final String string) {
builder.append(string + "\n"); builder.append(string + "\n");
} }

View File

@@ -11,11 +11,9 @@ public class GnuplotSettings {
private int width = 1600; private int width = 1600;
private String timefmt = "%s"; // time as unix epoch, but as double private String timefmt = "%s"; // time as unix epoch, but as double
// set datafile separator // set datafile separator
private String datafileSeparator = ","; private String datafileSeparator = ",";
// set ylabel // set ylabel
private String ylabel = "Duration in ms"; private String ylabel = "Duration in ms";
@@ -27,25 +25,20 @@ public class GnuplotSettings {
private boolean keyOutside = false; private boolean keyOutside = false;
private XAxisSettings xAxisSettings = new XAxisSettings(); private XAxisSettings xAxisSettings = new XAxisSettings();
private boolean renderLabels = true;
public GnuplotSettings(final Path output) { public GnuplotSettings(final Path output) {
this.output = output; this.output = output;
} }
public XAxisSettings getxAxisSettings() { public XAxisSettings getxAxisSettings() {
return xAxisSettings; return xAxisSettings;
} }
public void setxAxisSettings(final XAxisSettings xAxisSettings) {
public void setxAxisSettings(XAxisSettings xAxisSettings) {
this.xAxisSettings = xAxisSettings; this.xAxisSettings = xAxisSettings;
} }
public String getTerminal() { public String getTerminal() {
return terminal; return terminal;
} }
@@ -78,7 +71,6 @@ public class GnuplotSettings {
this.timefmt = timefmt; this.timefmt = timefmt;
} }
public String getDatafileSeparator() { public String getDatafileSeparator() {
return datafileSeparator; return datafileSeparator;
} }
@@ -107,7 +99,7 @@ public class GnuplotSettings {
return yAxisScale; return yAxisScale;
} }
public void setAggregate(AggregateHandler aggregate) { public void setAggregate(final AggregateHandler aggregate) {
this.aggregate = aggregate; this.aggregate = aggregate;
} }
@@ -115,7 +107,7 @@ public class GnuplotSettings {
return aggregate; return aggregate;
} }
public void setKeyOutside(boolean keyOutside) { public void setKeyOutside(final boolean keyOutside) {
this.keyOutside = keyOutside; this.keyOutside = keyOutside;
} }
@@ -123,6 +115,14 @@ public class GnuplotSettings {
return keyOutside; return keyOutside;
} }
public void renderLabels(final boolean renderLabels) {
this.renderLabels = renderLabels;
}
public boolean isRenderLabels() {
return renderLabels;
}
// plot 'sample.txt' using 1:2 title 'Bytes' with linespoints 2 // plot 'sample.txt' using 1:2 title 'Bytes' with linespoints 2
} }

View File

@@ -1,7 +1,5 @@
package org.lucares.recommind.logs; package org.lucares.recommind.logs;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@@ -23,8 +21,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.imageio.ImageIO;
import org.lucares.pdb.api.Entry; import org.lucares.pdb.api.Entry;
import org.lucares.pdb.api.GroupResult; import org.lucares.pdb.api.GroupResult;
import org.lucares.pdb.api.Result; import org.lucares.pdb.api.Result;
@@ -111,6 +107,7 @@ public class ScatterPlot implements ConcretePlotter {
DataSeries.setColors(dataSeries); DataSeries.setColors(dataSeries);
final Path outputFile = Files.createTempFile(outputDir, "", ".png"); final Path outputFile = Files.createTempFile(outputDir, "", ".png");
{
final Gnuplot gnuplot = new Gnuplot(tmpBaseDir); final Gnuplot gnuplot = new Gnuplot(tmpBaseDir);
final GnuplotSettings gnuplotSettings = new GnuplotSettings(outputFile); final GnuplotSettings gnuplotSettings = new GnuplotSettings(outputFile);
gnuplotSettings.setHeight(height); gnuplotSettings.setHeight(height);
@@ -121,9 +118,25 @@ public class ScatterPlot implements ConcretePlotter {
gnuplotSettings.setAggregate(plotSettings.getAggregate()); gnuplotSettings.setAggregate(plotSettings.getAggregate());
gnuplotSettings.setKeyOutside(plotSettings.isKeyOutside()); gnuplotSettings.setKeyOutside(plotSettings.isKeyOutside());
gnuplot.plot(gnuplotSettings, dataSeries); gnuplot.plot(gnuplotSettings, dataSeries);
}
final Path thumbnail = createOptionalThumbnail(outputFile, plotSettings.getThumbnailMaxWidth(), final Path thumbnail;
plotSettings.getThumbnailMaxHeight()); if (plotSettings.isGenerateThumbnail()) {
thumbnail = Files.createTempFile(outputDir, "", ".png");
final Gnuplot gnuplot = new Gnuplot(tmpBaseDir);
final GnuplotSettings gnuplotSettings = new GnuplotSettings(thumbnail);
gnuplotSettings.setHeight(plotSettings.getThumbnailMaxHeight());
gnuplotSettings.setWidth(plotSettings.getThumbnailMaxWidth());
defineXAxis(gnuplotSettings, plotSettings.dateFrom(), plotSettings.dateTo());
gnuplotSettings.setYAxisScale(plotSettings.getYAxisScale());
gnuplotSettings.setAggregate(plotSettings.getAggregate());
gnuplotSettings.setKeyOutside(false);
gnuplotSettings.renderLabels(false);
gnuplot.plot(gnuplotSettings, dataSeries);
} else {
thumbnail = null;
}
return new PlotResult(outputFile, dataSeries, thumbnail); return new PlotResult(outputFile, dataSeries, thumbnail);
} catch (final InterruptedException e) { } catch (final InterruptedException e) {
@@ -137,43 +150,6 @@ public class ScatterPlot implements ConcretePlotter {
} }
} }
private static BufferedImage resizeImage(final BufferedImage originalImage, final Integer img_width,
final Integer img_height) {
final int type = originalImage.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : originalImage.getType();
final BufferedImage resizedImage = new BufferedImage(img_width, img_height, type);
final Graphics2D g = resizedImage.createGraphics();
g.drawImage(originalImage, 0, 0, img_width, img_height, null);
g.dispose();
return resizedImage;
}
private Path createOptionalThumbnail(final Path originalImage, final int thumbnailMaxWidth,
final int thumbnailMaxHeight) {
Path result;
if (thumbnailMaxWidth > 0 && thumbnailMaxHeight > 0 && Files.exists(originalImage)) {
try {
final long start = System.nanoTime();
final BufferedImage image = ImageIO.read(originalImage.toFile());
final BufferedImage thumbnail = resizeImage(image, thumbnailMaxWidth, thumbnailMaxHeight);
final Path thumbnailPath = Files.createTempFile(outputDir, "", ".png");
ImageIO.write(thumbnail, "png", thumbnailPath.toFile());
LOGGER.info("thumbnail creation: " + (System.nanoTime() - start) / 1_000_000.0 + "ms");
result = thumbnailPath;
} catch (final IOException | RuntimeException e) {
LOGGER.warn("failed to scale image", e);
result = null;
}
} else {
result = null;
}
return result;
}
private void defineXAxis(final GnuplotSettings gnuplotSettings, final OffsetDateTime minDate, private void defineXAxis(final GnuplotSettings gnuplotSettings, final OffsetDateTime minDate,
final OffsetDateTime maxDate) { final OffsetDateTime maxDate) {

View File

@@ -130,7 +130,7 @@ public class PdbController implements HardcodedValues, PropertyKeys {
final String thumbnailUrl = result.getThumbnailPath() != null final String thumbnailUrl = result.getThumbnailPath() != null
? WEB_IMAGE_OUTPUT_PATH + "/" + result.getThumbnailName() ? WEB_IMAGE_OUTPUT_PATH + "/" + result.getThumbnailName()
: imageUrl; : "img/no-thumbnail.png";
final PlotResponseStats stats = PlotResponseStats.fromDataSeries(result.getDataSeries()); final PlotResponseStats stats = PlotResponseStats.fromDataSeries(result.getDataSeries());
final PlotResponse plotResponse = new PlotResponse(stats, imageUrl, thumbnailUrl); final PlotResponse plotResponse = new PlotResponse(stats, imageUrl, thumbnailUrl);
@@ -184,6 +184,7 @@ public class PdbController implements HardcodedValues, PropertyKeys {
plotSettings.setPlotType(plotType); plotSettings.setPlotType(plotType);
plotSettings.setAggregate(PlotSettingsTransformer.toAggregateInternal(aggregate)); plotSettings.setAggregate(PlotSettingsTransformer.toAggregateInternal(aggregate));
plotSettings.setKeyOutside(keyOutside); plotSettings.setKeyOutside(keyOutside);
plotSettings.setGenerateThumbnail(false);
if (plotterLock.tryLock()) { if (plotterLock.tryLock()) {
try { try {

View File

@@ -26,6 +26,7 @@ class PlotSettingsTransformer {
result.setKeyOutside(request.isKeyOutside()); result.setKeyOutside(request.isKeyOutside());
result.setThumbnailMaxWidth(request.getThumbnailMaxWidth()); result.setThumbnailMaxWidth(request.getThumbnailMaxWidth());
result.setThumbnailMaxHeight(request.getThumbnailMaxHeight()); result.setThumbnailMaxHeight(request.getThumbnailMaxHeight());
result.setGenerateThumbnail(request.isGenerateThumbnail());
return result; return result;
} }

View File

@@ -35,6 +35,8 @@ public class PlotRequest {
private boolean keyOutside; private boolean keyOutside;
private boolean generateThumbnail;
public String getQuery() { public String getQuery() {
return query; return query;
} }
@@ -154,4 +156,13 @@ public class PlotRequest {
public boolean isKeyOutside() { public boolean isKeyOutside() {
return keyOutside; return keyOutside;
} }
public boolean isGenerateThumbnail() {
return generateThumbnail;
}
public void setGenerateThumbnail(final boolean generateThumbnail) {
this.generateThumbnail = generateThumbnail;
}
} }

View File

@@ -189,16 +189,18 @@ textarea {
float: left; float: left;
background: white; background: white;
box-shadow: 5px 5px 10px 0px #aaa; box-shadow: 5px 5px 10px 0px #aaa;
position: relative;
} }
.dashboard-item img { .dashboard-item img {
max-width: 300px; max-width: 300px;
max-height: 180px; max-height: 200px;
display:block; /* removes 3 pixels extra height around the image */ display:block; /* removes 3 pixels extra height around the image */
} }
.dashboard-item fieldValue{ .dashboard-item .fieldValue{
position: absolute;
bottom: 0;
} }
#result-view-dashboard-image-viewer { #result-view-dashboard-image-viewer {

View File

@@ -841,9 +841,9 @@ function plotCurrent()
var splitByField = splitBy['field']; var splitByField = splitBy['field'];
var splitByValue = splitBy['values'][splitBy['index']]; var splitByValue = splitBy['values'][splitBy['index']];
var query = createQuery(originalQuery, splitByField, splitByValue); var query = createQuery(originalQuery, splitByField, splitByValue);
sendPlotRequest(query); sendPlotRequest(query, false);
}else{ }else{
sendPlotRequest(data.searchBar.query); sendPlotRequest(data.searchBar.query, false);
} }
} }
@@ -869,7 +869,7 @@ function groupBy()
return result; return result;
} }
function createRequest(query){ function createRequest(query, generateThumbnail){
var request = {}; var request = {};
request['query'] = query; request['query'] = query;
request['height'] = Math.floor($('#result').height()); request['height'] = Math.floor($('#result').height());
@@ -883,12 +883,13 @@ function createRequest(query){
request['plotType'] = data.searchBar.plotType; request['plotType'] = data.searchBar.plotType;
request['aggregate'] = data.searchBar.aggregate; request['aggregate'] = data.searchBar.aggregate;
request['keyOutside'] = data.searchBar.keyOutside; request['keyOutside'] = data.searchBar.keyOutside;
request['generateThumbnail'] = generateThumbnail;
return request; return request;
} }
function sendPlotRequest(query){ function sendPlotRequest(query, generateThumbnail){
const request = createRequest(query); const request = createRequest(query, generateThumbnail);
const success = function(response){ const success = function(response){
data.resultView.imageUrl = response.imageUrl; data.resultView.imageUrl = response.imageUrl;
@@ -970,7 +971,8 @@ function createDashboardItem(originalQuery, field, imageHeight, imageWidth)
if (data.dashboard.toBeRendered.length > 0) { if (data.dashboard.toBeRendered.length > 0) {
var fieldValue = data.dashboard.toBeRendered.pop(); var fieldValue = data.dashboard.toBeRendered.pop();
const query = createQuery(originalQuery, field, fieldValue); const query = createQuery(originalQuery, field, fieldValue);
const request = createRequest(query); const generateThumbnail = true;
const request = createRequest(query, generateThumbnail);
request['height'] = imageHeight; request['height'] = imageHeight;
request['width'] = imageWidth; request['width'] = imageWidth;
request['thumbnailMaxWidth'] = 300; request['thumbnailMaxWidth'] = 300;