mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-12-15 05:41:51 +08:00
Added a players online graph to Query page to help selecting view
This commit is contained in:
parent
b8b9af828b
commit
4b4aa2d7d9
@ -138,8 +138,9 @@ public class GraphJSONCreator {
|
||||
long now = System.currentTimeMillis();
|
||||
long halfYearAgo = now - TimeUnit.DAYS.toMillis(180L);
|
||||
|
||||
List<Point> points = Lists.map(db.query(TPSQueries.fetchPlayersOnlineOfServer(halfYearAgo, now, serverUUID)),
|
||||
point -> new Point(point.getDate(), point.getValue())
|
||||
List<Point> points = Lists.map(
|
||||
db.query(TPSQueries.fetchPlayersOnlineOfServer(halfYearAgo, now, serverUUID)),
|
||||
Point::fromDateObj
|
||||
);
|
||||
return "{\"playersOnline\":" + graphs.line().lineGraph(points).toHighChartsSeries() +
|
||||
",\"color\":\"" + theme.getValue(ThemeVal.GRAPH_PLAYERS_ONLINE) + "\"}";
|
||||
|
@ -44,8 +44,12 @@ public class LineGraphFactory {
|
||||
}
|
||||
|
||||
public LineGraph lineGraph(List<Point> points) {
|
||||
return lineGraph(points, shouldDisplayGapsInData());
|
||||
}
|
||||
|
||||
public LineGraph lineGraph(List<Point> points, boolean displayGaps) {
|
||||
points.sort(new PointComparator());
|
||||
return new LineGraph(points, shouldDisplayGapsInData());
|
||||
return new LineGraph(points, displayGaps);
|
||||
}
|
||||
|
||||
public LineGraph chunkGraph(TPSMutator mutator) {
|
||||
|
@ -16,6 +16,8 @@
|
||||
*/
|
||||
package com.djrapitops.plan.delivery.rendering.json.graphs.line;
|
||||
|
||||
import com.djrapitops.plan.delivery.domain.DateObj;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@ -23,15 +25,21 @@ import java.util.Objects;
|
||||
*/
|
||||
public class Point {
|
||||
private final double x;
|
||||
private final Double y;
|
||||
private Double y;
|
||||
|
||||
public Point(double x, Double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public Point(double x, double y) {
|
||||
this(x, (Double) y);
|
||||
public <V extends Number> Point(double x, V 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() {
|
||||
@ -42,6 +50,10 @@ public class Point {
|
||||
return y;
|
||||
}
|
||||
|
||||
public void setY(Double y) {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
@ -63,7 +75,7 @@ public class Point {
|
||||
"y=" + y + '}';
|
||||
}
|
||||
|
||||
public double[] toArray() {
|
||||
return new double[]{x, y};
|
||||
public Double[] toArray() {
|
||||
return new Double[]{x, y};
|
||||
}
|
||||
}
|
||||
|
@ -16,15 +16,23 @@
|
||||
*/
|
||||
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.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.Resolver;
|
||||
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.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.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 javax.inject.Inject;
|
||||
@ -34,19 +42,29 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Singleton
|
||||
public class FiltersJSONResolver implements Resolver {
|
||||
|
||||
private final ServerInfo serverInfo;
|
||||
private final DBSystem dbSystem;
|
||||
private final QueryFilters filters;
|
||||
private final Graphs graphs;
|
||||
private final Formatters formatters;
|
||||
|
||||
@Inject
|
||||
public FiltersJSONResolver(
|
||||
ServerInfo serverInfo,
|
||||
DBSystem dbSystem,
|
||||
QueryFilters filters,
|
||||
Graphs graphs,
|
||||
Formatters formatters
|
||||
) {
|
||||
this.serverInfo = serverInfo;
|
||||
this.dbSystem = dbSystem;
|
||||
this.filters = filters;
|
||||
this.graphs = graphs;
|
||||
this.formatters = formatters;
|
||||
}
|
||||
|
||||
@ -62,11 +80,23 @@ public class FiltersJSONResolver implements Resolver {
|
||||
}
|
||||
|
||||
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()
|
||||
.setMimeType(MimeType.JSON)
|
||||
.setJSONContent(new FilterResponseJSON(
|
||||
filters.getFilters(),
|
||||
new ViewJSON(formatters)
|
||||
new ViewJSON(formatters),
|
||||
viewPoints
|
||||
)).build();
|
||||
}
|
||||
|
||||
@ -76,8 +106,10 @@ public class FiltersJSONResolver implements Resolver {
|
||||
static class FilterResponseJSON {
|
||||
final List<FilterJSON> filters;
|
||||
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<>();
|
||||
for (Map.Entry<String, Filter> entry : filtersByKind.entrySet()) {
|
||||
filters.add(new FilterJSON(entry.getKey(), entry.getValue()));
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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) {
|
||||
String sql = SELECT + ServerTable.SERVER_UUID + ',' + DATE + ',' + PLAYERS_ONLINE +
|
||||
FROM + TABLE_NAME +
|
||||
|
@ -232,11 +232,12 @@ function isValidDate(value) {
|
||||
const d = value.match(
|
||||
/^(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
|
||||
const parsedDay = Number(d[1]);
|
||||
const parsedMonth = Number(d[2]) - 1; // 0=January, 11=December
|
||||
const parsedYear = Number(d[3]);
|
||||
return d ? new Date(parsedYear, parsedMonth, parsedDay) : null;
|
||||
return new Date(parsedYear, parsedMonth, parsedDay);
|
||||
}
|
||||
|
||||
function correctDate(value) {
|
||||
@ -270,11 +271,11 @@ function isValidTime(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;
|
||||
let hour = d[1];
|
||||
let hour = Number(d[1]);
|
||||
while (hour > 23) hour--;
|
||||
let minute = d[2];
|
||||
let minute = Number(d[2]);
|
||||
while (minute > 59) minute--;
|
||||
return hour + ":" + minute;
|
||||
}
|
||||
@ -289,23 +290,49 @@ function setFilterOption(
|
||||
const query = id === 'view' ? filterView : filterQuery.find(function (f) {
|
||||
return f.id === id;
|
||||
});
|
||||
const element = $(`#${elementId}`);
|
||||
let value = element.val();
|
||||
const element = document.getElementById(elementId);
|
||||
let value = element.value;
|
||||
|
||||
value = correctionFunction.apply(element, [value]);
|
||||
element.val(value);
|
||||
element.value = value;
|
||||
|
||||
const isValid = isValidFunction.apply(element, [value]);
|
||||
if (isValid) {
|
||||
element.removeClass("is-invalid");
|
||||
query[propertyName] = value;
|
||||
element.classList.remove("is-invalid");
|
||||
query[propertyName] = value; // Updates either the query or filterView properties
|
||||
InvalidEntries.setAsValid(elementId);
|
||||
if (id === 'view') updateViewGraph();
|
||||
} else {
|
||||
element.addClass("is-invalid");
|
||||
element.classList.add("is-invalid");
|
||||
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 = [];
|
||||
|
||||
function performQuery() {
|
||||
|
@ -141,6 +141,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-area" id="viewChart"><span class="loader"></span></div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div id="filters"></div>
|
||||
@ -354,6 +356,52 @@
|
||||
document.getElementById('viewToDateField').setAttribute('placeholder', json.view.beforeDate);
|
||||
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 = '';
|
||||
for (let i = 0; i < filters.length; i++) {
|
||||
filterElements += createFilterSelector('#filters', i, filters[i]);
|
||||
|
@ -1315,7 +1315,7 @@
|
||||
});
|
||||
|
||||
// HighCharts Series
|
||||
var s = {
|
||||
const s = {
|
||||
name: {
|
||||
playersOnline: 'Players Online',
|
||||
uniquePlayers: 'Unique Players',
|
||||
|
Loading…
Reference in New Issue
Block a user