UNPKG

aframe-stats-panel

Version:

A-Frame statistics panel for additional statistics, in the style of A-Frame stats

283 lines (223 loc) 8.44 kB
AFRAME.registerComponent('stats-panel', { schema: { merge: {type: 'boolean', default: true} }, init() { const container = document.querySelector('.rs-container') if (container && this.data.merge) { //stats panel exists, just merge into it. this.container = container return; } // if stats panel doesn't exist, add one to support our custom stats. this.base = document.createElement('div') this.base.classList.add('rs-base') const body = document.body || document.getElementsByTagName('body')[0] if (container && !this.data.merge) { this.base.style.top = "auto" this.base.style.bottom = "20px" } body.appendChild(this.base) this.container = document.createElement('div') this.container.classList.add('rs-container') this.base.appendChild(this.container) } }); AFRAME.registerComponent('stats-group', { multiple: true, schema: { label: {type: 'string'} }, init() { let container const baseComponent = this.el.components['stats-panel'] if (baseComponent) { container = baseComponent.container } else { container = document.querySelector('.rs-container') } if (!container) { console.warn(`Couldn't find stats container to add stats to. Add either stats or stats-panel component to a-scene`) return; } this.groupHeader = document.createElement('h1') this.groupHeader.innerHTML = this.data.label container.appendChild(this.groupHeader) this.group = document.createElement('div') this.group.classList.add('rs-group') // rs-group hs style flex-direction of 'column-reverse' // No idea why it's like that, but it's not what we want for our stats. // We prefer them rendered in the order speified. // So override this style. this.group.style.flexDirection = 'column' this.group.style.webKitFlexDirection = 'column' container.appendChild(this.group) } }); AFRAME.registerComponent('stats-row', { multiple: true, schema: { // name of the group to add the stats row to. group: {type: 'string'}, // name of an event to listen for event: {type: 'string'}, // property from event to output in stats panel properties: {type: 'array'}, // label for the row in the stats panel label: {type: 'string'} }, init () { const groupComponentName = "stats-group__" + this.data.group const groupComponent = this.el.components[groupComponentName] || this.el.sceneEl.components[groupComponentName] || this.el.components["stats-group"] || this.el.sceneEl.components["stats-group"] if (!groupComponent) { console.warn(`Couldn't find stats group ${groupComponentName}`) return; } this.counter = document.createElement('div') this.counter.classList.add('rs-counter-base') groupComponent.group.appendChild(this.counter) this.counterId = document.createElement('div') this.counterId.classList.add('rs-counter-id') this.counterId.innerHTML = this.data.label this.counter.appendChild(this.counterId) this.counterValues = {} this.data.properties.forEach((property) => { const counterValue = document.createElement('div') counterValue.classList.add('rs-counter-value') counterValue.innerHTML = "..." this.counter.appendChild(counterValue) this.counterValues[property] = counterValue }) this.updateStatsData = this.updateStatsData.bind(this) this.el.addEventListener(this.data.event, this.updateStatsData) this.splitCache = {} }, updateStatsData(e) { if (!this.data.properties) return this.data.properties.forEach((property) => { const split = this.splitDot(property); let value = e.detail; for (i = 0; i < split.length; i++) { value = value[split[i]]; } this.counterValues[property].innerHTML = value }) }, splitDot (path) { if (path in this.splitCache) { return this.splitCache[path]; } this.splitCache[path] = path.split('.'); return this.splitCache[path]; } }); AFRAME.registerComponent('stats-collector', { multiple: true, schema: { // name of an event to listen for inEvent: {type: 'string'}, // property from event to output in stats panel properties: {type: 'array'}, // frequency of output in terms of events received. outputFrequency: {type: 'number', default: 100}, // name of event to emit outEvent: {type: 'string'}, // outputs (generated for each property) // Combination of: mean, max, percentile__XX.X (where XX.X is a number) outputs: {type: 'array'}, // Whether to output to console as well as generating events // If a string is specified, this is output to console, together with the event data // If no string is specified, nothing is output to console. outputToConsole: {type: 'string'} }, init() { this.statsData = {} this.resetData() this.outputDetail = {} this.data.properties.forEach((property) => { this.outputDetail[property] = {} }) this.statsReceived = this.statsReceived.bind(this) this.el.addEventListener(this.data.inEvent, this.statsReceived) }, resetData() { this.counter = 0 this.data.properties.forEach((property) => { // For calculating percentiles like 0.01 and 99.9% we'll want to store // additional data - something like this... // Store off outliers, and discard data. // const min = Math.min(...this.statsData[property]) // this.lowOutliers[property].push(min) // const max = Math.max(...this.statsData[property]) // this.highOutliers[property].push(max) this.statsData[property] = [] }) }, statsReceived(e) { this.updateStatsData(e.detail) this.counter++ if (this.counter === this.data.outputFrequency) { this.outputData() this.resetData() } }, updateStatsData(detail) { this.data.properties.forEach((property) => { let value = detail; value = value[property]; this.statsData[property].push(value) }) }, outputData() { this.data.properties.forEach((property) => { this.data.outputs.forEach((output) => { this.outputDetail[property][output] = this.computeOutput(output, this.statsData[property]) }) }) if (this.data.outEvent) { this.el.emit(this.data.outEvent, this.outputDetail) } if (this.data.outputToConsole) { console.log(this.data.outputToConsole, this.outputDetail) } }, computeOutput(outputInstruction, data) { const outputInstructions = outputInstruction.split("__") const outputType = outputInstructions[0] let output switch (outputType) { case "mean": output = data.reduce((a, b) => a + b, 0) / data.length; break; case "max": output = Math.max(...data) break; case "min": output = Math.min(...data) break; case "percentile": const sorted = data.sort((a, b) => a - b) // decimal percentiles encoded like 99+9 rather than 99.9 due to "." being used as a // separator for nested properties. const percentileString = outputInstructions[1].replace("_", ".") const proportion = +percentileString / 100 // Note that this calculation of the percentile is inaccurate when there is insufficient data // e.g. for 0.1th or 99.9th percentile when only 100 data points. // Greater accuracy would require storing off more data (specifically outliers) and folding these // into the computation. const position = (data.length - 1) * proportion const base = Math.floor(position) const delta = position - base; if (sorted[base + 1] !== undefined) { output = sorted[base] + delta * (sorted[base + 1] - sorted[base]); } else { output = sorted[base]; } break; } return output.toFixed(2) } });