From efbc078aea73966d242591889c0c68d1eb6f329e Mon Sep 17 00:00:00 2001 From: HardiReady Date: Sun, 29 Oct 2017 17:36:55 +0100 Subject: [PATCH] Finish basic fraction war stats page --- README.md | 10 +- api/models/logs/budget.js | 2 +- api/models/logs/flag.js | 2 +- api/models/logs/kill.js | 2 +- api/models/logs/points.js | 2 +- api/models/logs/respawn.js | 2 +- api/models/logs/revive.js | 2 +- api/models/logs/transport.js | 2 +- api/routes/logs.js | 44 ++- api/tools/log-parse-tool.js | 34 ++- static/package.json | 3 +- static/src/app/services/logs/logs.service.ts | 5 + .../war-detail/war-detail.component.css | 2 +- .../war-detail/war-detail.component.html | 115 +++++++- .../war-detail/war-detail.component.ts | 272 ++++++++++++++---- static/src/assets/fraction-btn.png | Bin 1412 -> 1710 bytes 16 files changed, 415 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 9ae3311..c9778a0 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,14 @@ _MEAN Application_ ## Installation -## Development +### Setup mongoDB + +### Setup node and npm + +## Development and Execution + +### First run in dev mode + +### Run in Prodction ## License Information diff --git a/api/models/logs/budget.js b/api/models/logs/budget.js index 7bcac48..46f93b1 100644 --- a/api/models/logs/budget.js +++ b/api/models/logs/budget.js @@ -10,7 +10,7 @@ const LogBudgetSchema = new Schema({ required: true }, time: { - type: Number, + type: Date, required: true }, fraction: { diff --git a/api/models/logs/flag.js b/api/models/logs/flag.js index 0243ac1..13a0eea 100644 --- a/api/models/logs/flag.js +++ b/api/models/logs/flag.js @@ -10,7 +10,7 @@ const LogFlagSchema = new Schema({ required: true }, time: { - type: Number, + type: Date, required: true }, player: { diff --git a/api/models/logs/kill.js b/api/models/logs/kill.js index 944c6cf..b48761e 100644 --- a/api/models/logs/kill.js +++ b/api/models/logs/kill.js @@ -10,7 +10,7 @@ const LogKillSchema = new Schema({ required: true }, time: { - type: Number, + type: Date, required: true }, shooter: { diff --git a/api/models/logs/points.js b/api/models/logs/points.js index 39a82ef..28aa39b 100644 --- a/api/models/logs/points.js +++ b/api/models/logs/points.js @@ -10,7 +10,7 @@ const LogKillSchema = new Schema({ required: true }, time: { - type: Number, + type: Date, required: true }, ptBlufor: { diff --git a/api/models/logs/respawn.js b/api/models/logs/respawn.js index c81d062..a79486a 100644 --- a/api/models/logs/respawn.js +++ b/api/models/logs/respawn.js @@ -10,7 +10,7 @@ const LogRespawnSchema = new Schema({ required: true }, time: { - type: Number, + type: Date, required: true }, player: { diff --git a/api/models/logs/revive.js b/api/models/logs/revive.js index 9af4a82..c5c9b10 100644 --- a/api/models/logs/revive.js +++ b/api/models/logs/revive.js @@ -10,7 +10,7 @@ const LogReviveSchema = new Schema({ required: true }, time: { - type: Number, + type: Date, required: true }, medic: { diff --git a/api/models/logs/transport.js b/api/models/logs/transport.js index 92e75e0..09accf0 100644 --- a/api/models/logs/transport.js +++ b/api/models/logs/transport.js @@ -10,7 +10,7 @@ const LogTransportSchema = new Schema({ required: true }, time: { - type: Number, + type: Date, required: true }, driver: { diff --git a/api/routes/logs.js b/api/routes/logs.js index 52dcd1e..4891d1b 100644 --- a/api/routes/logs.js +++ b/api/routes/logs.js @@ -2,6 +2,7 @@ // modules const express = require('express'); +const async = require('async'); const logger = require('debug')('cc:logs'); const routerHandling = require('../middleware/router-handling'); @@ -26,18 +27,47 @@ function processLogRequest(model, filter, res, next) { err.status = require('./http-codes').notfound; return next(err) } - const updatedTimeItems = []; - for (let i =0; i { + const filter = {war: req.params.warId}; + const sort = {sort: {time: 1}}; + + const pointsObjects = LogPointsModel.find(filter, {}, sort); + const budgetObjects = LogBudgetModel.find(filter, {}, sort); + const respawnObjects = LogRespawnModel.find(filter, {}, sort); + const reviveObjects = LogReviveModel.find(filter, {}, sort); + const killObjects = LogKillModel.find(filter, {}, sort); + const transportObjects = LogTransportModel.find(filter, {}, sort); + const flagObjects = LogFlagModel.find(filter, {}, sort); + const resources = { + points: pointsObjects.exec.bind(pointsObjects), + budget: budgetObjects.exec.bind(budgetObjects), + respawn: respawnObjects.exec.bind(respawnObjects), + revive: reviveObjects.exec.bind(reviveObjects), + kill: killObjects.exec.bind(killObjects), + transport: transportObjects.exec.bind(transportObjects), + flag: flagObjects.exec.bind(flagObjects) + }; + + async.parallel(resources, function (error, results){ + if (error) { + res.status(500).send(error); + return; + } + res.locals.items = results; + next(); + }); + }) + .all( + routerHandling.httpMethodNotAllowed + ); + logsRouter.route('/:warId/budget') .get((req, res, next) => { const filter = {war: req.params.warId}; diff --git a/api/tools/log-parse-tool.js b/api/tools/log-parse-tool.js index a619043..e363b90 100644 --- a/api/tools/log-parse-tool.js +++ b/api/tools/log-parse-tool.js @@ -1,6 +1,5 @@ 'use strict'; -const timeStringToDecimal = require('../tools/util').timeStringToDecimal; const playerArrayContains = require('./util').playerArrayContains; const parseWarLog = (lineArray, war) => { @@ -47,7 +46,7 @@ const parseWarLog = (lineArray, war) => { stats.kills.push({ war: war._id, - time: timeStringToDecimal(line.split(' ')[5]), + time: getFullTimeDate(war.date, line.split(' ')[5]), shooter: shooter ? shooter.name : null, target: target.name, friendlyFire: shooter ? target.fraction === shooter.fraction : false, @@ -71,7 +70,7 @@ const parseWarLog = (lineArray, war) => { stats.war['budgetBlufor'] = transformMoneyString(budg[11]); stats.war['budgetOpfor'] = transformMoneyString(budg[14]); } else { - stats.budget.push(getBudgetEntry(budg, war._id)); + stats.budget.push(getBudgetEntry(budg, war._id, war.date)); } } @@ -86,7 +85,7 @@ const parseWarLog = (lineArray, war) => { stats.flag.push({ war: war._id, - time: timeStringToDecimal(line.split(' ')[5]), + time: getFullTimeDate(war.date, line.split(' ')[5]), player: playerName, flagFraction: flagFraction, capture: capture @@ -105,7 +104,7 @@ const parseWarLog = (lineArray, war) => { stats.war['ptOpfor'] = parseInt(pt[14].slice(0, -1)); return true; } else { - stats.points.push(getPointsEntry(pt, line, war._id)) + stats.points.push(getPointsEntry(pt, line, war._id, war.date)) } } @@ -116,7 +115,7 @@ const parseWarLog = (lineArray, war) => { stats.clean.push(line); const resp = line.split(' '); const playerName = line.substring(line.lastIndexOf('Spieler:') + 9, line.lastIndexOf('-') - 1); - stats.respawn.push(getRespawnEntry(resp, playerName, war._id)); + stats.respawn.push(getRespawnEntry(resp, playerName, war._id, war.date)); } /** @@ -133,7 +132,7 @@ const parseWarLog = (lineArray, war) => { stats.revive.push({ war: war._id, - time: timeStringToDecimal(line.split(' ')[5]), + time: getFullTimeDate(war.date, line.split(' ')[5]), stabilized: stabilized, medic: medic.name, patient: patient.name, @@ -156,7 +155,7 @@ const parseWarLog = (lineArray, war) => { stats.transport.push({ war: war._id, - time: timeStringToDecimal(line.split(' ')[5]), + time: getFullTimeDate(war.date, line.split(' ')[5]), driver: driver.name, passenger: passenger ? passenger.name : null, fraction: driver.fraction, @@ -185,28 +184,28 @@ const parseWarLog = (lineArray, war) => { return stats; }; -const getRespawnEntry = (respawn, playerName, warId) => { +const getRespawnEntry = (respawn, playerName, warId, warDate) => { return { war: warId, - time: timeStringToDecimal(respawn[5]), + time: getFullTimeDate(warDate, respawn[5]), player: playerName } }; -const getPointsEntry = (pt, line, warId) => { +const getPointsEntry = (pt, line, warId, warDate) => { return { war: warId, - time: timeStringToDecimal(pt[5]), + time: getFullTimeDate(warDate, pt[5]), ptBlufor: parseInt(pt[12]), ptOpfor: parseInt(pt[15].slice(0, -1)), fraction: line.includes('no Domination') ? 'NONE' : line.includes('NATO +1') ? 'BLUFOR' : 'OPFOR' } }; -const getBudgetEntry = (budg, warId) => { +const getBudgetEntry = (budg, warId, warDate) => { return { war: warId, - time: timeStringToDecimal(budg[5]), + time: getFullTimeDate(warDate, budg[5]), fraction: budg[9] === 'NATO' ? 'BLUFOR' : 'OPFOR', oldBudget: transformMoneyString(budg[11]), newBudget: transformMoneyString(budg[14]) @@ -229,7 +228,14 @@ const transformMoneyString = (budgetString) => { } const budget = budgetString.split('e+'); return Math.round(parseFloat(budget[0]) * Math.pow(10, parseInt(budget[1]))); +}; +const getFullTimeDate = (date, timeString) => { + const returnDate = new Date(date); + const time = timeString.split(':'); + returnDate.setHours(time[0]); + returnDate.setMinutes(time[1]); + return returnDate; }; module.exports = parseWarLog; diff --git a/static/package.json b/static/package.json index c1c7093..61e2a2d 100644 --- a/static/package.json +++ b/static/package.json @@ -23,7 +23,7 @@ "@angular/platform-browser": "^4.4.4", "@angular/platform-browser-dynamic": "^4.4.4", "@angular/router": "^4.4.4", - "@swimlane/ngx-charts": "^6.0.2", + "@swimlane/ngx-charts": "^6.1.0", "@swimlane/ngx-datatable": "^10.2.3", "bootstrap": "^3.3.7", "core-js": "^2.4.1", @@ -31,6 +31,7 @@ "jquery": "^3.1.0", "jquery-ui": "^1.12.0", "jquery-ui-bundle": "^1.11.4", + "ngx-bootstrap": "^1.9.3", "ngx-clipboard": "^8.1.0", "ngx-cookie-service": "^1.0.9", "ngx-infinite-scroll": "^0.5.2", diff --git a/static/src/app/services/logs/logs.service.ts b/static/src/app/services/logs/logs.service.ts index 90d0cd9..f956b81 100644 --- a/static/src/app/services/logs/logs.service.ts +++ b/static/src/app/services/logs/logs.service.ts @@ -10,6 +10,11 @@ export class LogsService { private config: AppConfig) { } + getFullLog(warId: string) { + return this.http.get(this.config.apiLogsPath + '/' + warId) + .map(res => res.json()) + } + getBudgetLogs(warId: string, fraction = '') { const params = new URLSearchParams(); params.append('fraction', fraction); diff --git a/static/src/app/statistic/war-detail/war-detail.component.css b/static/src/app/statistic/war-detail/war-detail.component.css index 405964f..a249eb6 100644 --- a/static/src/app/statistic/war-detail/war-detail.component.css +++ b/static/src/app/statistic/war-detail/war-detail.component.css @@ -43,7 +43,7 @@ } .chart-container { - width: 80%; + width: 95%; min-width: 500px; height: 400px; padding: 15px; diff --git a/static/src/app/statistic/war-detail/war-detail.component.html b/static/src/app/statistic/war-detail/war-detail.component.html index 2daa191..4d5e279 100644 --- a/static/src/app/statistic/war-detail/war-detail.component.html +++ b/static/src/app/statistic/war-detail/war-detail.component.html @@ -10,11 +10,11 @@ {{war.ptOpfor}} CSAT -
+

Teilnehmer:

- + Scoreboard @@ -101,6 +101,7 @@ [gradient]="gradient" [xAxis]="xAxis" [yAxis]="yAxis" + [curve]="monotoneYCurve" [legend]="legend" [legendTitle]="legendTitle" [showXAxisLabel]="showXAxisLabel" @@ -118,6 +119,7 @@ [gradient]="gradient" [xAxis]="xAxis" [yAxis]="yAxis" + [curve]="monotoneYCurve" [legend]="legend" [legendTitle]="legendTitle" [showXAxisLabel]="showXAxisLabel" @@ -128,6 +130,113 @@ [roundDomains]="roundDomains">
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
diff --git a/static/src/app/statistic/war-detail/war-detail.component.ts b/static/src/app/statistic/war-detail/war-detail.component.ts index 3992d53..10833f8 100644 --- a/static/src/app/statistic/war-detail/war-detail.component.ts +++ b/static/src/app/statistic/war-detail/war-detail.component.ts @@ -3,6 +3,8 @@ import {ActivatedRoute, Router} from "@angular/router"; import {WarService} from "../../services/logs/war.service"; import {War} from "../../models/model-interfaces"; import {LogsService} from "../../services/logs/logs.service"; +import {TabsetComponent} from "ngx-bootstrap"; +import * as d3 from "d3"; @Component({ @@ -12,14 +14,16 @@ import {LogsService} from "../../services/logs/logs.service"; }) export class WarDetailComponent { + @ViewChild('overview') private overviewContainer: ElementRef; + + @ViewChild('staticTabs') staticTabs: TabsetComponent; + war: War = {players: []}; fractionRadioSelect: string; playerChart: any[] = []; - @ViewChild('overview') private overviewContainer: ElementRef; - cellHeight = 40; rows = []; @@ -33,6 +37,21 @@ export class WarDetailComponent { pointData: any[] = []; budgetData: any[] = []; + killData: any[] = []; + friendlyFireData: any[] = []; + transportData: any[] = []; + reviveData: any[] = []; + stabilizedData: any[] = []; + flagData: any[] = []; + + tmpPointData; + tmpBudgetData; + tmpKillData; + tmpFrienlyFireData; + tmpTransportData; + tmpReviveData; + tmpStabilizeData; + tmpFlagCaptureData; colorScheme = { domain: ['#0000FF', '#B22222'] @@ -40,7 +59,15 @@ export class WarDetailComponent { yAxisLabelPoints = 'Punkte'; yAxisLabelBudget = 'Budget'; + yAxisLabelKill = 'Kills'; + yAxisLabelFriendlyFire = 'FriendlyFire'; + yAxisLabelTransport = 'Lufttransport'; + yAxisLabelRevive = 'Revive'; + yAxisLabelStabilize = 'Stabilisiert'; + yAxisLabelFlag = 'Flaggenbesitz'; + monotoneYCurve = d3.curveMonotoneY; + stepCurve = d3.curveStepAfter; gradient = false; yAxis = true; xAxis = true; @@ -51,7 +78,7 @@ export class WarDetailComponent { autoscale = true; timeline = false; roundDomains = true; - fractionInitialized: boolean = false; + fractionChartsInitialized: boolean = false; constructor(private route: ActivatedRoute, private router: Router, @@ -77,7 +104,29 @@ export class WarDetailComponent { "value": war.playersBlufor } ]; - Object.assign(this, [this.playerChart, this.pointData, this.budgetData]); + this.tmpPointData = [ + { + "name": "NATO", + "series": [] + }, + { + "name": "CSAT", + "series": [] + } + ]; + this.tmpBudgetData = JSON.parse(JSON.stringify(this.tmpPointData)); + this.tmpKillData = JSON.parse(JSON.stringify(this.tmpPointData)); + this.tmpFrienlyFireData = JSON.parse(JSON.stringify(this.tmpPointData)); + this.tmpTransportData = JSON.parse(JSON.stringify(this.tmpPointData)); + this.tmpReviveData = JSON.parse(JSON.stringify(this.tmpPointData)); + this.tmpStabilizeData = JSON.parse(JSON.stringify(this.tmpPointData)); + this.tmpFlagCaptureData = JSON.parse(JSON.stringify(this.tmpPointData)); + + Object.assign(this, [this.playerChart, this.pointData, this.budgetData, this.killData, + this.friendlyFireData, this.transportData, this.reviveData, this.stabilizedData, this.flagData]); + + this.fractionChartsInitialized = false; + this.staticTabs.tabs[0].active = true; this.scrollOverviewTop(); }); } @@ -107,71 +156,194 @@ export class WarDetailComponent { } loadFractionData() { - if (!this.fractionInitialized) { - const tmpPointData = [ - { - "name": "NATO", - "series": [] - }, - { - "name": "CSAT", - "series": [] - } - ]; - const tmpBudgetData = JSON.parse(JSON.stringify(tmpPointData)); - const tmpKillData = JSON.parse(JSON.stringify(tmpPointData)); - const tmpFrienlyFireData = JSON.parse(JSON.stringify(tmpPointData)); - const tmpTransportData = JSON.parse(JSON.stringify(tmpPointData)); - const tmpReviveData = JSON.parse(JSON.stringify(tmpPointData)); - const tmpStabilizeData = JSON.parse(JSON.stringify(tmpPointData)); - const tmpFlagCaptureData = JSON.parse(JSON.stringify(tmpPointData)); + if (!this.fractionChartsInitialized) { + const startDateObj = new Date(this.war.date); + startDateObj.setHours(0); + startDateObj.setMinutes(1); - // POINTS - this.logsService.getPointsLogs(this.war._id).subscribe((data) => { - data.forEach(pointEntry => { + this.logsService.getFullLog(this.war._id).subscribe((data) => { + // POINTS + data.points.forEach(pointEntry => { const dateObj = new Date(this.war.date); const time = pointEntry.time.split(':'); dateObj.setHours(time[0]); dateObj.setMinutes(time[1]); - tmpPointData[0].series.push({ - "name": dateObj, + this.tmpPointData[0].series.push({ + "name": new Date(pointEntry.time), "value": pointEntry.ptBlufor }); - tmpPointData[1].series.push({ - "name": dateObj, + this.tmpPointData[1].series.push({ + "name": new Date(pointEntry.time), "value": pointEntry.ptOpfor }); }); - this.pointData = tmpPointData; - }); + this.pointData = this.tmpPointData; - // BUDGET - this.logsService.getBudgetLogs(this.war._id).subscribe((data) => { - const dateObj = new Date(this.war.date); - dateObj.setHours(0); - dateObj.setMinutes(0); - tmpBudgetData[0].series.push({ - "name": dateObj, + // BUDGET + this.tmpBudgetData[0].series.push({ + "name": startDateObj, "value": this.war.budgetBlufor }); - tmpBudgetData[1].series.push({ - "name": dateObj, + this.tmpBudgetData[1].series.push({ + "name": startDateObj, "value": this.war.budgetOpfor }); - data.forEach(budgetEntry => { - const time = budgetEntry.time.split(':'); - const dateObj = new Date(this.war.date); - dateObj.setHours(time[0]); - dateObj.setMinutes(time[1]); - tmpBudgetData[budgetEntry.fraction === 'BLUFOR' ? 0 : 1].series.push({ - "name": dateObj, + data.budget.forEach(budgetEntry => { + this.tmpBudgetData[budgetEntry.fraction === 'BLUFOR' ? 0 : 1].series.push({ + "name": new Date(budgetEntry.time), "value": budgetEntry.newBudget }); }); - this.budgetData = tmpBudgetData; - }); + this.budgetData = this.tmpBudgetData; - this.fractionInitialized = true; + // KILLS + let killCountBlufor = 0; + let killCountOpfor = 0; + let ffKillCountBlufor = 0; + let ffKillCountOpfor = 0; + this.tmpKillData[0].series.push({ + "name": startDateObj, + "value": killCountBlufor + }); + this.tmpKillData[1].series.push({ + "name": startDateObj, + "value": killCountOpfor + }); + this.tmpFrienlyFireData[0].series.push({ + "name": startDateObj, + "value": ffKillCountBlufor + }); + this.tmpFrienlyFireData[1].series.push({ + "name": startDateObj, + "value": ffKillCountOpfor + }); + + data.kill.forEach(killEntry => { + if (killEntry.fraction === 'BLUFOR') { + if (killEntry.friendlyFire === false) { + killCountBlufor++; + } else { + ffKillCountBlufor++; + } + } else { + if (killEntry.friendlyFire === false) { + killCountOpfor++; + } else { + ffKillCountOpfor++; + } + } + this.tmpKillData[killEntry.fraction === 'BLUFOR' ? 0 : 1].series.push({ + "name": new Date(killEntry.time), + "value": killEntry.fraction === 'BLUFOR' ? killCountBlufor : killCountOpfor + }); + this.tmpFrienlyFireData[killEntry.fraction === 'BLUFOR' ? 0 : 1].series.push({ + "name": new Date(killEntry.time), + "value": killEntry.fraction === 'BLUFOR' ? ffKillCountBlufor : ffKillCountOpfor + }); + }); + this.killData = this.tmpKillData; + this.friendlyFireData = this.tmpFrienlyFireData; + + // TRANSPORT + let transportCountBlufor = 0; + let transportCountOpfor = 0; + this.tmpTransportData[0].series.push({ + "name": startDateObj, + "value": transportCountBlufor + }); + this.tmpTransportData[1].series.push({ + "name": startDateObj, + "value": transportCountOpfor + }); + + data.transport.forEach(transportEntry => { + if (transportEntry.fraction === 'BLUFOR') { + transportCountBlufor++; + } else { + transportCountOpfor++; + } + this.tmpTransportData[transportEntry.fraction === 'BLUFOR' ? 0 : 1].series.push({ + "name": new Date(transportEntry.time), + "value": transportEntry.fraction === 'BLUFOR' ? transportCountBlufor : transportCountOpfor + }); + }); + this.transportData = this.tmpTransportData; + + // REVIVE & STABILIZE + let reviveCountBlufor = 0; + let reviveCountOpfor = 0; + let stabilizeCountBlufor = 0; + let stabilizeCountOpfor = 0; + this.tmpReviveData[0].series.push({ + "name": startDateObj, + "value": reviveCountBlufor + }); + this.tmpReviveData[1].series.push({ + "name": startDateObj, + "value": reviveCountOpfor + }); + this.tmpStabilizeData[0].series.push({ + "name": startDateObj, + "value": stabilizeCountBlufor + }); + this.tmpStabilizeData[1].series.push({ + "name": startDateObj, + "value": stabilizeCountOpfor + }); + data.revive.forEach(reviveEntry => { + if (reviveEntry.fraction === 'BLUFOR') { + if (reviveEntry.stabilized === false) { + reviveCountBlufor++; + } else { + reviveCountOpfor++; + } + } else { + if (reviveEntry.stabilized === false) { + stabilizeCountBlufor++; + } else { + stabilizeCountOpfor++; + } + } + this.tmpReviveData[reviveEntry.fraction === 'BLUFOR' ? 0 : 1].series.push({ + "name": new Date(reviveEntry.time), + "value": reviveEntry.fraction === 'BLUFOR' ? reviveCountBlufor : reviveCountOpfor + }); + this.tmpStabilizeData[reviveEntry.fraction === 'BLUFOR' ? 0 : 1].series.push({ + "name": new Date(reviveEntry.time), + "value": reviveEntry.fraction === 'BLUFOR' ? stabilizeCountBlufor : stabilizeCountOpfor + }); + }); + this.reviveData = this.tmpReviveData; + this.stabilizedData = this.tmpStabilizeData; + + + // FLAG + let flagStatusBlufor = 1; + let flagStatusOpfor = 1; + this.tmpFlagCaptureData[0].series.push({ + "name": startDateObj, + "value": flagStatusBlufor + }); + this.tmpFlagCaptureData[1].series.push({ + "name": startDateObj, + "value": flagStatusOpfor + }); + + data.flag.forEach(flagEntry => { + if (flagEntry.flagFraction === 'BLUFOR') { + flagStatusBlufor = flagEntry.capture ? 0 : 1 + } else { + flagStatusOpfor = flagEntry.capture ? 0 : 1; + } + this.tmpFlagCaptureData[flagEntry.flagFraction === 'BLUFOR' ? 0 : 1].series.push({ + "name": new Date(flagEntry.time), + "value": flagEntry.flagFraction === 'BLUFOR' ? flagStatusBlufor : flagStatusOpfor + }); + }); + this.flagData = this.tmpFlagCaptureData; + + this.fractionChartsInitialized = true; + }); } } diff --git a/static/src/assets/fraction-btn.png b/static/src/assets/fraction-btn.png index 9d2c68ede8884c97e72914933d7ffa9a22709295..56771c1683f1176892679a1b39e2e37d52607933 100644 GIT binary patch delta 1688 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