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 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) + "\"}";
|
||||||
|
@ -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) {
|
||||||
|
@ -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};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()));
|
||||||
|
@ -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) {
|
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 +
|
||||||
|
@ -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() {
|
||||||
|
@ -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]);
|
||||||
|
@ -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',
|
||||||
|
Loading…
Reference in New Issue
Block a user