make the tics on the y-axis easier readable

People are having trouble to understand durations like
100000 or 2.7E+6 milliseconds. Therefore we are
hanging the labels on the y-axis to include the unit
in the tic's label. We also use multiples of seconds,
minutes, hours and days instead of multiples of 10.
This commit is contained in:
2019-08-25 10:25:47 +02:00
parent 4e9b556ea9
commit 5b57417f75
5 changed files with 186 additions and 21 deletions

View File

@@ -1,5 +1,5 @@
package org.lucares.pdb.plot.api;
public enum AxisScale {
LINEAR, LOG10, LOG2
LINEAR, LOG10
}

View File

@@ -1,5 +1,6 @@
package org.lucares.recommind.logs;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
@@ -107,4 +108,14 @@ public interface DataSeries {
}
}
public static long maxValue(final Collection<DataSeries> dataSeries) {
long result = 0;
for (final DataSeries series : dataSeries) {
result = Math.max(result, series.getMaxValue());
}
return result;
}
}

View File

@@ -1,11 +1,23 @@
package org.lucares.recommind.logs;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.lucares.pdb.plot.api.AxisScale;
public class GnuplotFileGenerator {
private static final int KEY_FONT_SIZE = 10;
private static final int TICKS_FONT_SIZE = 12;
public String generate(final GnuplotSettings settings, final Collection<DataSeries> dataSeries) {
final StringBuilder result = new StringBuilder();
@@ -14,6 +26,10 @@ public class GnuplotFileGenerator {
settings.getHeight());
appendfln(result, "set datafile separator \"%s\"", settings.getDatafileSeparator());
// appendln(result, "set locale \"de_DE.UTF-8\"");
// appendln(result, "set format y \"%.0f\"");
appendfln(result, "set tics font \",%d\"", TICKS_FONT_SIZE);
appendln(result, computeYTicks(settings, dataSeries));
settings.getAggregate().addGnuplotDefinitions(result, settings.getDatafileSeparator(), dataSeries);
@@ -37,16 +53,12 @@ public class GnuplotFileGenerator {
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");
@@ -62,7 +74,7 @@ public class GnuplotFileGenerator {
if (settings.isKeyOutside()) {
appendfln(result, "set key outside");
}
appendfln(result, "set key font \",10\"");
appendfln(result, "set key font \",%d\"", KEY_FONT_SIZE);
if (!settings.isRenderLabels()) {
appendfln(result, "set format x \"\"");
@@ -82,7 +94,7 @@ public class GnuplotFileGenerator {
// (horizontal: 1 unit = 10px; vertical: 1 unit = 19px)
appendln(result, "set lmargin 11"); // margin 11 -> 110px
appendln(result, "set rmargin 11"); // margin 11 -> 110px
appendln(result, "set tmargin 3"); // margin 3 -> 57px
appendln(result, "set tmargin 3"); // margin 3 -> 57px - marker (1)
appendln(result, "set bmargin 4"); // margin 4 -> 76
}
@@ -109,4 +121,158 @@ public class GnuplotFileGenerator {
builder.append(String.format(format, args));
}
private String computeYTicks(final GnuplotSettings settings, final Collection<DataSeries> dataSeries) {
String result = "";
final long yRangeMax;
final long yRangeMin;
if (settings.hasYRange()) {
yRangeMax = settings.getYRangeMax();
yRangeMin = settings.getYRangeMin();
} else {
yRangeMax = DataSeries.maxValue(dataSeries);
yRangeMin = 0;
}
final int height = settings.getHeight();
switch (settings.getYAxisScale()) {
case LINEAR:
result = computeLinearYTicks(height, yRangeMin, yRangeMax);
break;
case LOG10:
result = computeLog10YTicks(height, yRangeMin, yRangeMax);
break;
default:
// use the default
}
return result;
}
private String computeLog10YTicks(final int height, final long yRangeMin, final long yRangeMax) {
final StringBuilder result = new StringBuilder();
appendfln(result, "set ylabel \"%s\"", "Duration");
final List<String> ticsLabels = Arrays.asList(//
"\"1ms\" 1", //
"\"2ms\" 2", //
"\"5ms\" 5", //
"\"10ms\" 10", //
"\"20ms\" 20", //
"\"50ms\" 50", //
"\"100ms\" 100", //
"\"200ms\" 200", //
"\"500ms\" 500", //
"\"1s\" 1000", //
"\"2s\" 2000", //
"\"5s\" 5000", //
"\"10s\" 10000", //
"\"30s\" 30000", //
"\"1m\" 60000", //
"\"2m\" 120000", //
"\"5m\" 300000", //
"\"10m\" 600000", //
"\"30m\" 1800000", //
"\"1h\" 3600000", //
"\"2h\" 7200000", //
"\"4h\" 14400000", //
"\"8h\" 28800000", //
"\"16h\" 57600000", //
"\"1d\" 86400000", //
"\"2d\" 172800000", //
"\"1 week\" 604800000", //
"\"2 week\" 1209600000.0", //
"\"4 week\" 2419200000.0" //
);
result.append("set ytics (");
result.append(String.join(", ", ticsLabels));
result.append(")\n");
return result.toString();
}
private String computeLinearYTicks(final long height, final long yRangeMin, final long yRangeMax) {
final StringBuilder result = new StringBuilder();
appendfln(result, "set ylabel \"%s\"", "Duration");
final long plotHeight = height - 133; // sum of top/bottom margin, see marker (1)
final long maxLabels = plotHeight / (TICKS_FONT_SIZE * 5);
System.out.println("labels: " + maxLabels);
final long range = yRangeMax - yRangeMin;
final long msPerLabel = roundToLinearLabelSteps(range / maxLabels);
final long digits = (long) Math.ceil(Math.log10(msPerLabel));
System.out.println("range: " + range);
System.out.println("msPerLabel: " + msPerLabel);
System.out.println("digits: " + digits);
final List<String> ticsLabels = new ArrayList<>();
for (long i = yRangeMin; i < yRangeMax; i += msPerLabel) {
ticsLabels.add("\"" + msToTic(i, msPerLabel) + "\" " + i);
}
ticsLabels.add("\"" + msToTic(yRangeMax, msPerLabel) + "\" " + yRangeMax);
result.append("set ytics (");
result.append(String.join(", ", ticsLabels));
result.append(")\n");
return result.toString();
}
private long roundToLinearLabelSteps(final long msPerLabel) {
final List<Long> steps = Arrays.asList(2L, 5L, 10L, 20L, 50L, 100L, 200L, 500L, 1000L, 2000L, 5000L, 10_000L,
20_000L, MINUTES.toMillis(1), MINUTES.toMillis(2), MINUTES.toMillis(5), MINUTES.toMillis(10),
MINUTES.toMillis(15), MINUTES.toMillis(30), HOURS.toMillis(1), HOURS.toMillis(2), HOURS.toMillis(5),
HOURS.toMillis(10), HOURS.toMillis(12), DAYS.toMillis(1), DAYS.toMillis(2), DAYS.toMillis(5),
DAYS.toMillis(7));
for (final Long step : steps) {
if (msPerLabel < step) {
return step;
}
}
return msPerLabel;
}
private String msToTic(final long ms, final double msPerLabel) {
if (ms < 1000) {
return ms + "ms";
} else if (ms < MINUTES.toMillis(1)) {
if (msPerLabel % 1000 == 0) {
return String.format("%ds", ms / 1_000);
} else {
return String.format("%.1fs", ms / 1_000.0);
}
} else if (ms < TimeUnit.HOURS.toMillis(1)) {
final long sec = (ms % MINUTES.toMillis(1)) / SECONDS.toMillis(1);
final long min = ms / MINUTES.toMillis(1);
if (msPerLabel % MINUTES.toMillis(1) == 0) {
return min + "m ";
} else {
return min + "m " + sec + "s";
}
} else if (ms < DAYS.toMillis(1)) {
// ms is a multiple of 1 hour, see roundToLinearLabelSteps
final long hour = (ms % DAYS.toMillis(1)) / HOURS.toMillis(1);
final long min = (ms % HOURS.toMillis(1)) / MINUTES.toMillis(1);
final long sec = (ms % MINUTES.toMillis(1)) / SECONDS.toMillis(1);
if (msPerLabel % MINUTES.toMillis(1) == 0) {
return hour + "h " + min + "m ";
} else if (msPerLabel % HOURS.toMillis(1) == 0) {
return hour + "h ";
} else {
return hour + "h " + min + "m " + sec + "s";
}
} else {
// ms is a multiple of 1 day, see roundToLinearLabelSteps
final long day = ms / DAYS.toMillis(1);
final long hour = (ms % DAYS.toMillis(1)) / HOURS.toMillis(1);
return day + "d " + hour + "h ";
}
}
}

View File

@@ -14,9 +14,6 @@ public class GnuplotSettings {
// set datafile separator
private String datafileSeparator = ",";
// set ylabel
private String ylabel = "Duration in ms";
// set output "datausage.png"
private final Path output;
@@ -81,14 +78,6 @@ public class GnuplotSettings {
this.datafileSeparator = datafileSeparator;
}
public String getYlabel() {
return ylabel;
}
public void setYlabel(final String ylabel) {
this.ylabel = ylabel;
}
public Path getOutput() {
return output;
}

View File

@@ -770,8 +770,7 @@ Vue.component('search-bar', {
<label for="search-y-axis-scale">Y-Axis:</label>
<select id="search-y-axis-scale" v-model="searchBar.axisScale">
<option value="LINEAR" selected="selected">linear</option>
<option value="LOG10">log 10</option>
<option value="LOG2">log 2</option>
<option value="LOG10">log</option>
</select>
</div>
@@ -811,7 +810,7 @@ Vue.component('search-bar', {
<option value="SECONDS" title="seconds">seconds</option>
<option value="MINUTES" title="minutes">minutes</option>
<option value="HOURS" title="hours">hours</option>
<option value="DAYS" title="days">hours</option>
<option value="DAYS" title="days">days</option>
</select>
</div>