add dashboard

This commit is contained in:
2018-04-27 19:36:31 +02:00
parent 38ffff38de
commit 913057c6df
11 changed files with 304 additions and 60 deletions

View File

@@ -20,6 +20,10 @@ public class PlotSettings {
private int width;
private int thumbnailMaxWidth = 0;
private int thumbnailMaxHeight = 0;
private List<String> groupBy;
private Limit limitBy;
@@ -62,6 +66,22 @@ public class PlotSettings {
this.width = width;
}
public int getThumbnailMaxWidth() {
return thumbnailMaxWidth;
}
public void setThumbnailMaxWidth(final int thumbnailMaxWidth) {
this.thumbnailMaxWidth = thumbnailMaxWidth;
}
public int getThumbnailMaxHeight() {
return thumbnailMaxHeight;
}
public void setThumbnailMaxHeight(final int thumbnailMaxHeight) {
this.thumbnailMaxHeight = thumbnailMaxHeight;
}
public List<String> getGroupBy() {
return groupBy;
}
@@ -160,12 +180,14 @@ public class PlotSettings {
@Override
public String toString() {
return "PlotSettings [query=" + query + ", height=" + height + ", width=" + width + ", groupBy=" + groupBy
return "PlotSettings [query=" + query + ", height=" + height + ", width=" + width + ", thumbnailMaxWidth="
+ thumbnailMaxWidth + ", thumbnailMaxHeight=" + thumbnailMaxHeight + ", groupBy=" + groupBy
+ ", limitBy=" + limitBy + ", limit=" + limit + ", dateFrom=" + dateFrom + ", dateRange=" + dateRange
+ ", axisScale=" + yAxisScale + ", aggregate="+aggregate+", keyOutside="+keyOutside+"]";
+ ", yAxisScale=" + yAxisScale + ", aggregate=" + aggregate + ", keyOutside=" + keyOutside
+ ", plotType=" + plotType + "]";
}
public void setAggregate(AggregateHandler aggregate) {
public void setAggregate(final AggregateHandler aggregate) {
this.aggregate = aggregate;
}
@@ -173,7 +195,7 @@ public class PlotSettings {
return aggregate;
}
public void setKeyOutside(boolean keyOutside) {
public void setKeyOutside(final boolean keyOutside) {
this.keyOutside = keyOutside;
}
@@ -181,7 +203,7 @@ public class PlotSettings {
return keyOutside;
}
public void setPlotType(PlotType plotType) {
public void setPlotType(final PlotType plotType) {
this.plotType = plotType;
}

View File

@@ -101,7 +101,7 @@ public class PercentilePlot implements ConcretePlotter {
gnuplotSettings.setKeyOutside(plotSettings.isKeyOutside());
gnuplot.plot(gnuplotSettings, dataSeries);
return new PlotResult(outputFile, dataSeries);
return new PlotResult(outputFile, dataSeries, null); // TODO thumbail
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Plotting was interrupted.");

View File

@@ -6,11 +6,13 @@ import java.util.List;
public class PlotResult {
private final Path imagePath;
private final List<DataSeries> dataSeries;
private final Path thumbnail;
public PlotResult(final Path imagePath, final List<DataSeries> dataSeries) {
public PlotResult(final Path imagePath, final List<DataSeries> dataSeries, final Path thumbnail) {
super();
this.imagePath = imagePath;
this.dataSeries = dataSeries;
this.thumbnail = thumbnail;
}
public Path getImageName() {
@@ -21,6 +23,14 @@ public class PlotResult {
return imagePath;
}
public Path getThumbnailName() {
return thumbnail.getFileName();
}
public Path getThumbnailPath() {
return thumbnail;
}
public List<DataSeries> getDataSeries() {
return dataSeries;
}

View File

@@ -1,5 +1,7 @@
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;
@@ -21,6 +23,8 @@ 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;
@@ -118,7 +122,10 @@ public class ScatterPlot implements ConcretePlotter {
gnuplotSettings.setKeyOutside(plotSettings.isKeyOutside());
gnuplot.plot(gnuplotSettings, dataSeries);
return new PlotResult(outputFile, dataSeries);
final Path thumbnail = createOptionalThumbnail(outputFile, plotSettings.getThumbnailMaxWidth(),
plotSettings.getThumbnailMaxHeight());
return new PlotResult(outputFile, dataSeries, thumbnail);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Plotting was interrupted.");
@@ -130,6 +137,44 @@ 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) {
try {
final long start = System.nanoTime();
final BufferedImage image = ImageIO.read(originalImage.toFile());
final BufferedImage thumbnail = resizeImage(image, thumbnailMaxWidth, thumbnailMaxHeight);
final Path thumbnailPath = originalImage.getParent()
.resolve(originalImage.getFileName() + ".thumbnail.jpg");
ImageIO.write(thumbnail, "JPG", thumbnailPath.toFile());
LOGGER.info("thumbnail creation: " + (System.nanoTime() - start) / 1_000_000.0 + "ms");
result = thumbnailPath;
} catch (final IOException 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

@@ -101,7 +101,11 @@ public class PdbController implements HardcodedValues {
final String imageUrl = WEB_IMAGE_OUTPUT_PATH + "/" + result.getImageName();
LOGGER.trace("image url: {}", imageUrl);
return new PlotResponse(DataSeries.toMap(result.getDataSeries()), imageUrl);
final String thumbnailUrl = result.getThumbnailPath() != null
? WEB_IMAGE_OUTPUT_PATH + "/" + result.getThumbnailName()
: imageUrl;
return new PlotResponse(DataSeries.toMap(result.getDataSeries()), imageUrl, thumbnailUrl);
} catch (final NoDataPointsException e) {
throw new NotFoundException(e);
} finally {
@@ -127,8 +131,8 @@ public class PdbController implements HardcodedValues {
@RequestParam(name = "plotType", defaultValue = "SCATTER") final PlotType plotType,
@RequestParam(name = "aggregate", defaultValue = "NONE") final Aggregate aggregate,
@RequestParam(name = "keyOutside", defaultValue = "false") final boolean keyOutside,
@RequestParam(name = "height", defaultValue = "1080") final int height,
@RequestParam(name = "width", defaultValue = "1920") final int hidth) {
@RequestParam(name = "width", defaultValue = "1920") final int hidth,
@RequestParam(name = "height", defaultValue = "1080") final int height) {
return (final OutputStream outputStream) -> {
if (StringUtils.isBlank(query)) {

View File

@@ -24,6 +24,8 @@ class PlotSettingsTransformer {
result.setPlotType(request.getPlotType());
result.setAggregate(toAggregateInternal(request.getAggregate()));
result.setKeyOutside(request.isKeyOutside());
result.setThumbnailMaxWidth(request.getThumbnailMaxWidth());
result.setThumbnailMaxHeight(request.getThumbnailMaxHeight());
return result;
}

View File

@@ -13,6 +13,10 @@ public class PlotRequest {
private int width = 1000;
private int thumbnailMaxWidth = 0;
private int thumbnailMaxHeight = 0;
private List<String> groupBy;
private Limit limitBy = Limit.NO_LIMIT;
@@ -55,6 +59,22 @@ public class PlotRequest {
this.height = height;
}
public int getThumbnailMaxWidth() {
return thumbnailMaxWidth;
}
public void setThumbnailMaxWidth(final int thumbnailMaxWidth) {
this.thumbnailMaxWidth = thumbnailMaxWidth;
}
public int getThumbnailMaxHeight() {
return thumbnailMaxHeight;
}
public void setThumbnailMaxHeight(final int thumbnailMaxHeight) {
this.thumbnailMaxHeight = thumbnailMaxHeight;
}
@Override
public String toString() {
return query + ":" + height + "x" + width;
@@ -115,11 +135,11 @@ public class PlotRequest {
return plotType;
}
public void setPlotType(PlotType plotType) {
public void setPlotType(final PlotType plotType) {
this.plotType = plotType;
}
public void setAggregate(Aggregate aggregate) {
public void setAggregate(final Aggregate aggregate) {
this.aggregate = aggregate;
}
@@ -127,7 +147,7 @@ public class PlotRequest {
return aggregate;
}
public void setKeyOutside(boolean keyOutside) {
public void setKeyOutside(final boolean keyOutside) {
this.keyOutside = keyOutside;
}

View File

@@ -5,10 +5,12 @@ import java.util.Map;
public class PlotResponse {
private String imageUrl = "";
private Map<String, Integer> dataSeries;
private final String thumbnailUrl;
public PlotResponse(final Map<String, Integer> dataSeries, final String imageUrl) {
public PlotResponse(final Map<String, Integer> dataSeries, final String imageUrl, final String thumbnailUrl) {
this.dataSeries = dataSeries;
this.imageUrl = imageUrl;
this.thumbnailUrl = thumbnailUrl;
}
public String getImageUrl() {
@@ -19,6 +21,10 @@ public class PlotResponse {
this.imageUrl = imageUrl;
}
public String getThumbnailUrl() {
return thumbnailUrl;
}
public Map<String, Integer> getDataSeries() {
return dataSeries;
}
@@ -29,6 +35,6 @@ public class PlotResponse {
@Override
public String toString() {
return imageUrl + " " + dataSeries;
return imageUrl + " " + dataSeries + " " + thumbnailUrl;
}
}

View File

@@ -83,10 +83,13 @@ textarea {
background: #eee;
margin: 0;
padding: 0;
overflow: hidden;
overflow: auto;
position:relative;
}
#result-view {
}
#filter-bar {
grid-area: filter_bar;
}
@@ -139,11 +142,12 @@ textarea {
margin-right:3px;
}
#result-image {
height: 100%;
}
#result-image img {
display:block; /* removes 3 pixels extra height around the image */
}
#prev_image, #next_image {
position: absolute;
@@ -175,6 +179,25 @@ textarea {
top: 0;
}
.dashboard-item {
width: 300px;
height: 200px;
margin: 10px;
float: left;
background: white;
box-shadow: 5px 5px 10px 0px #aaa;
}
.dashboard-item img {
max-width: 300px;
max-height: 180px;
display:block; /* removes 3 pixels extra height around the image */
}
.dashboard-item fieldValue{
}
.center
{
display: flex;

View File

@@ -207,14 +207,40 @@ Vue.component('result-view', {
}
},
template: `
<div id="result">
<div id="prev_image" v-show="showPrevNext" v-on:click="prev_image" title="Previous Plot"><i class="fa fa-angle-double-left" aria-hidden="true"></i></div>
<div id="next_image" v-show="showPrevNext" v-on:click="next_image" title="Next Plot"><i class="fa fa-angle-double-right" aria-hidden="true"></i></div>
<div id="result-view" v-if="resultView.imageUrl || resultView.errorMessage">
<div id="prev_image" v-if="showPrevNext" v-on:click="prev_image" title="Previous Plot"><i class="fa fa-angle-double-left" aria-hidden="true"></i></div>
<div id="next_image" v-if="showPrevNext" v-on:click="next_image" title="Next Plot"><i class="fa fa-angle-double-right" aria-hidden="true"></i></div>
<div id="result-image"><img v-bind:src="resultView.imageUrl" v-if="resultView.imageUrl"/></div>
<div id="result-error-message" v-if="resultView.errorMessage">{{ resultView.errorMessage }}</div>
</div>`
});
Vue.component('result-view-dashboard', {
props: ['dashboard'],
methods: {
},
template: `
<div id="result-view-dashboard"
v-if="dashboard.tiles">
<result-view-dashboard-item
v-for="item in dashboard.tiles"
v-bind:key="item.fieldValue"
v-bind:dashboardItem="item"
></result-view-dashboard-item>
</div>
`
});
Vue.component('result-view-dashboard-item', {
props: ['dashboardItem'],
template: `
<div class="dashboard-item">
<div class="error-message" v-if="dashboardItem.error">{{ dashboardItem.error }}</div>
<img v-bind:src="dashboardItem.thumbnailUrl" v-if="dashboardItem.thumbnailUrl" />
<div class="fieldValue">{{ dashboardItem.fieldValue }}</div>
</div>`
});
Vue.component('navigation-bar', {
props: [],
methods: {
@@ -376,17 +402,22 @@ Vue.component('navigation-bar', {
return date;
}
},
computed: {
navigationDisabled: function() {
return !data.resultView.imageUrl;
}
},
template: `
<div id="navigation">
<button id="nav_left" v-on:click="dateLeftShift" title="Show Older Values"><i class="fa fa-angle-double-left" aria-hidden="true"></i></button>
<button id="nav_left_half" v-on:click="dateHalfLeftShift" title="Show Older Values"><i class="fa fa-angle-left" aria-hidden="true"></i></button>
<button id="nav_left" v-on:click="dateLeftShift" :disabled="navigationDisabled" title="Show Older Values"><i class="fa fa-angle-double-left" aria-hidden="true"></i></button>
<button id="nav_left_half" v-on:click="dateHalfLeftShift" :disabled="navigationDisabled" title="Show Older Values"><i class="fa fa-angle-left" aria-hidden="true"></i></button>
<div>
<button id="zoom_in" v-on:click="zoomIn" title="Zoom In"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
<button id="refresh" v-on:click="refresh" title="Refresh"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button id="zoom_out" v-on:click="zoomOut" title="Zoom Out"><i class="fa fa-minus-circle" aria-hidden="true"></i></button>
<button id="zoom_in" v-on:click="zoomIn" :disabled="navigationDisabled" title="Zoom In"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
<button id="refresh" v-on:click="refresh" :disabled="navigationDisabled" title="Refresh"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button id="zoom_out" v-on:click="zoomOut" :disabled="navigationDisabled" title="Zoom Out"><i class="fa fa-minus-circle" aria-hidden="true"></i></button>
</div>
<button id="nav_right_half" v-on:click="dateHalfRightShift" title="Show Newer Values"><i class="fa fa-angle-right" aria-hidden="true"></i></button>
<button id="nav_right" v-on:click="dateRightShift" title="Show Newer Values"><i class="fa fa-angle-double-right" aria-hidden="true"></i></button>
<button id="nav_right_half" v-on:click="dateHalfRightShift" :disabled="navigationDisabled" title="Show Newer Values"><i class="fa fa-angle-right" aria-hidden="true"></i></button>
<button id="nav_right" v-on:click="dateRightShift" :disabled="navigationDisabled" title="Show Newer Values"><i class="fa fa-angle-double-right" aria-hidden="true"></i></button>
</div>`
});
@@ -418,8 +449,8 @@ Vue.component('search-bar', {
plotCurrent();
}
},
dashboard: function (event) {
alert('dashboard');
createNewDashboard: function (event) {
createDashboard(this);
},
enableSplitBy: function(fieldValues) {
data.searchBar.splitBy['field'] = data.searchBar.splitByKeys.selected;
@@ -467,6 +498,9 @@ Vue.component('search-bar', {
var link = window.location.origin+ window.location.pathname + "?" + jQuery.param( params );
return link;
},
dashboardActive: function (){
return data.searchBar.splitByKeys.selected != "";
}
},
template: `
@@ -573,13 +607,12 @@ Vue.component('search-bar', {
title="Create Plot"
v-on:click.prevent.stop="plot"
><i class="fa fa-area-chart" aria-hidden="true"></i> Plot</button>
<!--
<button
id="dashboard-submit"
title="Create Dashboard"
v-on:click.prevent.stop="dashboard"
title="Create Dashboard (only active if 'Split' is set)"
:disabled="!dashboardActive"
v-on:click.prevent.stop="createNewDashboard"
><i class="fa fa-object-group" aria-hidden="true"></i> Dashboard</button>
-->
<a v-bind:href="permalink" v-show="permalink" title="Permanent link to the current settings." class="permalink"><i class="fa fa-sliders" aria-hidden="true"></i></a>
<a v-bind:href="searchBar.imagelink" v-show="searchBar.imagelink" title="Image Link" class="permalink"><i class="fa fa-image" aria-hidden="true"></i></a>
@@ -658,13 +691,15 @@ var data = {
errorMessage: '',
loadingGameActive: false
},
dashboard: {
tiles: []
}
};
function showLoadingIcon()
{
data.resultView.imageUrl = '';
data.resultView.errorMessage = '';
hidePlotAndDashboard();
data.resultView.loadingGameActive = true;
startInvaders();
@@ -676,26 +711,35 @@ function hideLoadingIcon()
pauseInvaders();
}
function hidePlotAndDashboard()
{
data.resultView.imageUrl = '';
data.resultView.errorMessage = '';
data.dashboard.tiles = [];
}
function plotCurrent()
{
showLoadingIcon();
if (data.searchBar.splitBy['field']) {
var query = createQuery();
var splitBy = data.searchBar.splitBy;
var originalQuery = splitBy['query'];
var splitByField = splitBy['field'];
var splitByValue = splitBy['values'][splitBy['index']];
var query = createQuery(originalQuery, splitByField, splitByValue);
sendPlotRequest(query);
}else{
sendPlotRequest(data.searchBar.query);
}
}
function createQuery()
function createQuery(query, splitByField, splitByValue)
{
var splitBy = data.searchBar.splitBy;
var query = splitBy['query'];
if (query.length > 0) {
query = "("+query+") and "+splitBy['field']+ " = " +splitBy['values'][splitBy['index']];
query = "("+query+") and "+splitByField+ " = " +splitByValue;
} else {
query = splitBy['field']+ " = " +splitBy['values'][splitBy['index']];
query = splitByField+ " = " +splitByValue;
}
return query;
}
@@ -712,12 +756,11 @@ function groupBy()
return result;
}
function sendPlotRequest(query){
function createRequest(query){
var request = {};
request['query'] = query;
request['height'] = Math.floor($('#result-image').height());
request['width'] = Math.floor($('#result-image').width());
request['height'] = Math.floor($('#result').height());
request['width'] = Math.floor($('#result').width());
request['groupBy'] = groupBy();
request['limitBy'] = data.searchBar.limitBy.selected;
request['limit'] = parseInt(data.searchBar.limitBy.number);
@@ -728,14 +771,19 @@ function sendPlotRequest(query){
request['aggregate'] = data.searchBar.aggregate;
request['keyOutside'] = data.searchBar.keyOutside;
return request;
}
var success = function(response){
function sendPlotRequest(query){
const request = createRequest(query);
const success = function(response){
data.resultView.imageUrl = response.imageUrl;
data.resultView.errorMessage = '';
hideLoadingIcon();
updateImageLink(query);
};
var error = function(e) {
const error = function(e) {
data.resultView.imageUrl = '';
if (e.status == 404){
data.resultView.errorMessage = "No data points found for query: " + query;
@@ -770,13 +818,74 @@ function updateImageLink(query) {
'plotType': data.searchBar.plotType,
'aggregate': data.searchBar.aggregate,
'keyOutside': data.searchBar.keyOutside,
'width': Math.floor($('#result-image').width()),
'height': Math.floor($('#result-image').height())
'width': Math.floor($('#result').width()),
'height': Math.floor($('#result').height())
};
data.searchBar.imagelink = window.location.origin+ window.location.pathname + "plots?" + jQuery.param(params);
}
function createDashboard(vm){
hidePlotAndDashboard();
const imageHeight = Math.floor($('#app').height() - $('#search-bar').height() - $('#navigation').height());
const imageWidth = Math.floor($('#app').width()- $('#search-bar').width() - $('#navigation').width());
const originalQuery = data.searchBar.query;
vm.splitQueries(function (fieldValues) {
var splitByField = data.searchBar.splitByKeys.selected;
createDashboardItem(fieldValues, originalQuery, splitByField, imageHeight, imageWidth);
});
}
function createDashboardItem(fieldValues, originalQuery, field, imageHeight, imageWidth)
{
if (fieldValues.length > 0) {
var fieldValue = fieldValues.pop();
const query = createQuery(originalQuery, field, fieldValue);
const request = createRequest(query);
request['height'] = imageHeight;
request['width'] = imageWidth;
request['thumbnailMaxWidth'] = 300;
request['thumbnailMaxHeight'] = 200;
const success = function(response){
data.dashboard.tiles.push({
fieldValue: fieldValue,
thumbnailUrl: response.thumbnailUrl,
imageUrl: response.imageUrl
});
createDashboardItem(fieldValues, originalQuery, field, imageHeight, imageWidth);
};
const error = function(e) {
var errorMessage = '';
if (e.status == 404){
// skip
}
else if (e.status == 503){
errorMessage = "Too many parallel requests.";
}
else{
errorMessage = "FAILED: " + JSON.parse(e.responseText).message;
}
hideLoadingIcon();
if (errorMessage) {
data.dashboard.tiles.push({
fieldValue: fieldValue,
error: errorMessage
});
}
createDashboardItem(fieldValues, originalQuery, field, imageHeight, imageWidth);
};
postJson("plots", request, success, error);
}
}
function postJson(url, requestData, successCallback, errorCallback) {
$.ajax({

View File

@@ -19,7 +19,10 @@
<div id="app">
<search-bar v-bind="{ 'searchBar': searchBar }"></search-bar>
<navigation-bar v-bind="{ 'searchBar': searchBar }"></navigation-bar>
<result-view v-bind="{ 'searchBar': searchBar, 'resultView': resultView }"></result-view>
<div id="result">
<result-view v-bind="{ 'searchBar': searchBar, 'resultView': resultView }"></result-view>
<result-view-dashboard v-bind="{ 'dashboard': dashboard }"></result-view-dashboard>
</div>
</div>
</body>
</html>