Files
traintracker/src/pages/InsightsPage.vue
2023-03-28 18:51:06 +01:00

408 lines
13 KiB
Vue

<template>
<Navbar />
<!-- Table to be shown on screen widths > 1000px -->
<div class="card-group">
<div class="card">
<h4 style="text-align:center;">Train Insights</h4>
<div v-if="readyToRender" class="piechart"><pieChart :id="stationPie" :trainChart="true"/></div>
<div class="card-body">
<h5 class="card-title">Train Data</h5>
<p class="card-text">Total number of trains: {{ this.insights["totalNumTrains"] }}</p>
<ul>
<li><p class="card-stats">Trains: {{ this.insights["numTrains"] }}</p></li>
<li><p class="card-stats">DARTs: {{ this.insights["numDarts"] }}</p></li>
</ul>
</div>
</div>
<div class="card">
<h4 style="text-align:center;">Station Insights</h4>
<div v-if="readyToRender" class="piechart"><pieChart :id="stationPie" :trainChart="false"/></div>
<div class="card-body">
<h5 class="card-title">Station Data</h5>
<p class="card-text">Total number of stations: {{ this.insights["totalNumStations"] }}</p>
<ul>
<li><p class="card-stats">Trains: {{ this.insights["numTrainStations"] }}</p></li>
<li><p class="card-stats">DARTs: {{ this.insights["numDartStations"] }}</p></li>
</ul>
</div>
</div>
<div class="card">
<h4 style="text-align:center;">Punctuality Insights</h4>
<div v-if="readyToRender"><BarChart id="barChart"/></div>
<div class="card-body">
<h5 class="card-title">Punctuality Data</h5>
<p class="card-text">Number of actively running trains: {{ this.insights["numRunningTrains"] }}</p>
<ul>
<li><p class="card-stats">Percentage late: {{ this.insights["percentageLate"] }}%</p></li>
<li><p class="card-stats">Percentage early or ontime: {{ this.insights["percentageNotLate"] }}%</p></li>
</ul>
</div>
</div>
</div>
<div id="leaderboardTitleDiv"><p>Leaderboard</p></div>
<table>
<div style="left:3px; top:3px;" class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" v-model="showTopEarliestLatest"/>
<label v-if="showTopEarliestLatest" class="form-check-label" for="showTopEarliestLatest">Showing All Trains</label>
<label v-else class="form-check-label" for="showTopEarliestLatest">Showing Top 3 Earliest/Latest Trains</label>
</div>
<thead>
<tr>
<th>Code</th>
<th>Time</th>
<th>Type</th>
<th>Origin</th>
<th>Destination</th>
</tr>
</thead>
<tbody v-if="!showTopEarliestLatest" v-for="train in topEarliestLatest">
<tr>
<td>{{ this.rawData[train.jsonIndex]["TrainCode"][0] }}</td>
<td v-if="train.time > 0"><span style="color: #fc1919;">{{ train.time }} mins late</span></td>
<td v-else><span style="color: rgb(129, 213, 3);">{{ train.time * -1}} mins early</span></td>
<td>{{ this.rawData[train.jsonIndex]["TrainType"][0] }}</td>
<td>{{ this.rawData[train.jsonIndex]["Origin"][0] }}</td>
<td>{{ this.rawData[train.jsonIndex]["Destination"][0] }}</td>
</tr>
</tbody>
<tbody v-else v-for="train in orderedTrains">
<tr>
<td>{{ this.rawData[train.jsonIndex]["TrainCode"][0] }}</td>
<td v-if="train.time > 0"><span style="color: #fc1919;">{{ train.time }} mins late</span></td>
<td v-else><span style="color: rgb(129, 213, 3);">{{ train.time * -1}} mins early</span></td>
<td>{{ this.rawData[train.jsonIndex]["TrainType"][0] }}</td>
<td>{{ this.rawData[train.jsonIndex]["Origin"][0] }}</td>
<td>{{ this.rawData[train.jsonIndex]["Destination"][0] }}</td>
</tr>
</tbody>
</table>
</template>
<script>
import { store } from '../store/store'
import { getFunctions, httpsCallable, connectFunctionsEmulator } from "firebase/functions";
import { createToast } from 'mosha-vue-toastify';
import 'mosha-vue-toastify/dist/style.css'
import Navbar from '../components/Navbar.vue'
import BarChart from '../components/BarChart.vue'
import pieChart from '../components/pieChart.vue'
import app from '../api/firebase';
export default {
name: "InsightsPage",
data() {
const toast = () => {
createToast(this.toastMessage, {
hideProgressBar: true,
timeout: 4000,
toastBackgroundColor: this.toastBackground
})
}
let loader = this.$loading.show({
loader: 'dots',
container: this.$refs.container,
canCancel: false
});
return {
insights: {},
rawData: {},
orderedTrains: [],
topEarliestLatest: [],
showTopEarliestLatest: false,
readyToRender: false,
store,
toastMessage: "",
toastBackground: "",
toast,
loader
}
},
components: {
Navbar,
pieChart,
BarChart
},
created() {
// check if no insight data exists
if (!store.orderedTrains.length > 0) {
let host = window.location.hostname
if (host === '127.0.0.1' || host === 'localhost') {
this.postTrainAndStationData();
}
else {
this.getTrainAndStationData();
}
}
else {
this.insights = store.insights
this.rawData = store.rawData
this.orderedTrains = store.orderedTrains
this.topEarliestLatest = this.orderedTrains.slice(0, 3).concat(this.orderedTrains.slice(-3))
this.loader.hide();
this.readyToRender = true
}
},
methods: {
showToast(message, backgroundColour) {
this.toastMessage = message
this.toastBackground = backgroundColour
this.toast()
},
postTrainAndStationData() {
const functions = getFunctions(app);
let host = window.location.hostname
if (host === '127.0.0.1' || host === 'localhost') {
connectFunctionsEmulator(functions, host, 5001);
}
const postTrainData = httpsCallable(functions, 'postLiveTrainData');
postTrainData().then(() => {
const postStationData = httpsCallable(functions, 'postStationData');
postStationData().then(() => {
this.getTrainAndStationData()
})
})
.catch((error) => {
this.showToast(error.message, "red")
})
},
// condensed version
getTrainAndStationData() {
const functions = getFunctions(app);
let host = window.location.hostname
if (host === '127.0.0.1' || host == 'localhost') {
connectFunctionsEmulator(functions, host, 5001);
}
const getTrainData = httpsCallable(functions, 'getLiveTrainData');
getTrainData().then((response) => {
try {
if (!response.data) throw new Error("Error fetching live train data from the database")
var unorderedTrains = [];
var insights = {
"totalNumTrains": 0,
"numRunningTrains": 0,
"numLateRunningTrains": 0,
"numTrains": 0,
"numDarts": 0,
"totalNumStations": 0,
"numTrainStations": 0,
"numDartStations": 0
};
for (var i=0; i<response.data.length; i++) {
let train = response.data[i];
insights["totalNumTrains"] += 1
if (train["TrainType"][0] == "Train") insights["numTrains"] += 1;
else if (train["TrainType"][0] == "DART") insights["numDarts"] += 1;
train["PublicMessage"][0] = train["PublicMessage"][0].replace(/\\n/g, ". ");
let publicMessage = train["PublicMessage"][0];
// check if the train is running
if (train["TrainStatus"][0] == "R") {
insights["numRunningTrains"] += 1;
let startTimeStr = publicMessage.indexOf("(");
let timeEnd = publicMessage.indexOf(" ", startTimeStr+1);
let num = parseInt(publicMessage.substring(startTimeStr+1, timeEnd))
unorderedTrains.push({"time": num, "jsonIndex": i})
// check if the train is late
if (publicMessage[startTimeStr+1] != "-" && publicMessage[startTimeStr+1] != "0") {
insights["numLateRunningTrains"] += 1;
}
}
}
insights["percentageLate"] = ((insights["numLateRunningTrains"] / insights["numRunningTrains"]) * 100).toFixed(2);
insights["percentageNotLate"] = (100 - insights["percentageLate"]).toFixed(2);
store.setRawData(response.data);
store.setOrderedTrains(unorderedTrains);
const getStationData = httpsCallable(functions, 'getStationData');
getStationData().then((response) => {
if (!response.data) throw new Error("Error fetching station from the database");
for (var i=0; i<response.data.length; i++) {
let station = response.data[i];
insights["totalNumStations"] += 1
if (station["StationType"][0] == "DART") insights["numDartStations"] += 1;
else if (station["StationType"][0] == "Train") insights["numTrainStations"] += 1;
}
store.setInsights(insights);
this.insights = insights
this.rawData = store.rawData
this.orderedTrains = store.orderedTrains
this.topEarliestLatest = this.orderedTrains.slice(0, 3).concat(this.orderedTrains.slice(-3))
this.loader.hide();
this.readyToRender = true
})
}
catch {
this.loader.hide()
this.showToast(error.message, "red")
}
})
.catch((error) => {
this.loader.hide()
this.showToast("Error fetching live data", "red")
})
}
}
}
</script>
<style scoped>
.card-text, .card-stats {
font-size: 17px;
}
.piechart {
display: flex;
justify-content: center;
padding-bottom: 0;
margin-bottom: 0;
height: 53%
}
#barChart {
position:relative;
padding: 10px;
width: 100%;
top: 20px;
height: 40%;
}
th {
padding: 15px;
text-align: center;
font-size: 19px;
}
/* #leaderboardToggle{
position: absolute;
top: 70px;
left: 35%;
} */
#leaderboardTitleDiv p {
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
text-align: center;
font-size: 50px;
padding-top:10px;
}
table {
border-spacing: 1;
border-collapse: collapse;
background: white;
border-radius: 6px;
overflow: hidden;
max-width: 1400px;
width: 100%;
margin: 0 auto;
position: relative;
font-size: 19px;
}
*{
position: relative;
}
td,th {
padding-left: 8px;
}
thead tr {
height: 60px;
background: #ffed86;
font-size: 16px;
}
tbody tr {
height: 48px;
border-bottom: 1px solid #e3f1d5;
}
tbody tr:last-child {
border: 0;
border-bottom: 2px solid #d5e0f1;
}
td, th{
text-align: left;
}
td.l, th.l{
text-align: right;
}
@media screen and (max-width: 820px){
table{
display:block;
}
table tr,td,th, *{
display: block;
}
thead{
display:none;
}
tbody tr {
height: auto;
padding: 8px 0;
}
tbody tr td{
padding-left: 45%;
margin-bottom: 12px;
}
tbody tr td:last-child{
margin-bottom: 0;
}
tbody tr td:before{
position: absolute;
font-weight: 700;
width: 40%;
left: 10px;
top: 0;
}
tbody tr td:nth-child(1):before {
content: "Code";
}
tbody tr td:nth-child(2):before {
content: "Time";
}
tbody tr td:nth-child(3):before {
content: "Type";
}
tbody tr td:nth-child(4):before {
content: "Origin";
}
tbody tr td:nth-child(5):before {
content: "Destination";
}
}
</style>