make it possible to draw the legend outside of the plot area

This commit is contained in:
2017-09-30 17:51:33 +02:00
parent d4fd25dc4c
commit 386f211377
9 changed files with 1460 additions and 1402 deletions

View File

@@ -1,171 +1,181 @@
package org.lucares.pdb.plot.api;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
public class PlotSettings {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private String query;
private int height;
private int width;
private List<String> groupBy;
private Limit limitBy;
private int limit;
private String dateFrom;
private String dateRange;
private AxisScale yAxisScale;
private AggreateInternal aggregate;
public String getQuery() {
return query;
}
public void setQuery(final String query) {
this.query = query;
}
public int getHeight() {
return height;
}
public void setHeight(final int height) {
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(final int width) {
this.width = width;
}
public List<String> getGroupBy() {
return groupBy;
}
public void setGroupBy(final List<String> groupBy) {
this.groupBy = groupBy;
}
public Limit getLimitBy() {
return limitBy;
}
public void setLimitBy(final Limit limitBy) {
this.limitBy = limitBy;
}
public int getLimit() {
return limit;
}
public void setLimit(final int limit) {
this.limit = limit;
}
public String getDateFrom() {
return dateFrom;
}
public void setDateFrom(final String dateFrom) {
this.dateFrom = dateFrom;
}
public String getDateRange() {
return dateRange;
}
public void setDateRange(final String dateRange) {
this.dateRange = dateRange;
}
public OffsetDateTime dateFrom() {
if (StringUtils.isEmpty(dateFrom)) {
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(Long.MIN_VALUE), ZoneOffset.UTC);
} else {
return LocalDateTime.parse(dateFrom, DATE_FORMAT).atOffset(ZoneOffset.UTC);
}
}
public OffsetDateTime dateTo() {
if (StringUtils.isEmpty(dateRange)) {
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(Long.MAX_VALUE), ZoneOffset.UTC);
} else {
final int period = Integer.parseInt(dateRange.split(" ")[0]);
final ChronoUnit unit = toChronoUnit(dateRange.split(" ")[1]);
return dateFrom().plus(period, unit);
}
}
private ChronoUnit toChronoUnit(final String string) {
switch (string) {
case "second":
case "seconds":
return ChronoUnit.SECONDS;
case "minute":
case "minutes":
return ChronoUnit.MINUTES;
case "hour":
case "hours":
return ChronoUnit.HOURS;
case "day":
case "days":
return ChronoUnit.DAYS;
case "week":
case "weeks":
return ChronoUnit.WEEKS;
case "month":
case "months":
return ChronoUnit.MONTHS;
default:
throw new IllegalArgumentException(string + " is an unknown chrono unit");
}
}
public void setYAxisScale(final AxisScale axisScale) {
this.yAxisScale = axisScale;
}
public AxisScale getYAxisScale() {
return yAxisScale;
}
@Override
public String toString() {
return "PlotSettings [query=" + query + ", height=" + height + ", width=" + width + ", groupBy=" + groupBy
+ ", limitBy=" + limitBy + ", limit=" + limit + ", dateFrom=" + dateFrom + ", dateRange=" + dateRange
+ ", axisScale=" + yAxisScale + ", aggregate="+aggregate+"]";
}
public void setAggregate(AggreateInternal aggregate) {
this.aggregate = aggregate;
}
public AggreateInternal getAggregate() {
return aggregate;
}
}
package org.lucares.pdb.plot.api;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
public class PlotSettings {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private String query;
private int height;
private int width;
private List<String> groupBy;
private Limit limitBy;
private int limit;
private String dateFrom;
private String dateRange;
private AxisScale yAxisScale;
private AggreateInternal aggregate;
private boolean keyOutside;
public String getQuery() {
return query;
}
public void setQuery(final String query) {
this.query = query;
}
public int getHeight() {
return height;
}
public void setHeight(final int height) {
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(final int width) {
this.width = width;
}
public List<String> getGroupBy() {
return groupBy;
}
public void setGroupBy(final List<String> groupBy) {
this.groupBy = groupBy;
}
public Limit getLimitBy() {
return limitBy;
}
public void setLimitBy(final Limit limitBy) {
this.limitBy = limitBy;
}
public int getLimit() {
return limit;
}
public void setLimit(final int limit) {
this.limit = limit;
}
public String getDateFrom() {
return dateFrom;
}
public void setDateFrom(final String dateFrom) {
this.dateFrom = dateFrom;
}
public String getDateRange() {
return dateRange;
}
public void setDateRange(final String dateRange) {
this.dateRange = dateRange;
}
public OffsetDateTime dateFrom() {
if (StringUtils.isEmpty(dateFrom)) {
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(Long.MIN_VALUE), ZoneOffset.UTC);
} else {
return LocalDateTime.parse(dateFrom, DATE_FORMAT).atOffset(ZoneOffset.UTC);
}
}
public OffsetDateTime dateTo() {
if (StringUtils.isEmpty(dateRange)) {
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(Long.MAX_VALUE), ZoneOffset.UTC);
} else {
final int period = Integer.parseInt(dateRange.split(" ")[0]);
final ChronoUnit unit = toChronoUnit(dateRange.split(" ")[1]);
return dateFrom().plus(period, unit);
}
}
private ChronoUnit toChronoUnit(final String string) {
switch (string) {
case "second":
case "seconds":
return ChronoUnit.SECONDS;
case "minute":
case "minutes":
return ChronoUnit.MINUTES;
case "hour":
case "hours":
return ChronoUnit.HOURS;
case "day":
case "days":
return ChronoUnit.DAYS;
case "week":
case "weeks":
return ChronoUnit.WEEKS;
case "month":
case "months":
return ChronoUnit.MONTHS;
default:
throw new IllegalArgumentException(string + " is an unknown chrono unit");
}
}
public void setYAxisScale(final AxisScale axisScale) {
this.yAxisScale = axisScale;
}
public AxisScale getYAxisScale() {
return yAxisScale;
}
@Override
public String toString() {
return "PlotSettings [query=" + query + ", height=" + height + ", width=" + width + ", groupBy=" + groupBy
+ ", limitBy=" + limitBy + ", limit=" + limit + ", dateFrom=" + dateFrom + ", dateRange=" + dateRange
+ ", axisScale=" + yAxisScale + ", aggregate="+aggregate+", keyOutside="+keyOutside+"]";
}
public void setAggregate(AggreateInternal aggregate) {
this.aggregate = aggregate;
}
public AggreateInternal getAggregate() {
return aggregate;
}
public void setKeyOutside(boolean keyOutside) {
this.keyOutside = keyOutside;
}
public boolean isKeyOutside() {
return keyOutside;
}
}

View File

@@ -1,87 +1,92 @@
package org.lucares.recommind.logs;
import java.util.Collection;
import org.lucares.pdb.plot.api.AggreateInternal;
import org.lucares.pdb.plot.api.AxisScale;
public class GnuplotFileGenerator {
public String generate(final GnuplotSettings settings, final Collection<DataSeries> dataSeries) {
final StringBuilder result = new StringBuilder();
appendfln(result, "set terminal %s noenhanced size %d,%d", settings.getTerminal(), settings.getWidth(),
settings.getHeight());
appendfln(result, "set datafile separator \"%s\"", settings.getDatafileSeparator());
int count = 1;
if (settings.getAggregate() != AggreateInternal.NONE)
{
for (final DataSeries dataSerie : dataSeries) {
appendfln(result, "stats '%s' using 2 prefix \"A%d\"", dataSerie.getDataFile(),count);
count++;
}
}
appendfln(result, "set timefmt '%s'", settings.getTimefmt());
appendfln(result, "set xdata time");
appendfln(result, "set format x \"%s\"", settings.getFormatX());
appendfln(result, "set xlabel \"%s\"", settings.getXlabel());
appendfln(result, "set xtics rotate by %d", settings.getRotateXAxisLabel());
appendfln(result, "set xrange [\"%s\":\"%s\"]", settings.getDateFrom(), settings.getDateTo());
final long graphOffset = settings.getYAxisScale() == AxisScale.LINEAR ? 0 : 1;
appendfln(result, "set yrange [\""+graphOffset+"\":]");
appendfln(result, "set ylabel \"%s\"", settings.getYlabel());
switch (settings.getYAxisScale()) {
case LINEAR:
break;
case LOG10:
appendfln(result, "set logscale y");
break;
case LOG2:
appendfln(result, "set logscale y 2");
break;
}
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.getDateFrom());
final long maxDate = Long.parseLong(settings.getDateTo());
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");
appendf(result, "plot ");
count = 1;
for (final DataSeries dataSerie : dataSeries) {
appendfln(result, "'%s' using 1:2 title '%s' with points, \\", dataSerie.getDataFile(),
dataSerie.getTitle());
if (settings.getAggregate() == AggreateInternal.MEAN) {
appendfln(result, "A%d_mean title '%s Mean', \\", count, dataSerie.getTitle(),
dataSerie.getTitle());
}
count++;
}
return result.toString();
}
private void appendfln(final StringBuilder builder, final String format, final Object... args) {
builder.append(String.format(format + "\n", args));
}
private void appendf(final StringBuilder builder, final String format, final Object... args) {
builder.append(String.format(format, args));
}
}
package org.lucares.recommind.logs;
import java.util.Collection;
import org.lucares.pdb.plot.api.AggreateInternal;
import org.lucares.pdb.plot.api.AxisScale;
public class GnuplotFileGenerator {
public String generate(final GnuplotSettings settings, final Collection<DataSeries> dataSeries) {
final StringBuilder result = new StringBuilder();
appendfln(result, "set terminal %s noenhanced size %d,%d", settings.getTerminal(), settings.getWidth(),
settings.getHeight());
appendfln(result, "set datafile separator \"%s\"", settings.getDatafileSeparator());
int count = 1;
if (settings.getAggregate() != AggreateInternal.NONE)
{
for (final DataSeries dataSerie : dataSeries) {
appendfln(result, "stats '%s' using 2 prefix \"A%d\"", dataSerie.getDataFile(),count);
count++;
}
}
appendfln(result, "set timefmt '%s'", settings.getTimefmt());
appendfln(result, "set xdata time");
appendfln(result, "set format x \"%s\"", settings.getFormatX());
appendfln(result, "set xlabel \"%s\"", settings.getXlabel());
appendfln(result, "set xtics rotate by %d", settings.getRotateXAxisLabel());
appendfln(result, "set xrange [\"%s\":\"%s\"]", settings.getDateFrom(), settings.getDateTo());
final long graphOffset = settings.getYAxisScale() == AxisScale.LINEAR ? 0 : 1;
appendfln(result, "set yrange [\""+graphOffset+"\":]");
appendfln(result, "set ylabel \"%s\"", settings.getYlabel());
switch (settings.getYAxisScale()) {
case LINEAR:
break;
case LOG10:
appendfln(result, "set logscale y");
break;
case LOG2:
appendfln(result, "set logscale y 2");
break;
}
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.getDateFrom());
final long maxDate = Long.parseLong(settings.getDateTo());
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\"");
appendf(result, "plot ");
count = 1;
for (final DataSeries dataSerie : dataSeries) {
appendfln(result, "'%s' using 1:2 title '%s' with points, \\", dataSerie.getDataFile(),
dataSerie.getTitle());
if (settings.getAggregate() == AggreateInternal.MEAN) {
appendfln(result, "A%d_mean title '%s Mean', \\", count, dataSerie.getTitle(),
dataSerie.getTitle());
}
count++;
}
return result.toString();
}
private void appendfln(final StringBuilder builder, final String format, final Object... args) {
builder.append(String.format(format + "\n", args));
}
private void appendf(final StringBuilder builder, final String format, final Object... args) {
builder.append(String.format(format, args));
}
}

View File

@@ -1,150 +1,159 @@
package org.lucares.recommind.logs;
import java.nio.file.Path;
import org.lucares.pdb.plot.api.AggreateInternal;
import org.lucares.pdb.plot.api.AxisScale;
public class GnuplotSettings {
private String terminal = "png";
private int height = 1200;
private int width = 1600;
private String timefmt = "%s"; //"%Y-%m-%dT%H:%M:%S"; // TODO @ahr timefmt
// set format for x-axis
private String formatX = "%Y-%m-%d %H:%M:%S";
// set datafile separator
private String datafileSeparator = ",";
// set xlabel
private String xlabel = "Time";
// set ylabel
private String ylabel = "Duration in ms";
// set output "datausage.png"
private final Path output;
// set xtics rotate by 10 degree
private int rotateXAxisLabel = -10;
private AxisScale yAxisScale;
private String dateFrom;
private String dateTo;
private AggreateInternal aggregate;
public GnuplotSettings(final Path output) {
this.output = output;
}
public int getRotateXAxisLabel() {
return rotateXAxisLabel;
}
public void setRotateXAxisLabel(final int rotateXAxisLabel) {
this.rotateXAxisLabel = rotateXAxisLabel;
}
public String getTerminal() {
return terminal;
}
public void setTerminal(final String terminal) {
this.terminal = terminal;
}
public int getHeight() {
return height;
}
public void setHeight(final int height) {
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(final int width) {
this.width = width;
}
public String getTimefmt() {
return timefmt;
}
public void setTimefmt(final String timefmt) {
this.timefmt = timefmt;
}
public String getFormatX() {
return formatX;
}
public void setFormatX(final String formatX) {
this.formatX = formatX;
}
public String getDatafileSeparator() {
return datafileSeparator;
}
public void setDatafileSeparator(final String datafileSeparator) {
this.datafileSeparator = datafileSeparator;
}
public String getXlabel() {
return xlabel;
}
public void setXlabel(final String xlabel) {
this.xlabel = xlabel;
}
public String getYlabel() {
return ylabel;
}
public void setYlabel(final String ylabel) {
this.ylabel = ylabel;
}
public Path getOutput() {
return output;
}
public void setYAxisScale(final AxisScale yAxisScale) {
this.yAxisScale = yAxisScale;
}
public AxisScale getYAxisScale() {
return yAxisScale;
}
public void setDateFrom(final String dateFrom) {
this.dateFrom = dateFrom;
}
public String getDateFrom() {
return dateFrom;
}
public void setDateTo(final String dateTo) {
this.dateTo = dateTo;
}
public String getDateTo() {
return dateTo;
}
public void setAggregate(AggreateInternal aggregate) {
this.aggregate = aggregate;
}
public AggreateInternal getAggregate() {
return aggregate;
}
// plot 'sample.txt' using 1:2 title 'Bytes' with linespoints 2
}
package org.lucares.recommind.logs;
import java.nio.file.Path;
import org.lucares.pdb.plot.api.AggreateInternal;
import org.lucares.pdb.plot.api.AxisScale;
public class GnuplotSettings {
private String terminal = "png";
private int height = 1200;
private int width = 1600;
private String timefmt = "%s"; //"%Y-%m-%dT%H:%M:%S"; // TODO @ahr timefmt
// set format for x-axis
private String formatX = "%Y-%m-%d %H:%M:%S";
// set datafile separator
private String datafileSeparator = ",";
// set xlabel
private String xlabel = "Time";
// set ylabel
private String ylabel = "Duration in ms";
// set output "datausage.png"
private final Path output;
// set xtics rotate by 10 degree
private int rotateXAxisLabel = -10;
private AxisScale yAxisScale;
private String dateFrom;
private String dateTo;
private AggreateInternal aggregate;
private boolean keyOutside = false;
public GnuplotSettings(final Path output) {
this.output = output;
}
public int getRotateXAxisLabel() {
return rotateXAxisLabel;
}
public void setRotateXAxisLabel(final int rotateXAxisLabel) {
this.rotateXAxisLabel = rotateXAxisLabel;
}
public String getTerminal() {
return terminal;
}
public void setTerminal(final String terminal) {
this.terminal = terminal;
}
public int getHeight() {
return height;
}
public void setHeight(final int height) {
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(final int width) {
this.width = width;
}
public String getTimefmt() {
return timefmt;
}
public void setTimefmt(final String timefmt) {
this.timefmt = timefmt;
}
public String getFormatX() {
return formatX;
}
public void setFormatX(final String formatX) {
this.formatX = formatX;
}
public String getDatafileSeparator() {
return datafileSeparator;
}
public void setDatafileSeparator(final String datafileSeparator) {
this.datafileSeparator = datafileSeparator;
}
public String getXlabel() {
return xlabel;
}
public void setXlabel(final String xlabel) {
this.xlabel = xlabel;
}
public String getYlabel() {
return ylabel;
}
public void setYlabel(final String ylabel) {
this.ylabel = ylabel;
}
public Path getOutput() {
return output;
}
public void setYAxisScale(final AxisScale yAxisScale) {
this.yAxisScale = yAxisScale;
}
public AxisScale getYAxisScale() {
return yAxisScale;
}
public void setDateFrom(final String dateFrom) {
this.dateFrom = dateFrom;
}
public String getDateFrom() {
return dateFrom;
}
public void setDateTo(final String dateTo) {
this.dateTo = dateTo;
}
public String getDateTo() {
return dateTo;
}
public void setAggregate(AggreateInternal aggregate) {
this.aggregate = aggregate;
}
public AggreateInternal getAggregate() {
return aggregate;
}
public void setKeyOutside(boolean keyOutside) {
this.keyOutside = keyOutside;
}
public boolean isKeyOutside() {
return keyOutside;
}
// plot 'sample.txt' using 1:2 title 'Bytes' with linespoints 2
}

View File

@@ -1,284 +1,285 @@
package org.lucares.recommind.logs;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Formatter;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.lucares.pdb.api.Entry;
import org.lucares.pdb.api.GroupResult;
import org.lucares.pdb.api.Result;
import org.lucares.pdb.api.Tags;
import org.lucares.pdb.plot.api.Limit;
import org.lucares.pdb.plot.api.PlotSettings;
import org.lucares.performance.db.PerformanceDb;
import org.lucares.utils.file.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Plotter {
private static final Logger LOGGER = LoggerFactory.getLogger(Plotter.class);
private static final Logger METRICS_LOGGER = LoggerFactory.getLogger("org.lucares.metrics.plotter");
private static final String DEFAULT_GROUP = "<none>";
private static final int INT_TO_STRING_CACHE_SIZE= 1000;
private static final String[] INT_TO_STRING;
static {
INT_TO_STRING = new String[INT_TO_STRING_CACHE_SIZE];
for (int i = 0; i < INT_TO_STRING_CACHE_SIZE; i++){
INT_TO_STRING[i] = String.valueOf(i);
}
}
private final PerformanceDb db;
private final Path tmpBaseDir;
private final Path outputDir;
public Plotter(final PerformanceDb db, final Path tmpBaseDir, final Path outputDir) {
this.db = db;
this.tmpBaseDir = tmpBaseDir;
this.outputDir = outputDir;
if (!Files.isDirectory(tmpBaseDir, LinkOption.NOFOLLOW_LINKS)) {
throw new IllegalArgumentException(tmpBaseDir + " is not a directory");
}
if (!Files.isDirectory(outputDir)) {
throw new IllegalArgumentException(outputDir + " is not a directory");
}
}
public Path getOutputDir() {
return outputDir;
}
public PlotResult plot(final PlotSettings plotSettings) throws InternalPlottingException {
LOGGER.trace("start plot: {}", plotSettings);
final String tmpSubDir = uniqueDirectoryName();
final Path tmpDir = tmpBaseDir.resolve(tmpSubDir);
try {
Files.createDirectories(tmpDir);
final List<DataSeries> dataSeries = new ArrayList<>();
final String query = plotSettings.getQuery();
final List<String> groupBy = plotSettings.getGroupBy();
final int height = plotSettings.getHeight();
final int width = plotSettings.getWidth();
final OffsetDateTime dateFrom = plotSettings.dateFrom();
final OffsetDateTime dateTo = plotSettings.dateTo();
final Result result = db.get(query, groupBy);
for (final GroupResult groupResult : result.getGroups()) {
final Stream<Entry> entries = groupResult.asStream();
final File dataFile = File.createTempFile("data", ".dat", tmpDir.toFile());
final CsvSummary csvSummary = toCsv(entries, dataFile, dateFrom, dateTo);
final String title = title(groupResult.getGroupedBy(), csvSummary.getValues());
final DataSeries dataSerie = new DataSeries(dataFile, title, csvSummary.getValues(), csvSummary.getMaxValue());
if (dataSerie.getValues() > 0) {
dataSeries.add(dataSerie);
}
}
if (dataSeries.isEmpty()) {
throw new NoDataPointsException();
}
sortAndLimit(dataSeries, plotSettings);
final Path outputFile = Files.createTempFile(outputDir, "out", ".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());
gnuplotSettings.setYAxisScale(plotSettings.getYAxisScale());
gnuplotSettings.setAggregate(plotSettings.getAggregate());
gnuplot.plot(gnuplotSettings, dataSeries);
return new PlotResult(outputFile.getFileName(), dataSeries);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Plotting was interrupted.");
} catch (final IOException e) {
throw new InternalPlottingException("Plotting failed: " + e.getMessage(), e);
} finally {
FileUtils.delete(tmpDir);
LOGGER.trace("done plot");
}
}
private void defineXAxis(final GnuplotSettings gnuplotSettings, final OffsetDateTime minDate,
final OffsetDateTime maxDate) {
String formatX;
int rotateX;
String formattedMinDate;
String formattedMaxDate;
if (minDate.until(maxDate, ChronoUnit.WEEKS) > 1) {
formatX = "%Y-%m-%d";
rotateX = 0;
} else if (minDate.until(maxDate, ChronoUnit.SECONDS) > 30) {
formatX = "%Y-%m-%d %H:%M:%S";
rotateX = gnuplotSettings.getRotateXAxisLabel();
} else {
formatX = "%Y-%m-%d %H:%M:%.3S";
rotateX = gnuplotSettings.getRotateXAxisLabel();
}
formattedMinDate = String.valueOf(minDate.toEpochSecond());
formattedMaxDate = String.valueOf(maxDate.toEpochSecond());
gnuplotSettings.setFormatX(formatX);
gnuplotSettings.setRotateXAxisLabel(rotateX);
gnuplotSettings.setDateFrom(formattedMinDate);
gnuplotSettings.setDateTo(formattedMaxDate);
}
private String uniqueDirectoryName() {
return OffsetDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm_ss")) + "_"
+ UUID.randomUUID().toString();
}
private void sortAndLimit(final List<DataSeries> dataSeries, final PlotSettings plotSettings) {
final Limit limitBy = plotSettings.getLimitBy();
dataSeries.sort(getDataSeriesComparator(limitBy));
switch (limitBy) {
case FEWEST_VALUES:
case MOST_VALUES:
case MAX_VALUE:
case MIN_VALUE:
while (dataSeries.size() > plotSettings.getLimit()) {
dataSeries.remove(plotSettings.getLimit());
}
break;
case NO_LIMIT:
}
}
private Comparator<? super DataSeries> getDataSeriesComparator(final Limit limitBy) {
switch (limitBy) {
case MOST_VALUES:
return DataSeries.BY_NUMBER_OF_VALUES.reversed();
case FEWEST_VALUES:
return DataSeries.BY_NUMBER_OF_VALUES;
case MAX_VALUE:
return DataSeries.BY_MAX_VALUE.reversed();
case MIN_VALUE:
return DataSeries.BY_MAX_VALUE;
case NO_LIMIT:
return DataSeries.BY_NUMBER_OF_VALUES;
}
throw new IllegalStateException("unhandled enum: "+ limitBy);
}
private String title(final Tags tags, final int values) {
final StringBuilder result = new StringBuilder();
if (tags.isEmpty()) {
result.append(DEFAULT_GROUP);
} else {
tags.forEach((k, v) -> {
if (result.length() > 0) {
result.append(" / ");
}
result.append(v);
});
}
result.append(" (");
result.append(values);
result.append(")");
return result.toString();
}
private static CsvSummary toCsv(final Stream<Entry> entries, final File dataFile, final OffsetDateTime dateFrom,
final OffsetDateTime dateTo) throws IOException {
final long start = System.nanoTime();
int count = 0;
final long fromEpochMilli = dateFrom.toInstant().toEpochMilli();
final long toEpochMilli = dateTo.toInstant().toEpochMilli();
final boolean useMillis = (toEpochMilli - fromEpochMilli) < TimeUnit.MINUTES.toMillis(5);
long maxValue = 0;
long ignoredValues = 0;
final int separator = ',';
final int newline = '\n';
final StringBuilder formattedDateBuilder = new StringBuilder();
try (final Writer output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(dataFile), StandardCharsets.US_ASCII));
final Formatter formatter = new Formatter(formattedDateBuilder);) {
final Iterator<Entry> it = entries.iterator();
while (it.hasNext()) {
final Entry entry = it.next();
if (fromEpochMilli <= entry.getEpochMilli() && entry.getEpochMilli() <= toEpochMilli) {
final String value = longToString(entry.getValue());
final String formattedDate;
if (useMillis){
formattedDateBuilder.delete(0, formattedDateBuilder.length());
formatter.format("%.3f", entry.getEpochMilli() / 1000.0);
formattedDate = formattedDateBuilder.toString();
}else {
formattedDate = String.valueOf(entry.getEpochMilli() / 1000);
}
output.write(formattedDate);
output.write(separator);
output.write(value);
output.write(newline);
count++;
maxValue = Math.max(maxValue, entry.getValue());
}else {
ignoredValues++;
}
}
}
METRICS_LOGGER.debug("wrote {} values to csv in: {}ms (ignored {} values) use millis: {}", count, (System.nanoTime() - start) / 1_000_000.0, ignoredValues, Boolean.toString(useMillis));
return new CsvSummary(count, maxValue);
}
private static String longToString(final long value){
// using pre-generated strings reduces memory allocation by up to 25%
if (value < INT_TO_STRING_CACHE_SIZE){
return INT_TO_STRING[(int) value];
}
return String.valueOf(value);
}
}
package org.lucares.recommind.logs;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Formatter;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.lucares.pdb.api.Entry;
import org.lucares.pdb.api.GroupResult;
import org.lucares.pdb.api.Result;
import org.lucares.pdb.api.Tags;
import org.lucares.pdb.plot.api.Limit;
import org.lucares.pdb.plot.api.PlotSettings;
import org.lucares.performance.db.PerformanceDb;
import org.lucares.utils.file.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Plotter {
private static final Logger LOGGER = LoggerFactory.getLogger(Plotter.class);
private static final Logger METRICS_LOGGER = LoggerFactory.getLogger("org.lucares.metrics.plotter");
private static final String DEFAULT_GROUP = "<none>";
private static final int INT_TO_STRING_CACHE_SIZE= 1000;
private static final String[] INT_TO_STRING;
static {
INT_TO_STRING = new String[INT_TO_STRING_CACHE_SIZE];
for (int i = 0; i < INT_TO_STRING_CACHE_SIZE; i++){
INT_TO_STRING[i] = String.valueOf(i);
}
}
private final PerformanceDb db;
private final Path tmpBaseDir;
private final Path outputDir;
public Plotter(final PerformanceDb db, final Path tmpBaseDir, final Path outputDir) {
this.db = db;
this.tmpBaseDir = tmpBaseDir;
this.outputDir = outputDir;
if (!Files.isDirectory(tmpBaseDir, LinkOption.NOFOLLOW_LINKS)) {
throw new IllegalArgumentException(tmpBaseDir + " is not a directory");
}
if (!Files.isDirectory(outputDir)) {
throw new IllegalArgumentException(outputDir + " is not a directory");
}
}
public Path getOutputDir() {
return outputDir;
}
public PlotResult plot(final PlotSettings plotSettings) throws InternalPlottingException {
LOGGER.trace("start plot: {}", plotSettings);
final String tmpSubDir = uniqueDirectoryName();
final Path tmpDir = tmpBaseDir.resolve(tmpSubDir);
try {
Files.createDirectories(tmpDir);
final List<DataSeries> dataSeries = new ArrayList<>();
final String query = plotSettings.getQuery();
final List<String> groupBy = plotSettings.getGroupBy();
final int height = plotSettings.getHeight();
final int width = plotSettings.getWidth();
final OffsetDateTime dateFrom = plotSettings.dateFrom();
final OffsetDateTime dateTo = plotSettings.dateTo();
final Result result = db.get(query, groupBy);
for (final GroupResult groupResult : result.getGroups()) {
final Stream<Entry> entries = groupResult.asStream();
final File dataFile = File.createTempFile("data", ".dat", tmpDir.toFile());
final CsvSummary csvSummary = toCsv(entries, dataFile, dateFrom, dateTo);
final String title = title(groupResult.getGroupedBy(), csvSummary.getValues());
final DataSeries dataSerie = new DataSeries(dataFile, title, csvSummary.getValues(), csvSummary.getMaxValue());
if (dataSerie.getValues() > 0) {
dataSeries.add(dataSerie);
}
}
if (dataSeries.isEmpty()) {
throw new NoDataPointsException();
}
sortAndLimit(dataSeries, plotSettings);
final Path outputFile = Files.createTempFile(outputDir, "out", ".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());
gnuplotSettings.setYAxisScale(plotSettings.getYAxisScale());
gnuplotSettings.setAggregate(plotSettings.getAggregate());
gnuplotSettings.setKeyOutside(plotSettings.isKeyOutside());
gnuplot.plot(gnuplotSettings, dataSeries);
return new PlotResult(outputFile.getFileName(), dataSeries);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Plotting was interrupted.");
} catch (final IOException e) {
throw new InternalPlottingException("Plotting failed: " + e.getMessage(), e);
} finally {
FileUtils.delete(tmpDir);
LOGGER.trace("done plot");
}
}
private void defineXAxis(final GnuplotSettings gnuplotSettings, final OffsetDateTime minDate,
final OffsetDateTime maxDate) {
String formatX;
int rotateX;
String formattedMinDate;
String formattedMaxDate;
if (minDate.until(maxDate, ChronoUnit.WEEKS) > 1) {
formatX = "%Y-%m-%d";
rotateX = 0;
} else if (minDate.until(maxDate, ChronoUnit.SECONDS) > 30) {
formatX = "%Y-%m-%d %H:%M:%S";
rotateX = gnuplotSettings.getRotateXAxisLabel();
} else {
formatX = "%Y-%m-%d %H:%M:%.3S";
rotateX = gnuplotSettings.getRotateXAxisLabel();
}
formattedMinDate = String.valueOf(minDate.toEpochSecond());
formattedMaxDate = String.valueOf(maxDate.toEpochSecond());
gnuplotSettings.setFormatX(formatX);
gnuplotSettings.setRotateXAxisLabel(rotateX);
gnuplotSettings.setDateFrom(formattedMinDate);
gnuplotSettings.setDateTo(formattedMaxDate);
}
private String uniqueDirectoryName() {
return OffsetDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm_ss")) + "_"
+ UUID.randomUUID().toString();
}
private void sortAndLimit(final List<DataSeries> dataSeries, final PlotSettings plotSettings) {
final Limit limitBy = plotSettings.getLimitBy();
dataSeries.sort(getDataSeriesComparator(limitBy));
switch (limitBy) {
case FEWEST_VALUES:
case MOST_VALUES:
case MAX_VALUE:
case MIN_VALUE:
while (dataSeries.size() > plotSettings.getLimit()) {
dataSeries.remove(plotSettings.getLimit());
}
break;
case NO_LIMIT:
}
}
private Comparator<? super DataSeries> getDataSeriesComparator(final Limit limitBy) {
switch (limitBy) {
case MOST_VALUES:
return DataSeries.BY_NUMBER_OF_VALUES.reversed();
case FEWEST_VALUES:
return DataSeries.BY_NUMBER_OF_VALUES;
case MAX_VALUE:
return DataSeries.BY_MAX_VALUE.reversed();
case MIN_VALUE:
return DataSeries.BY_MAX_VALUE;
case NO_LIMIT:
return DataSeries.BY_NUMBER_OF_VALUES;
}
throw new IllegalStateException("unhandled enum: "+ limitBy);
}
private String title(final Tags tags, final int values) {
final StringBuilder result = new StringBuilder();
if (tags.isEmpty()) {
result.append(DEFAULT_GROUP);
} else {
tags.forEach((k, v) -> {
if (result.length() > 0) {
result.append(" / ");
}
result.append(v);
});
}
result.append(" (");
result.append(values);
result.append(")");
return result.toString();
}
private static CsvSummary toCsv(final Stream<Entry> entries, final File dataFile, final OffsetDateTime dateFrom,
final OffsetDateTime dateTo) throws IOException {
final long start = System.nanoTime();
int count = 0;
final long fromEpochMilli = dateFrom.toInstant().toEpochMilli();
final long toEpochMilli = dateTo.toInstant().toEpochMilli();
final boolean useMillis = (toEpochMilli - fromEpochMilli) < TimeUnit.MINUTES.toMillis(5);
long maxValue = 0;
long ignoredValues = 0;
final int separator = ',';
final int newline = '\n';
final StringBuilder formattedDateBuilder = new StringBuilder();
try (final Writer output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(dataFile), StandardCharsets.US_ASCII));
final Formatter formatter = new Formatter(formattedDateBuilder);) {
final Iterator<Entry> it = entries.iterator();
while (it.hasNext()) {
final Entry entry = it.next();
if (fromEpochMilli <= entry.getEpochMilli() && entry.getEpochMilli() <= toEpochMilli) {
final String value = longToString(entry.getValue());
final String formattedDate;
if (useMillis){
formattedDateBuilder.delete(0, formattedDateBuilder.length());
formatter.format("%.3f", entry.getEpochMilli() / 1000.0);
formattedDate = formattedDateBuilder.toString();
}else {
formattedDate = String.valueOf(entry.getEpochMilli() / 1000);
}
output.write(formattedDate);
output.write(separator);
output.write(value);
output.write(newline);
count++;
maxValue = Math.max(maxValue, entry.getValue());
}else {
ignoredValues++;
}
}
}
METRICS_LOGGER.debug("wrote {} values to csv in: {}ms (ignored {} values) use millis: {}", count, (System.nanoTime() - start) / 1_000_000.0, ignoredValues, Boolean.toString(useMillis));
return new CsvSummary(count, maxValue);
}
private static String longToString(final long value){
// using pre-generated strings reduces memory allocation by up to 25%
if (value < INT_TO_STRING_CACHE_SIZE){
return INT_TO_STRING[(int) value];
}
return String.valueOf(value);
}
}

View File

@@ -1,68 +1,69 @@
package org.lucares.pdbui;
import org.lucares.pdb.plot.api.AggreateInternal;
import org.lucares.pdb.plot.api.AxisScale;
import org.lucares.pdb.plot.api.Limit;
import org.lucares.pdb.plot.api.PlotSettings;
import org.lucares.pdbui.domain.Aggregate;
import org.lucares.pdbui.domain.LimitBy;
import org.lucares.pdbui.domain.PlotRequest;
import org.lucares.pdbui.domain.YAxis;
class PlotSettingsTransformer {
static PlotSettings toSettings(final PlotRequest request) {
final PlotSettings result = new PlotSettings();
result.setQuery(request.getQuery());
result.setGroupBy(request.getGroupBy());
result.setHeight(request.getHeight());
result.setWidth(request.getWidth());
result.setLimit(request.getLimit());
result.setLimitBy(toLimit(request.getLimitBy()));
result.setDateFrom(request.getDateFrom());
result.setDateRange(request.getDateRange());
result.setYAxisScale(toAxisScale(request.getAxisScale()));
result.setAggregate(toAggregateInternal(request.getAggregate()));
return result;
}
private static AggreateInternal toAggregateInternal(Aggregate aggregate) {
switch (aggregate) {
case NONE:return AggreateInternal.NONE;
case MEAN:return AggreateInternal.MEAN;
}
throw new IllegalStateException("unhandled enum: " + aggregate);
}
private static AxisScale toAxisScale(final YAxis yAxis) {
switch (yAxis) {
case LINEAR:
return AxisScale.LINEAR;
case LOG10:
return AxisScale.LOG10;
case LOG2:
return AxisScale.LOG2;
default:
throw new IllegalStateException("unhandled enum: " + yAxis);
}
}
private static Limit toLimit(final LimitBy limitBy) {
switch (limitBy) {
case NO_LIMIT:
return Limit.NO_LIMIT;
case FEWEST_VALUES:
return Limit.FEWEST_VALUES;
case MOST_VALUES:
return Limit.MOST_VALUES;
case MAX_VALUE:
return Limit.MAX_VALUE;
case MIN_VALUE:
return Limit.MIN_VALUE;
default:
throw new IllegalStateException("unhandled enum: " + limitBy);
}
}
}
package org.lucares.pdbui;
import org.lucares.pdb.plot.api.AggreateInternal;
import org.lucares.pdb.plot.api.AxisScale;
import org.lucares.pdb.plot.api.Limit;
import org.lucares.pdb.plot.api.PlotSettings;
import org.lucares.pdbui.domain.Aggregate;
import org.lucares.pdbui.domain.LimitBy;
import org.lucares.pdbui.domain.PlotRequest;
import org.lucares.pdbui.domain.YAxis;
class PlotSettingsTransformer {
static PlotSettings toSettings(final PlotRequest request) {
final PlotSettings result = new PlotSettings();
result.setQuery(request.getQuery());
result.setGroupBy(request.getGroupBy());
result.setHeight(request.getHeight());
result.setWidth(request.getWidth());
result.setLimit(request.getLimit());
result.setLimitBy(toLimit(request.getLimitBy()));
result.setDateFrom(request.getDateFrom());
result.setDateRange(request.getDateRange());
result.setYAxisScale(toAxisScale(request.getAxisScale()));
result.setAggregate(toAggregateInternal(request.getAggregate()));
result.setKeyOutside(request.isKeyOutside());
return result;
}
private static AggreateInternal toAggregateInternal(Aggregate aggregate) {
switch (aggregate) {
case NONE:return AggreateInternal.NONE;
case MEAN:return AggreateInternal.MEAN;
}
throw new IllegalStateException("unhandled enum: " + aggregate);
}
private static AxisScale toAxisScale(final YAxis yAxis) {
switch (yAxis) {
case LINEAR:
return AxisScale.LINEAR;
case LOG10:
return AxisScale.LOG10;
case LOG2:
return AxisScale.LOG2;
default:
throw new IllegalStateException("unhandled enum: " + yAxis);
}
}
private static Limit toLimit(final LimitBy limitBy) {
switch (limitBy) {
case NO_LIMIT:
return Limit.NO_LIMIT;
case FEWEST_VALUES:
return Limit.FEWEST_VALUES;
case MOST_VALUES:
return Limit.MOST_VALUES;
case MAX_VALUE:
return Limit.MAX_VALUE;
case MIN_VALUE:
return Limit.MIN_VALUE;
default:
throw new IllegalStateException("unhandled enum: " + limitBy);
}
}
}

View File

@@ -1,113 +1,123 @@
package org.lucares.pdbui.domain;
import java.util.List;
public class PlotRequest {
private String query;
private int height = 1000;
private int width = 1000;
private List<String> groupBy;
private LimitBy limitBy = LimitBy.NO_LIMIT;
private YAxis yAxis = YAxis.LINEAR;
private int limit = Integer.MAX_VALUE;
private String dateFrom;
private String dateRange;
private Aggregate aggregate = Aggregate.NONE;
public String getQuery() {
return query;
}
public void setQuery(final String query) {
this.query = query;
}
public int getWidth() {
return width;
}
public void setWidth(final int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(final int height) {
this.height = height;
}
@Override
public String toString() {
return query + ":" + height + "x" + width;
}
public List<String> getGroupBy() {
return groupBy;
}
public void setGroupBy(final List<String> groupBy) {
this.groupBy = groupBy;
}
public LimitBy getLimitBy() {
return limitBy;
}
public void setLimitBy(final LimitBy limitBy) {
this.limitBy = limitBy;
}
public int getLimit() {
return limit;
}
public void setLimit(final int limit) {
this.limit = limit;
}
public String getDateFrom() {
return dateFrom;
}
public void setDateFrom(final String dateFrom) {
this.dateFrom = dateFrom;
}
public String getDateRange() {
return dateRange;
}
public void setDateRange(final String dateRange) {
if (!dateRange.matches("\\d+ (second|minute|hour|day|week|month)s?")) {
throw new IllegalArgumentException(dateRange + " is not a valid range");
}
this.dateRange = dateRange;
}
public YAxis getAxisScale() {
return yAxis;
}
public void setAxisScale(final YAxis yAxis) {
this.yAxis = yAxis;
}
public void setAggregate(Aggregate aggregate) {
this.aggregate = aggregate;
}
public Aggregate getAggregate() {
return aggregate;
}
}
package org.lucares.pdbui.domain;
import java.util.List;
public class PlotRequest {
private String query;
private int height = 1000;
private int width = 1000;
private List<String> groupBy;
private LimitBy limitBy = LimitBy.NO_LIMIT;
private YAxis yAxis = YAxis.LINEAR;
private int limit = Integer.MAX_VALUE;
private String dateFrom;
private String dateRange;
private Aggregate aggregate = Aggregate.NONE;
private boolean keyOutside;
public String getQuery() {
return query;
}
public void setQuery(final String query) {
this.query = query;
}
public int getWidth() {
return width;
}
public void setWidth(final int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(final int height) {
this.height = height;
}
@Override
public String toString() {
return query + ":" + height + "x" + width;
}
public List<String> getGroupBy() {
return groupBy;
}
public void setGroupBy(final List<String> groupBy) {
this.groupBy = groupBy;
}
public LimitBy getLimitBy() {
return limitBy;
}
public void setLimitBy(final LimitBy limitBy) {
this.limitBy = limitBy;
}
public int getLimit() {
return limit;
}
public void setLimit(final int limit) {
this.limit = limit;
}
public String getDateFrom() {
return dateFrom;
}
public void setDateFrom(final String dateFrom) {
this.dateFrom = dateFrom;
}
public String getDateRange() {
return dateRange;
}
public void setDateRange(final String dateRange) {
if (!dateRange.matches("\\d+ (second|minute|hour|day|week|month)s?")) {
throw new IllegalArgumentException(dateRange + " is not a valid range");
}
this.dateRange = dateRange;
}
public YAxis getAxisScale() {
return yAxis;
}
public void setAxisScale(final YAxis yAxis) {
this.yAxis = yAxis;
}
public void setAggregate(Aggregate aggregate) {
this.aggregate = aggregate;
}
public Aggregate getAggregate() {
return aggregate;
}
public void setKeyOutside(boolean keyOutside) {
this.keyOutside = keyOutside;
}
public boolean isKeyOutside() {
return keyOutside;
}
}

View File

@@ -1,117 +1,125 @@
html {
height: 100%;
margin:0;
padding:0;
font-size: 14px;
}
body{
display: grid;
height: 100vh;
margin: 0;
grid:
"search_field logo" auto
"search logo" auto
"navigation navigation" auto
"result result" 1fr
/ 1fr auto;
}
@font-face {
font-family: 'FontAwesome';
src: url('../fonts/fontawesome-webfont.eot?v=4.7.0');
src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
font-weight: normal;
font-style: normal;
}
#logo {
grid-area: logo;
font-size: 1.2em;
font-weight: bold;
background-color: black;
color: white;
padding: 3px;
}
#search-input-wrapper {
grid-area: search_field;
}
#search-bar {
grid-area: search;
background-color: #aaa;
padding-bottom:3px;
}
#navigation {
grid-area: navigation;
background-color: #aaa;
display: flex;
justify-content: space-between;
}
.autocomplete .active {
background-color: #AAA;
}
.autocomplete, #search-input-wrapper .autocomplete.open {
overflow-y: scroll;
}
#search-input {
box-sizing: border-box;
border: 0;
}
#search-limit-value {
width: 4em;
}
.input_date {
max-width: 10em;
}
#add-filter {
float:right;
}
#button-bar {
text-align: right;
}
#search-submit {
margin-right:3px;
}
#result-view {
grid-area: result;
background: #eee;
margin: 0;
padding: 0;
overflow: hidden;
}
#result-view i {
background: white;
display:inline;
font-style:normal;
line-height: 1.2em;
}
.center
{
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
input:required:invalid {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAT1JREFUeNpi/P//PwMpgImBRMACY/x7/uDX39sXt/67cMoDyOVgMjBjYFbV/8kkqcCBrIER5KS/967s+rmkXxzI5wJiRSBm/v8P7NTfHHFFl5mVdIzhGv4+u///x+xmuAlcdXPB9KeqeLgYd3bDU2ZpRRmwH4DOeAI07QXIRKipYPD35184/nn17CO4p/+cOfjl76+/X4GYAYThGn7/g+Mfh/ZZwjUA/aABpJVhpv6+dQUjZP78Z0YEK7OezS2gwltg64GmfTu6i+HL+mUMP34wgvGvL78ZOEysf8M1sGgZvQIqfA1SDAL8iUUMPIFRQLf+AmMQ4DQ0vYYSrL9vXDz2sq9LFsiX4dLRA0t8OX0SHKzi5bXf2HUMBVA0gN356N7p7xdOS3w5fAgcfNxWtn+BJi9gVVBOQfYPQIABABvRq3BwGT3OAAAAAElFTkSuQmCC);
background-position: right top;
background-repeat: no-repeat;
box-shadow: none;
}
html {
height: 100%;
margin:0;
padding:0;
font-size: 14px;
}
body{
display: grid;
height: 100vh;
margin: 0;
grid:
"search_field logo" auto
"search logo" auto
"navigation navigation" auto
"result result" 1fr
/ 1fr auto;
}
@font-face {
font-family: 'FontAwesome';
src: url('../fonts/fontawesome-webfont.eot?v=4.7.0');
src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
font-weight: normal;
font-style: normal;
}
.group {
display: inline-block;
}
#logo {
grid-area: logo;
font-size: 1.2em;
font-weight: bold;
background-color: black;
color: white;
padding: 3px;
}
#search-input-wrapper {
grid-area: search_field;
}
#search-bar {
grid-area: search;
background-color: #aaa;
padding-bottom:3px;
}
#navigation {
grid-area: navigation;
background-color: #aaa;
display: flex;
justify-content: space-between;
}
.autocomplete .active {
background-color: #AAA;
}
/* scrollbars are nice, but with them an empty autocomplete box is shown
.autocomplete, #search-input-wrapper .autocomplete.open {
overflow-y: scroll;
}
*/
#search-input {
box-sizing: border-box;
border: 0;
}
#search-limit-value {
width: 4em;
}
.input_date {
max-width: 10em;
}
#add-filter {
float:right;
}
#button-bar {
text-align: right;
}
#search-submit {
margin-left:3px;
margin-right:3px;
}
#result-view {
grid-area: result;
background: #eee;
margin: 0;
padding: 0;
overflow: hidden;
}
#result-view i {
background: white;
display:inline;
font-style:normal;
line-height: 1.2em;
}
.center
{
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
input:required:invalid {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAT1JREFUeNpi/P//PwMpgImBRMACY/x7/uDX39sXt/67cMoDyOVgMjBjYFbV/8kkqcCBrIER5KS/967s+rmkXxzI5wJiRSBm/v8P7NTfHHFFl5mVdIzhGv4+u///x+xmuAlcdXPB9KeqeLgYd3bDU2ZpRRmwH4DOeAI07QXIRKipYPD35184/nn17CO4p/+cOfjl76+/X4GYAYThGn7/g+Mfh/ZZwjUA/aABpJVhpv6+dQUjZP78Z0YEK7OezS2gwltg64GmfTu6i+HL+mUMP34wgvGvL78ZOEysf8M1sGgZvQIqfA1SDAL8iUUMPIFRQLf+AmMQ4DQ0vYYSrL9vXDz2sq9LFsiX4dLRA0t8OX0SHKzi5bXf2HUMBVA0gN356N7p7xdOS3w5fAgcfNxWtn+BJi9gVVBOQfYPQIABABvRq3BwGT3OAAAAAElFTkSuQmCC);
background-position: right top;
background-repeat: no-repeat;
box-shadow: none;
}

View File

@@ -1,330 +1,331 @@
$(document).ready(function(){
$('#search-submit').click(plot);
renderGroupBy();
updateSearchLimitValue();
$('#search-limit-by').change(updateSearchLimitValue);
$('#nav_left').click(dateLeftShift);
$('#nav_left_half').click(dateHalfLeftShift);
$('#nav_right_half').click(dateHalfRightShift);
$('#nav_right').click(dateRightShift);
$('#zoom_in').click(zoomIn);
$('#zoom_out').click(zoomOut);
AutoComplete({
HttpMethod: "GET",
Delay: 300,
_QueryArg: function() {
var caretIndex = document.getElementById('search-input').selectionStart + 1;
return 'caretIndex=' + caretIndex + '&query';
},
_Pre: function() {
return encodeURI(this.Input.value);
},
_Post: function(response) {
var result = [];
var responseObject = JSON.parse(response);
responseObject['proposals'].forEach(function(item, index){
var proposal = {};
proposal['Label'] = item.value;
proposal['Value'] = item.proposedQuery;
result.push(proposal);
});
console.log(JSON.stringify(result));
return result;
}
});
});
function zoomIn()
{
shiftDate(0.25);
zoom(0.5);
plot();
}
function zoomOut()
{
shiftDate(-0.5);
zoom(2);
plot();
}
function dateLeftShift()
{
shiftDate(-1);
plot();
}
function dateHalfLeftShift()
{
shiftDate(-0.5);
plot();
}
function dateHalfRightShift()
{
shiftDate(0.5);
plot();
}
function dateRightShift()
{
shiftDate(1);
plot();
}
function zoom(factor)
{
if (!$('#search-date-range').is(":invalid")) {
var dateRange = $('#search-date-range').val();
var tokens = dateRange.split(/ +/,2);
if(tokens.length == 2)
{
var value = parseInt(tokens[0]);
var period = tokens[1];
var newValue = value*factor;
while (newValue != Math.round(newValue)){
switch (period) {
case "second":
case "seconds":
if (value == 1) {
// we reached the smallest range
}
else if (value % 2 == 1){
value = value -1;
}
break;
case "minute":
case "minutes":
value = value * 60;
period = "seconds";
break;
case "hour":
case "hours":
value = value * 60;
period = "minutes";
break;
case "day":
case "days":
value = value * 24;
period = "hours";
break;
case "week":
case "weeks":
value = value * 7;
period = "days";
break;
case "month":
case "months":
value = value * 30;
period = "days";
break;
default:
console.log("unhandled value: "+ period);
break;
}
newValue = value*factor
}
$('#search-date-range').val(newValue + " " + period);
}
}
}
function shiftDate(directionalFactor)
{
var dateBefore = Date.parse($('#search-date-from').val());
var newDate = shiftByInterval(dateBefore, directionalFactor);
$('#search-date-from').val(newDate.toString("yyyy-MM-dd HH:mm:ss"));
}
function shiftByInterval(date, directionalFactor)
{
if (!$('#search-date-range').is(":invalid")) {
var dateRange = $('#search-date-range').val();
var tokens = dateRange.split(/ +/,2);
if(tokens.length == 2)
{
var value = parseInt(tokens[0]);
var period = tokens[1];
var config = {};
value = directionalFactor * value;
switch (period) {
case "second":
case "seconds":
config = { seconds: value };
break;
case "minute":
case "minutes":
config = { minutes: value };
break;
case "hour":
case "hours":
config = { minutes: 60*value };
break;
case "day":
case "days":
config = { days: value };
break;
case "week":
case "weeks":
config = { days: 7*value };
break;
case "month":
case "months":
config = { days: 30*value };
break;
default:
break;
}
var newDate = date.add(config);
return newDate;
}
}
return date;
}
function updateSearchLimitValue () {
var optionSelected = $('#search-limit-by').find("option:selected");
var valueSelected = optionSelected.val();
if (valueSelected == "NO_LIMIT"){
$('#search-limit-value').hide();
}else{
$('#search-limit-value').show();
}
}
function renderGroupBy()
{
var request = {};
var success = function(response){
initSearchGroupBy('#search-group-by-1', response);
initSearchGroupBy('#search-group-by-2', response);
initSearchGroupBy('#search-group-by-3', response);
};
var error = function(e) {
$('#result-view').text("FAILED: " + JSON.parse(e.responseText).message);
};
getJson("fields", request, success, error);
}
function initSearchGroupBy(selector, response)
{
$(selector).empty();
var option = new Option("", "");
$(selector).append($(option));
response.forEach(
(item, index) => {
var option = new Option(item, item);
$(selector).append($(option));
}
);
}
function showLoadingIcon()
{
$('#result-view').html("<div class='center'><div class='uil-cube-css' style='-webkit-transform:scale(0.41)'><div /><div></div><div></div><div></div></div></div>");
}
function plot(event){
if(event){
event.preventDefault(); // prevent submit of form which would reload the page
}
showLoadingIcon();
var request = {};
request['query'] = $('#search-input').val();
request['height'] = $('#result-view').height();
request['width'] = $('#result-view').width();
request['groupBy'] = groupBy();
request['limitBy'] = $('#search-limit-by').val();
request['limit'] = parseInt($('#search-limit-value').val());
request['dateFrom'] = $('#search-date-from').val();
request['dateRange'] = $('#search-date-range').val();
request['axisScale'] = $('#search-y-axis-scale').val();
request['aggregate'] = $('#show-aggregate').val();
var success = function(response){
$('#result-view').html('<img src=\"'+response.imageUrls+'" />');
};
var error = function(e) {
if (e.status == 404){
$('#result-view').text("No data points found.");
}
else{
$('#result-view').text("FAILED: " + JSON.parse(e.responseText).message);
}
};
postJson("plots", request, success, error);
}
function groupBy()
{
var result = [];
for (var i = 1; i <= 3; i++)
{
if ($('#search-group-by-'+i).val() != "")
{
result.push($('#search-group-by-'+i).val());
}
}
return result;
}
function postJson(url, requestData, successCallback, errorCallback) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify(requestData),
contentType: 'application/json'
})
.done(successCallback)
.fail(errorCallback);
}
function getJson(url, requestData, successCallback, errorCallback) {
$.ajax({
type: "GET",
url: url,
data: requestData,
contentType: 'application/json'
})
.done(successCallback)
.fail(errorCallback);
}
$(document).ready(function(){
$('#search-submit').click(plot);
renderGroupBy();
updateSearchLimitValue();
$('#search-limit-by').change(updateSearchLimitValue);
$('#nav_left').click(dateLeftShift);
$('#nav_left_half').click(dateHalfLeftShift);
$('#nav_right_half').click(dateHalfRightShift);
$('#nav_right').click(dateRightShift);
$('#zoom_in').click(zoomIn);
$('#zoom_out').click(zoomOut);
AutoComplete({
HttpMethod: "GET",
Delay: 300,
_QueryArg: function() {
var caretIndex = document.getElementById('search-input').selectionStart + 1;
return 'caretIndex=' + caretIndex + '&query';
},
_Pre: function() {
return encodeURI(this.Input.value);
},
_Post: function(response) {
var result = [];
var responseObject = JSON.parse(response);
responseObject['proposals'].forEach(function(item, index){
var proposal = {};
proposal['Label'] = item.value;
proposal['Value'] = item.proposedQuery;
result.push(proposal);
});
console.log(JSON.stringify(result));
return result;
}
});
});
function zoomIn()
{
shiftDate(0.25);
zoom(0.5);
plot();
}
function zoomOut()
{
shiftDate(-0.5);
zoom(2);
plot();
}
function dateLeftShift()
{
shiftDate(-1);
plot();
}
function dateHalfLeftShift()
{
shiftDate(-0.5);
plot();
}
function dateHalfRightShift()
{
shiftDate(0.5);
plot();
}
function dateRightShift()
{
shiftDate(1);
plot();
}
function zoom(factor)
{
if (!$('#search-date-range').is(":invalid")) {
var dateRange = $('#search-date-range').val();
var tokens = dateRange.split(/ +/,2);
if(tokens.length == 2)
{
var value = parseInt(tokens[0]);
var period = tokens[1];
var newValue = value*factor;
while (newValue != Math.round(newValue)){
switch (period) {
case "second":
case "seconds":
if (value == 1) {
// we reached the smallest range
}
else if (value % 2 == 1){
value = value -1;
}
break;
case "minute":
case "minutes":
value = value * 60;
period = "seconds";
break;
case "hour":
case "hours":
value = value * 60;
period = "minutes";
break;
case "day":
case "days":
value = value * 24;
period = "hours";
break;
case "week":
case "weeks":
value = value * 7;
period = "days";
break;
case "month":
case "months":
value = value * 30;
period = "days";
break;
default:
console.log("unhandled value: "+ period);
break;
}
newValue = value*factor
}
$('#search-date-range').val(newValue + " " + period);
}
}
}
function shiftDate(directionalFactor)
{
var dateBefore = Date.parse($('#search-date-from').val());
var newDate = shiftByInterval(dateBefore, directionalFactor);
$('#search-date-from').val(newDate.toString("yyyy-MM-dd HH:mm:ss"));
}
function shiftByInterval(date, directionalFactor)
{
if (!$('#search-date-range').is(":invalid")) {
var dateRange = $('#search-date-range').val();
var tokens = dateRange.split(/ +/,2);
if(tokens.length == 2)
{
var value = parseInt(tokens[0]);
var period = tokens[1];
var config = {};
value = directionalFactor * value;
switch (period) {
case "second":
case "seconds":
config = { seconds: value };
break;
case "minute":
case "minutes":
config = { minutes: value };
break;
case "hour":
case "hours":
config = { minutes: 60*value };
break;
case "day":
case "days":
config = { days: value };
break;
case "week":
case "weeks":
config = { days: 7*value };
break;
case "month":
case "months":
config = { days: 30*value };
break;
default:
break;
}
var newDate = date.add(config);
return newDate;
}
}
return date;
}
function updateSearchLimitValue () {
var optionSelected = $('#search-limit-by').find("option:selected");
var valueSelected = optionSelected.val();
if (valueSelected == "NO_LIMIT"){
$('#search-limit-value').hide();
}else{
$('#search-limit-value').show();
}
}
function renderGroupBy()
{
var request = {};
var success = function(response){
initSearchGroupBy('#search-group-by-1', response);
initSearchGroupBy('#search-group-by-2', response);
initSearchGroupBy('#search-group-by-3', response);
};
var error = function(e) {
$('#result-view').text("FAILED: " + JSON.parse(e.responseText).message);
};
getJson("fields", request, success, error);
}
function initSearchGroupBy(selector, response)
{
$(selector).empty();
var option = new Option("", "");
$(selector).append($(option));
response.forEach(
(item, index) => {
var option = new Option(item, item);
$(selector).append($(option));
}
);
}
function showLoadingIcon()
{
$('#result-view').html("<div class='center'><div class='uil-cube-css' style='-webkit-transform:scale(0.41)'><div /><div></div><div></div><div></div></div></div>");
}
function plot(event){
if(event){
event.preventDefault(); // prevent submit of form which would reload the page
}
showLoadingIcon();
var request = {};
request['query'] = $('#search-input').val();
request['height'] = $('#result-view').height();
request['width'] = $('#result-view').width();
request['groupBy'] = groupBy();
request['limitBy'] = $('#search-limit-by').val();
request['limit'] = parseInt($('#search-limit-value').val());
request['dateFrom'] = $('#search-date-from').val();
request['dateRange'] = $('#search-date-range').val();
request['axisScale'] = $('#search-y-axis-scale').val();
request['aggregate'] = $('#show-aggregate').val();
request['keyOutside'] = $('#key-outside').is(":checked");
var success = function(response){
$('#result-view').html('<img src=\"'+response.imageUrls+'" />');
};
var error = function(e) {
if (e.status == 404){
$('#result-view').text("No data points found.");
}
else{
$('#result-view').text("FAILED: " + JSON.parse(e.responseText).message);
}
};
postJson("plots", request, success, error);
}
function groupBy()
{
var result = [];
for (var i = 1; i <= 3; i++)
{
if ($('#search-group-by-'+i).val() != "")
{
result.push($('#search-group-by-'+i).val());
}
}
return result;
}
function postJson(url, requestData, successCallback, errorCallback) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify(requestData),
contentType: 'application/json'
})
.done(successCallback)
.fail(errorCallback);
}
function getJson(url, requestData, successCallback, errorCallback) {
$.ajax({
type: "GET",
url: url,
data: requestData,
contentType: 'application/json'
})
.done(successCallback)
.fail(errorCallback);
}

View File

@@ -1,83 +1,96 @@
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="js/jquery-3.2.0.min.js"></script>
<script type="text/javascript" src="js/search.js"></script>
<script type="text/javascript" src="js/autocomplete.js"></script>
<script type="text/javascript" src="js/date.js"></script>
<link rel="stylesheet" type="text/css" href="css/typography.css">
<link rel="stylesheet" type="text/css" href="css/design.css">
<link rel="stylesheet" type="text/css" href="css/loading.css">
<link rel="stylesheet" type="text/css" href="css/autocomplete.min.css">
<link rel="stylesheet" type="text/css" href="css/font-awesome.min.css">
</head>
<body>
<div id="logo" aria-hidden="true">LOGO</div>
<div id="search-input-wrapper">
<input id="search-input" placeholder="field=value and anotherField=anotherValue" data-autocomplete="autocomplete"
data-autocomplete-empty-message="nothing found" />
</div>
<div id="search-bar">
<form>
<label for="search-group-by-1">Group By:</label>
<select id="search-group-by-1"></select>
<select id="search-group-by-2"></select>
<select id="search-group-by-3"></select>
<label for="search-limit-by">Limit By:</label>
<select id="search-limit-by">
<option value="NO_LIMIT" selected="selected">no limit</option>
<option value="MOST_VALUES">most values</option>
<option value="FEWEST_VALUES">fewest values</option>
<option value="MAX_VALUE">max value</option>
<option value="MIN_VALUE">min value</option>
</select>
<input type="number" id="search-limit-value" name="search-limit-value" min="1" max="1000" value="10"/>
<label for="search-date-from">From Date:</label>
<input id="search-date-from" class="input_date" type="text" value="{{oldestValue}}" required="required" pattern="\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2]\d|3[0-1]) [0-2]\d:[0-5]\d:[0-5]\d">
<label for="search-date-range">Interval:</label>
<input id="search-date-range" type="text" list="ranges" required="required" value="1 week" pattern="\d+ (second|minute|hour|day|week|month)s?">
<datalist id="ranges">
<option value="60 seconds">
<option value="5 minutes">
<option value="1 hour">
<option value="1 day">
<option value="1 week">
<option value="1 month">
</datalist>
<label for="search-y-axis-scale">Y-Axis:</label>
<select id="search-y-axis-scale">
<option value="LINEAR" selected="selected">linear</option>
<option value="LOG10">log 10</option>
<option value="LOG2">log 2</option>
</select>
<label for="show-aggregate">Aggregate:</label>
<select id="show-aggregate">
<option value="NONE" selected="selected">-</option>
<option value="MEAN">Mean</option>
</select>
<button id="search-submit"><i class="fa fa-area-chart" aria-hidden="true"></i> Plot</button>
</form>
</div>
<div id="navigation">
<button id="nav_left"><i class="fa fa-angle-double-left" aria-hidden="true"></i></button>
<button id="nav_left_half"><i class="fa fa-angle-left" aria-hidden="true"></i></button>
<div>
<button id="zoom_in"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
<button id="zoom_out"><i class="fa fa-minus-circle" aria-hidden="true"></i></button>
</div>
<button id="nav_right_half"><i class="fa fa-angle-right" aria-hidden="true"></i></button>
<button id="nav_right"><i class="fa fa-angle-double-right" aria-hidden="true"></i></button>
</div>
<div id="result-view">
</div>
</body>
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="js/jquery-3.2.0.min.js"></script>
<script type="text/javascript" src="js/search.js"></script>
<script type="text/javascript" src="js/autocomplete.js"></script>
<script type="text/javascript" src="js/date.js"></script>
<link rel="stylesheet" type="text/css" href="css/typography.css">
<link rel="stylesheet" type="text/css" href="css/design.css">
<link rel="stylesheet" type="text/css" href="css/loading.css">
<link rel="stylesheet" type="text/css" href="css/autocomplete.min.css">
<link rel="stylesheet" type="text/css" href="css/font-awesome.min.css">
</head>
<body>
<div id="logo" aria-hidden="true">LOGO</div>
<div id="search-input-wrapper">
<input id="search-input" placeholder="field=value and anotherField=anotherValue" data-autocomplete="autocomplete"
data-autocomplete-empty-message="nothing found" />
</div>
<div id="search-bar">
<form>
<div id="search-settings-bar">
<div class="group">
<label for="search-group-by-1">Group By:</label>
<select id="search-group-by-1"></select>
<select id="search-group-by-2"></select>
<select id="search-group-by-3"></select>
</div>
<div class="group">
<label for="search-limit-by">Limit By:</label>
<select id="search-limit-by">
<option value="NO_LIMIT" selected="selected">no limit</option>
<option value="MOST_VALUES">most values</option>
<option value="FEWEST_VALUES">fewest values</option>
<option value="MAX_VALUE">max value</option>
<option value="MIN_VALUE">min value</option>
</select>
<input type="number" id="search-limit-value" name="search-limit-value" min="1" max="1000" value="10"/>
</div>
<div class="group">
<label for="search-date-from">From Date:</label>
<input id="search-date-from" class="input_date" type="text" value="{{oldestValue}}" required="required" pattern="\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2]\d|3[0-1]) [0-2]\d:[0-5]\d:[0-5]\d">
</div>
<div class="group">
<label for="search-date-range">Interval:</label>
<input id="search-date-range" type="text" list="ranges" required="required" value="1 week" pattern="\d+ (second|minute|hour|day|week|month)s?">
<datalist id="ranges">
<option value="60 seconds">
<option value="5 minutes">
<option value="1 hour">
<option value="1 day">
<option value="1 week">
<option value="1 month">
</datalist>
</div>
<div class="group">
<label for="search-y-axis-scale">Y-Axis:</label>
<select id="search-y-axis-scale">
<option value="LINEAR" selected="selected">linear</option>
<option value="LOG10">log 10</option>
<option value="LOG2">log 2</option>
</select>
</div>
<div class="group">
<label for="show-aggregate">Aggregate:</label>
<select id="show-aggregate">
<option value="NONE" selected="selected">-</option>
<option value="MEAN">Mean</option>
</select>
</div>
<div class="group">
<input type="checkbox" id="key-outside" />
<label for="key-outside">Legend outside</label>
</div>
<button id="search-submit"><i class="fa fa-area-chart" aria-hidden="true"></i> Plot</button>
</div>
</form>
</div>
<div id="navigation">
<button id="nav_left"><i class="fa fa-angle-double-left" aria-hidden="true"></i></button>
<button id="nav_left_half"><i class="fa fa-angle-left" aria-hidden="true"></i></button>
<div>
<button id="zoom_in"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
<button id="zoom_out"><i class="fa fa-minus-circle" aria-hidden="true"></i></button>
</div>
<button id="nav_right_half"><i class="fa fa-angle-right" aria-hidden="true"></i></button>
<button id="nav_right"><i class="fa fa-angle-double-right" aria-hidden="true"></i></button>
</div>
<div id="result-view">
</div>
</body>
</html>