Added a players online graph to Query page to help selecting view

This commit is contained in:
Risto Lahtela 2021-01-29 11:58:06 +02:00
parent b8b9af828b
commit 4b4aa2d7d9
9 changed files with 179 additions and 21 deletions

View File

@ -138,8 +138,9 @@ public class GraphJSONCreator {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long halfYearAgo = now - TimeUnit.DAYS.toMillis(180L); long halfYearAgo = now - TimeUnit.DAYS.toMillis(180L);
List<Point> points = Lists.map(db.query(TPSQueries.fetchPlayersOnlineOfServer(halfYearAgo, now, serverUUID)), List<Point> points = Lists.map(
point -> new Point(point.getDate(), point.getValue()) db.query(TPSQueries.fetchPlayersOnlineOfServer(halfYearAgo, now, serverUUID)),
Point::fromDateObj
); );
return "{\"playersOnline\":" + graphs.line().lineGraph(points).toHighChartsSeries() + return "{\"playersOnline\":" + graphs.line().lineGraph(points).toHighChartsSeries() +
",\"color\":\"" + theme.getValue(ThemeVal.GRAPH_PLAYERS_ONLINE) + "\"}"; ",\"color\":\"" + theme.getValue(ThemeVal.GRAPH_PLAYERS_ONLINE) + "\"}";

View File

@ -44,8 +44,12 @@ public class LineGraphFactory {
} }
public LineGraph lineGraph(List<Point> points) { public LineGraph lineGraph(List<Point> points) {
return lineGraph(points, shouldDisplayGapsInData());
}
public LineGraph lineGraph(List<Point> points, boolean displayGaps) {
points.sort(new PointComparator()); points.sort(new PointComparator());
return new LineGraph(points, shouldDisplayGapsInData()); return new LineGraph(points, displayGaps);
} }
public LineGraph chunkGraph(TPSMutator mutator) { public LineGraph chunkGraph(TPSMutator mutator) {

View File

@ -16,6 +16,8 @@
*/ */
package com.djrapitops.plan.delivery.rendering.json.graphs.line; package com.djrapitops.plan.delivery.rendering.json.graphs.line;
import com.djrapitops.plan.delivery.domain.DateObj;
import java.util.Objects; import java.util.Objects;
/** /**
@ -23,15 +25,21 @@ import java.util.Objects;
*/ */
public class Point { public class Point {
private final double x; private final double x;
private final Double y; private Double y;
public Point(double x, Double y) { public Point(double x, Double y) {
this.x = x; this.x = x;
this.y = y; this.y = y;
} }
public Point(double x, double y) { public <V extends Number> Point(double x, V y) {
this(x, (Double) y); this.x = x;
this.y = y == null ? null : y.doubleValue();
}
public static <V extends Number> Point fromDateObj(DateObj<V> dateObj) {
V value = dateObj.getValue();
return new Point(dateObj.getDate(), value != null ? value.doubleValue() : null);
} }
public double getX() { public double getX() {
@ -42,6 +50,10 @@ public class Point {
return y; return y;
} }
public void setY(Double y) {
this.y = y;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@ -63,7 +75,7 @@ public class Point {
"y=" + y + '}'; "y=" + y + '}';
} }
public double[] toArray() { public Double[] toArray() {
return new double[]{x, y}; return new Double[]{x, y};
} }
} }

View File

@ -16,15 +16,23 @@
*/ */
package com.djrapitops.plan.delivery.webserver.resolver.json; package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.domain.DateObj;
import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters; import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.json.graphs.Graphs;
import com.djrapitops.plan.delivery.rendering.json.graphs.line.Point;
import com.djrapitops.plan.delivery.web.resolver.MimeType; import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver; import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response; import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request; import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser; import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.filter.Filter; import com.djrapitops.plan.storage.database.queries.filter.Filter;
import com.djrapitops.plan.storage.database.queries.filter.QueryFilters; import com.djrapitops.plan.storage.database.queries.filter.QueryFilters;
import com.djrapitops.plan.storage.database.queries.objects.SessionQueries;
import com.djrapitops.plan.storage.database.queries.objects.TPSQueries;
import com.djrapitops.plan.utilities.java.Lists;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import javax.inject.Inject; import javax.inject.Inject;
@ -34,19 +42,29 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Singleton @Singleton
public class FiltersJSONResolver implements Resolver { public class FiltersJSONResolver implements Resolver {
private final ServerInfo serverInfo;
private final DBSystem dbSystem;
private final QueryFilters filters; private final QueryFilters filters;
private final Graphs graphs;
private final Formatters formatters; private final Formatters formatters;
@Inject @Inject
public FiltersJSONResolver( public FiltersJSONResolver(
ServerInfo serverInfo,
DBSystem dbSystem,
QueryFilters filters, QueryFilters filters,
Graphs graphs,
Formatters formatters Formatters formatters
) { ) {
this.serverInfo = serverInfo;
this.dbSystem = dbSystem;
this.filters = filters; this.filters = filters;
this.graphs = graphs;
this.formatters = formatters; this.formatters = formatters;
} }
@ -62,11 +80,23 @@ public class FiltersJSONResolver implements Resolver {
} }
private Response getResponse() { private Response getResponse() {
List<DateObj<Integer>> data = dbSystem.getDatabase().query(TPSQueries.fetchQueryPreviewPlayersOnline(serverInfo.getServerUUID()));
Long earliestStart = dbSystem.getDatabase().query(SessionQueries.earliestSessionStart());
data.add(0, new DateObj<>(earliestStart, 1));
boolean displayGaps = true;
List<Double[]> viewPoints = graphs.line().lineGraph(Lists.map(data, Point::fromDateObj), displayGaps).getPoints()
.stream().map(point -> {
if (point.getY() == null) point.setY(0.0);
return point.toArray();
}).collect(Collectors.toList());
return Response.builder() return Response.builder()
.setMimeType(MimeType.JSON) .setMimeType(MimeType.JSON)
.setJSONContent(new FilterResponseJSON( .setJSONContent(new FilterResponseJSON(
filters.getFilters(), filters.getFilters(),
new ViewJSON(formatters) new ViewJSON(formatters),
viewPoints
)).build(); )).build();
} }
@ -76,8 +106,10 @@ public class FiltersJSONResolver implements Resolver {
static class FilterResponseJSON { static class FilterResponseJSON {
final List<FilterJSON> filters; final List<FilterJSON> filters;
final ViewJSON view; final ViewJSON view;
final List<Double[]> viewPoints;
public FilterResponseJSON(Map<String, Filter> filtersByKind, ViewJSON view) { public FilterResponseJSON(Map<String, Filter> filtersByKind, ViewJSON view, List<Double[]> viewPoints) {
this.viewPoints = viewPoints;
this.filters = new ArrayList<>(); this.filters = new ArrayList<>();
for (Map.Entry<String, Filter> entry : filtersByKind.entrySet()) { for (Map.Entry<String, Filter> entry : filtersByKind.entrySet()) {
filters.add(new FilterJSON(entry.getKey(), entry.getValue())); filters.add(new FilterJSON(entry.getKey(), entry.getValue()));

View File

@ -892,4 +892,15 @@ public class SessionQueries {
} }
}; };
} }
public static Query<Long> earliestSessionStart() {
String sql = SELECT + "MIN(" + SessionsTable.SESSION_START + ") as m" +
FROM + SessionsTable.TABLE_NAME;
return new QueryAllStatement<Long>(sql) {
@Override
public Long processResults(ResultSet set) throws SQLException {
return set.next() ? set.getLong("m") : -1L;
}
};
}
} }

View File

@ -154,6 +154,29 @@ public class TPSQueries {
}; };
} }
public static Query<List<DateObj<Integer>>> fetchQueryPreviewPlayersOnline(UUID serverUUID) {
String sql = SELECT + "MIN(" + DATE + ") as " + DATE + ',' +
"MAX(" + PLAYERS_ONLINE + ") as " + PLAYERS_ONLINE +
FROM + TABLE_NAME +
WHERE + SERVER_ID + "=" + ServerTable.STATEMENT_SELECT_SERVER_ID +
GROUP_BY + "FLOOR(" + DATE + "/?)";
return new QueryStatement<List<DateObj<Integer>>>(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setString(1, serverUUID.toString());
statement.setLong(2, TimeUnit.MINUTES.toMillis(15));
}
@Override
public List<DateObj<Integer>> processResults(ResultSet set) throws SQLException {
List<DateObj<Integer>> ofServer = new ArrayList<>();
while (set.next()) ofServer.add(new DateObj<>(set.getLong(DATE), set.getInt(PLAYERS_ONLINE)));
return ofServer;
}
};
}
public static Query<List<DateObj<Integer>>> fetchPlayersOnlineOfServer(long after, long before, UUID serverUUID) { public static Query<List<DateObj<Integer>>> fetchPlayersOnlineOfServer(long after, long before, UUID serverUUID) {
String sql = SELECT + ServerTable.SERVER_UUID + ',' + DATE + ',' + PLAYERS_ONLINE + String sql = SELECT + ServerTable.SERVER_UUID + ',' + DATE + ',' + PLAYERS_ONLINE +
FROM + TABLE_NAME + FROM + TABLE_NAME +

View File

@ -232,11 +232,12 @@ function isValidDate(value) {
const d = value.match( const d = value.match(
/^(0\d|\d{2})[\/|\-]?(0\d|\d{2})[\/|\-]?(\d{4,5})$/ /^(0\d|\d{2})[\/|\-]?(0\d|\d{2})[\/|\-]?(\d{4,5})$/
); );
if (!d) return false;
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
const parsedDay = Number(d[1]); const parsedDay = Number(d[1]);
const parsedMonth = Number(d[2]) - 1; // 0=January, 11=December const parsedMonth = Number(d[2]) - 1; // 0=January, 11=December
const parsedYear = Number(d[3]); const parsedYear = Number(d[3]);
return d ? new Date(parsedYear, parsedMonth, parsedDay) : null; return new Date(parsedYear, parsedMonth, parsedDay);
} }
function correctDate(value) { function correctDate(value) {
@ -270,11 +271,11 @@ function isValidTime(value) {
} }
function correctTime(value) { function correctTime(value) {
const d = value.match(/^(\d{2}):?(\d{2})$/); const d = value.match(/^(0\d|\d{2}):?(0\d|\d{2})$/);
if (!d) return value; if (!d) return value;
let hour = d[1]; let hour = Number(d[1]);
while (hour > 23) hour--; while (hour > 23) hour--;
let minute = d[2]; let minute = Number(d[2]);
while (minute > 59) minute--; while (minute > 59) minute--;
return hour + ":" + minute; return hour + ":" + minute;
} }
@ -289,23 +290,49 @@ function setFilterOption(
const query = id === 'view' ? filterView : filterQuery.find(function (f) { const query = id === 'view' ? filterView : filterQuery.find(function (f) {
return f.id === id; return f.id === id;
}); });
const element = $(`#${elementId}`); const element = document.getElementById(elementId);
let value = element.val(); let value = element.value;
value = correctionFunction.apply(element, [value]); value = correctionFunction.apply(element, [value]);
element.val(value); element.value = value;
const isValid = isValidFunction.apply(element, [value]); const isValid = isValidFunction.apply(element, [value]);
if (isValid) { if (isValid) {
element.removeClass("is-invalid"); element.classList.remove("is-invalid");
query[propertyName] = value; query[propertyName] = value; // Updates either the query or filterView properties
InvalidEntries.setAsValid(elementId); InvalidEntries.setAsValid(elementId);
if (id === 'view') updateViewGraph();
} else { } else {
element.addClass("is-invalid"); element.classList.add("is-invalid");
InvalidEntries.setAsInvalid(elementId); InvalidEntries.setAsInvalid(elementId);
} }
} }
function updateViewGraph() {
function parseTime(dateString, timeString) {
const d = dateString.match(
/^(0\d|\d{2})[\/|\-]?(0\d|\d{2})[\/|\-]?(\d{4,5})$/
);
const t = timeString.match(/^(0\d|\d{2}):?(0\d|\d{2})$/);
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
const parsedDay = Number(d[1]);
const parsedMonth = Number(d[2]) - 1; // 0=January, 11=December
const parsedYear = Number(d[3]);
let hour = Number(t[1]);
let minute = Number(t[2]);
return new Date(parsedYear, parsedMonth, parsedDay, hour, minute).getTime();
}
const graph = graphs[0];
const min = parseTime(filterView.afterDate, filterView.afterTime);
const max = parseTime(filterView.beforeDate, filterView.beforeTime);
for (const axis of graph.xAxis) {
axis.setExtremes(min, max);
}
}
let query = []; let query = [];
function performQuery() { function performQuery() {

View File

@ -141,6 +141,8 @@
</div> </div>
</div> </div>
</div> </div>
<div class="chart-area" id="viewChart"><span class="loader"></span></div>
<hr> <hr>
<div id="filters"></div> <div id="filters"></div>
@ -354,6 +356,52 @@
document.getElementById('viewToDateField').setAttribute('placeholder', json.view.beforeDate); document.getElementById('viewToDateField').setAttribute('placeholder', json.view.beforeDate);
document.getElementById('viewToTimeField').setAttribute('placeholder', json.view.beforeTime); document.getElementById('viewToTimeField').setAttribute('placeholder', json.view.beforeTime);
const s = {
name: {playersOnline: 'Players Online'},
tooltip: {zeroDecimals: {valueDecimals: 0}},
type: {areaSpline: 'areaspline'}
};
const playersOnlineSeries = {
name: 'Players Online', type: 'areaspline', tooltip: {valueDecimals: 0},
data: json.viewPoints, color: '#1E90FF', yAxis: 0
}
graphs.push(Highcharts.stockChart('viewChart', {
rangeSelector: {
selected: 3,
buttons: linegraphButtons
},
yAxis: {
softMax: 2,
softMin: 0
},
title: {text: ''},
plotOptions: {
areaspline: {
fillOpacity: 0.4
}
},
series: [playersOnlineSeries],
xAxis: {
events: {
afterSetExtremes: function (event) {
if (this) {
const afterDate = Highcharts.dateFormat('%d/%m/%Y', this.min);
const afterTime = Highcharts.dateFormat('%H:%M', this.min);
const beforeDate = Highcharts.dateFormat('%d/%m/%Y', this.max);
const beforeTime = Highcharts.dateFormat('%H:%M', this.max);
document.getElementById('viewFromDateField').value = afterDate;
document.getElementById('viewFromTimeField').value = afterTime;
document.getElementById('viewToDateField').value = beforeDate;
document.getElementById('viewToTimeField').value = beforeTime;
filterView = {afterDate, afterTime, beforeDate, beforeTime};
}
}
}
}
}));
let filterElements = ''; let filterElements = '';
for (let i = 0; i < filters.length; i++) { for (let i = 0; i < filters.length; i++) {
filterElements += createFilterSelector('#filters', i, filters[i]); filterElements += createFilterSelector('#filters', i, filters[i]);

View File

@ -1315,7 +1315,7 @@
}); });
// HighCharts Series // HighCharts Series
var s = { const s = {
name: { name: {
playersOnline: 'Players Online', playersOnline: 'Players Online',
uniquePlayers: 'Unique Players', uniquePlayers: 'Unique Players',