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 boolean generateThumbnail;
public String getQuery() {
return query;
}
@@ -210,4 +212,12 @@ public class PlotSettings {
public PlotType getPlotType() {
return plotType;
}
public void setGenerateThumbnail(final boolean generateThumbnail) {
this.generateThumbnail = generateThumbnail;
}
public boolean isGenerateThumbnail() {
return generateThumbnail;
}
}

View File

@@ -13,31 +13,29 @@ public class GnuplotFileGenerator {
appendfln(result, "set terminal %s noenhanced size %d,%d", settings.getTerminal(), settings.getWidth(),
settings.getHeight());
appendfln(result, "set datafile separator \"%s\"", settings.getDatafileSeparator());
settings.getAggregate().addGnuplotDefinitions(result, settings.getDatafileSeparator(), dataSeries);
appendfln(result, "set timefmt '%s'", settings.getTimefmt());
final XAxisSettings xAxis = settings.getxAxisSettings();
if (xAxis.isxDataTime()){
if (xAxis.isxDataTime()) {
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 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 x2label \"Percentile\"");
appendln(result, "set format x2 \"%.0f%%\"");
appendfln(result, "set x2tics");
appendfln(result, "set x2range [\"0\":\"100\"]");
final long graphOffset = settings.getYAxisScale() == AxisScale.LINEAR ? 0 : 1;
appendfln(result, "set yrange [\""+graphOffset+"\":]");
appendfln(result, "set yrange [\"" + graphOffset + "\":]");
appendfln(result, "set ylabel \"%s\"", settings.getYlabel());
switch (settings.getYAxisScale()) {
case LINEAR:
@@ -52,20 +50,33 @@ public class GnuplotFileGenerator {
appendfln(result, "set grid");
appendfln(result, "set output \"%s\"", settings.getOutput().toAbsolutePath().toString().replace("\\", "/"));
// marker lines that show which area will be zoomed
final long minDate = Long.parseLong(settings.getxAxisSettings().getFrom());
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.75)+","+graphOffset+" rto graph 0,1 lc rgb \"#EEEEEE\" nohead");
if (settings.isKeyOutside()){
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.75) + "," + graphOffset
+ " rto graph 0,1 lc rgb \"#EEEEEE\" nohead");
if (settings.isKeyOutside()) {
appendfln(result, "set key outside");
}
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 ");
for (final DataSeries dataSerie : dataSeries) {
appendfln(result, dataSerie.getGnuplotPlotDefinition());
}
@@ -77,10 +88,9 @@ public class GnuplotFileGenerator {
private void appendfln(final StringBuilder builder, final String format, final Object... args) {
builder.append(String.format(format + "\n", args));
}
private void appendln(final StringBuilder builder, final String string) {
builder.append(string+"\n");
builder.append(string + "\n");
}
private void appendf(final StringBuilder builder, final String format, final Object... args) {

View File

@@ -11,11 +11,9 @@ public class GnuplotSettings {
private int width = 1600;
private String timefmt = "%s"; // time as unix epoch, but as double
// set datafile separator
private String datafileSeparator = ",";
// set ylabel
private String ylabel = "Duration in ms";
@@ -25,27 +23,22 @@ public class GnuplotSettings {
private AxisScale yAxisScale;
private AggregateHandler aggregate;
private boolean keyOutside = false;
private XAxisSettings xAxisSettings = new XAxisSettings();
private boolean renderLabels = true;
public GnuplotSettings(final Path output) {
this.output = output;
}
public XAxisSettings getxAxisSettings() {
return xAxisSettings;
}
public void setxAxisSettings(XAxisSettings xAxisSettings) {
public void setxAxisSettings(final XAxisSettings xAxisSettings) {
this.xAxisSettings = xAxisSettings;
}
public String getTerminal() {
return terminal;
}
@@ -78,7 +71,6 @@ public class GnuplotSettings {
this.timefmt = timefmt;
}
public String getDatafileSeparator() {
return datafileSeparator;
}
@@ -107,15 +99,15 @@ public class GnuplotSettings {
return yAxisScale;
}
public void setAggregate(AggregateHandler aggregate) {
public void setAggregate(final AggregateHandler aggregate) {
this.aggregate = aggregate;
}
public AggregateHandler getAggregate() {
return aggregate;
}
public void setKeyOutside(boolean keyOutside) {
public void setKeyOutside(final boolean keyOutside) {
this.keyOutside = keyOutside;
}
@@ -123,6 +115,14 @@ public class GnuplotSettings {
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
}

View File

@@ -1,7 +1,5 @@
package org.lucares.recommind.logs;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
@@ -23,8 +21,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import javax.imageio.ImageIO;
import org.lucares.pdb.api.Entry;
import org.lucares.pdb.api.GroupResult;
import org.lucares.pdb.api.Result;
@@ -111,19 +107,36 @@ public class ScatterPlot implements ConcretePlotter {
DataSeries.setColors(dataSeries);
final Path outputFile = Files.createTempFile(outputDir, "", ".png");
final Gnuplot gnuplot = new Gnuplot(tmpBaseDir);
final GnuplotSettings gnuplotSettings = new GnuplotSettings(outputFile);
gnuplotSettings.setHeight(height);
gnuplotSettings.setWidth(width);
defineXAxis(gnuplotSettings, plotSettings.dateFrom(), plotSettings.dateTo());
{
final Gnuplot gnuplot = new Gnuplot(tmpBaseDir);
final GnuplotSettings gnuplotSettings = new GnuplotSettings(outputFile);
gnuplotSettings.setHeight(height);
gnuplotSettings.setWidth(width);
defineXAxis(gnuplotSettings, plotSettings.dateFrom(), plotSettings.dateTo());
gnuplotSettings.setYAxisScale(plotSettings.getYAxisScale());
gnuplotSettings.setAggregate(plotSettings.getAggregate());
gnuplotSettings.setKeyOutside(plotSettings.isKeyOutside());
gnuplot.plot(gnuplotSettings, dataSeries);
gnuplotSettings.setYAxisScale(plotSettings.getYAxisScale());
gnuplotSettings.setAggregate(plotSettings.getAggregate());
gnuplotSettings.setKeyOutside(plotSettings.isKeyOutside());
gnuplot.plot(gnuplotSettings, dataSeries);
}
final Path thumbnail = createOptionalThumbnail(outputFile, plotSettings.getThumbnailMaxWidth(),
plotSettings.getThumbnailMaxHeight());
final Path thumbnail;
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);
} 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,
final OffsetDateTime maxDate) {

View File

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

View File

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

View File

@@ -35,6 +35,8 @@ public class PlotRequest {
private boolean keyOutside;
private boolean generateThumbnail;
public String getQuery() {
return query;
}
@@ -154,4 +156,13 @@ public class PlotRequest {
public boolean isKeyOutside() {
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;
background: white;
box-shadow: 5px 5px 10px 0px #aaa;
position: relative;
}
.dashboard-item img {
max-width: 300px;
max-height: 180px;
max-height: 200px;
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 {

View File

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