diff --git a/server.go b/server.go index c74ebd3..b2d418f 100644 --- a/server.go +++ b/server.go @@ -2,8 +2,7 @@ package flightlesssomething import ( "bytes" - "crypto/sha1" - "encoding/hex" + "fmt" "html/template" "io/fs" "net/http" @@ -100,21 +99,11 @@ func Start(c *Config) { return } - // Read file content into a byte slice - content, err := fs.ReadFile(staticFS, "static"+filepath) - if err != nil { - c.Status(http.StatusInternalServerError) - return - } + // Generate ETag based on file modification time + etag := fmt.Sprintf("%x-%x", fileInfo.ModTime().Unix(), fileInfo.Size()) - // Generate ETag based on file content - hash := sha1.New() - hash.Write(content) - etag := hex.EncodeToString(hash.Sum(nil)) - - // Set ETag and Cache-Control headers + // Set ETag header c.Header("ETag", etag) - c.Header("Cache-Control", "public, max-age=3600") // Check if the ETag matches if match := c.GetHeader("If-None-Match"); match == etag { @@ -122,6 +111,13 @@ func Start(c *Config) { return } + // Read file content into a byte slice + content, err := fs.ReadFile(staticFS, "static"+filepath) + if err != nil { + c.Status(http.StatusInternalServerError) + return + } + // Serve the file with ETag and Last-Modified headers http.ServeContent(c.Writer, c.Request, fileInfo.Name(), fileInfo.ModTime(), bytes.NewReader(content)) }) diff --git a/static/js/benchmark.js b/static/js/benchmark.js index 294789f..fa8a701 100644 --- a/static/js/benchmark.js +++ b/static/js/benchmark.js @@ -1,236 +1,71 @@ -Highcharts.setOptions({ - chart: { - animation: false - }, - plotOptions: { - series: { - animation: false - } - } -}); +// Common chart options +const commonChartOptions = { + chart: {backgroundColor: null, style: {color: '#FFFFFF'}, animation: false}, + title: {style: {color: '#FFFFFF', fontSize: '16px'}}, + subtitle: {style: {color: '#FFFFFF', fontSize: '12px'}}, + xAxis: {labels: {style: {color: '#FFFFFF'}}, lineColor: '#FFFFFF', tickColor: '#FFFFFF'}, + yAxis: {labels: {style: {color: '#FFFFFF'}}, gridLineColor: 'rgba(255, 255, 255, 0.1)'}, + tooltip: {backgroundColor: '#1E1E1E', borderColor: '#FFFFFF', style: {color: '#FFFFFF'}}, + legend: {itemStyle: {color: '#FFFFFF'}}, + credits: {enabled: false}, + plotOptions: {series: {animation: false}} +}; +// Highcharts global options +Highcharts.setOptions({chart: {animation: false}, plotOptions: {series: {animation: false}}}); -var colors = Highcharts.getOptions().colors; +const colors = Highcharts.getOptions().colors; function getLineChartOptions(title, description, unit, maxY = null) { return { - chart: { - type: 'line', - backgroundColor: null, // Set background to transparent - style: { - color: '#FFFFFF' - }, - zooming: { - type: 'x' - } - }, - title: { - text: title, - style: { - color: '#FFFFFF', - fontSize: '16px' - } - }, - subtitle: { - text: description, - style: { - color: '#FFFFFF', - fontSize: '12px' - } - }, - xAxis: { - lineColor: '#FFFFFF', - tickColor: '#FFFFFF', - labels: { - enabled: false - } - }, - yAxis: { - title: { - text: null - }, - labels: { - formatter: function() { - return this.value.toFixed(2) + ' ' + unit; - }, - style: { - color: '#FFFFFF' - } - }, - gridLineColor: 'rgba(255, 255, 255, 0.1)', - max: maxY - }, - legend: { - align: 'center', - verticalAlign: 'bottom', - itemStyle: { - color: '#FFFFFF' - } - }, - tooltip: { - shared: false, - pointFormat: '{series.name}: {point.y:.2f} ' + unit + '
', // Include unit in tooltip - backgroundColor: '#1E1E1E', - borderColor: '#FFFFFF', - style: { - color: '#FFFFFF' - } - }, - plotOptions: { - line: { - marker: { - enabled: false, - symbol: 'circle', - lineColor: null, - radius: 1.5, - states: { - hover: { - enabled: true, - } - } - }, - lineWidth: 1, - animation: false - } - }, - credits: { - enabled: false - }, + ...commonChartOptions, + chart: {...commonChartOptions.chart, type: 'line', zooming: {type: 'x'}}, + title: {...commonChartOptions.title, text: title}, + subtitle: {...commonChartOptions.subtitle, text: description}, + xAxis: {...commonChartOptions.xAxis, labels: {enabled: false}}, // Hide X-axis labels + yAxis: {...commonChartOptions.yAxis, max: maxY, labels: {...commonChartOptions.yAxis.labels, formatter: function() {return this.value.toFixed(2) + ' ' + unit;}}}, + tooltip: {...commonChartOptions.tooltip, pointFormat: `{series.name}: {point.y:.2f} ${unit}
`}, + plotOptions: {line: {marker: {enabled: false, symbol: 'circle', radius: 1.5, states: {hover: {enabled: true}}}, lineWidth: 1}}, + legend: {...commonChartOptions.legend, enabled: true}, series: [], - exporting: { - buttons: { - contextButton: { - menuItems: [ - 'viewFullscreen', - 'printChart', - 'separator', - 'downloadPNG', - 'downloadJPEG', - 'downloadPDF', - 'downloadSVG', - 'separator', - 'downloadCSV', - 'downloadXLS' - ] - } - } - } + exporting: {buttons: {contextButton: {menuItems: ['viewFullscreen', 'printChart', 'separator', 'downloadPNG', 'downloadJPEG', 'downloadPDF', 'downloadSVG', 'separator', 'downloadCSV', 'downloadXLS']}}} }; } function getBarChartOptions(title, unit, maxY = null) { return { - chart: { - type: 'bar', - backgroundColor: null, // Set background to transparent - style: { - color: '#FFFFFF' - } - }, - title: { - text: title, - style: { - color: '#FFFFFF', - fontSize: '16px' - } - }, - xAxis: { - categories: [], - title: { - text: null - }, - labels: { - style: { - color: '#FFFFFF' - } - } - }, - yAxis: { - min: 0, - max: maxY, - title: { - text: unit, - align: 'high', - style: { - color: '#FFFFFF' - } - }, - labels: { - overflow: 'justify', - style: { - color: '#FFFFFF' - }, - formatter: function() { - return this.value.toFixed(2) + ' ' + unit; - } - }, - gridLineColor: 'rgba(255, 255, 255, 0.1)' - }, - tooltip: { - valueSuffix: ' ' + unit, - backgroundColor: '#1E1E1E', - borderColor: '#FFFFFF', - style: { - color: '#FFFFFF' - }, - formatter: function() { - return '' + this.point.category + ': ' + this.y.toFixed(2) + ' ' + unit; - } - }, - plotOptions: { - bar: { - dataLabels: { - enabled: true, - style: { - color: '#FFFFFF' - }, - formatter: function() { - return this.y.toFixed(2) + ' ' + unit; - } - } - } - }, - legend: { - enabled: false, // Disable legend - }, - credits: { - enabled: false - }, + ...commonChartOptions, + chart: {...commonChartOptions.chart, type: 'bar'}, + title: {...commonChartOptions.title, text: title}, + xAxis: {...commonChartOptions.xAxis, categories: [], title: {text: null}}, + yAxis: {...commonChartOptions.yAxis, min: 0, max: maxY, title: {text: unit, align: 'high', style: {color: '#FFFFFF'}}, labels: {...commonChartOptions.yAxis.labels, formatter: function() {return this.value.toFixed(2) + ' ' + unit;}}}, + tooltip: {...commonChartOptions.tooltip, valueSuffix: ' ' + unit, formatter: function() {return `${this.point.category}: ${this.y.toFixed(2)} ${unit}`;}}, + plotOptions: {bar: {dataLabels: {enabled: true, style: {color: '#FFFFFF'}, formatter: function() {return this.y.toFixed(2) + ' ' + unit;}}}}, + legend: {enabled: false}, series: [] }; } function createChart(chartId, title, description, unit, dataArrays, maxY = null) { - var options = getLineChartOptions(title, description, unit, maxY); - options.series = dataArrays.map(function(dataArray, index) { - return {name: dataArray.label, data: dataArray.data, color: colors[index % colors.length]}; - }); - + const options = getLineChartOptions(title, description, unit, maxY); + options.series = dataArrays.map((dataArray, index) => ({name: dataArray.label, data: dataArray.data, color: colors[index % colors.length]})); Highcharts.chart(chartId, options); } function createBarChart(chartId, title, unit, categories, data, colors, maxY = null) { - var options = getBarChartOptions(title, unit, maxY); + const options = getBarChartOptions(title, unit, maxY); options.xAxis.categories = categories; - options.series = [{ - name: title, - data: data, - colorByPoint: true, - colors: colors - }]; - + options.series = [{name: title, data: data, colorByPoint: true, colors: colors}]; Highcharts.chart(chartId, options); } function calculateAverage(data) { - const sum = data.reduce((acc, value) => acc + value, 0); - return sum / data.length; + return data.reduce((acc, value) => acc + value, 0) / data.length; } function calculatePercentile(data, percentile) { data.sort((a, b) => a - b); - const index = Math.ceil(percentile / 100 * data.length) - 1; - return data[index]; + return data[Math.ceil(percentile / 100 * data.length) - 1]; } // Create line charts @@ -248,256 +83,87 @@ createChart('ramUsedChart', 'RAM Usage', '', 'GB', ramUsedDataArrays); createChart('swapUsedChart', 'SWAP Usage', '', 'GB', swapUsedDataArrays); // Calculate average CPU and GPU load -var cpuLoadAverages = cpuLoadDataArrays.map(function(dataArray) { - return calculateAverage(dataArray.data); -}); - -var gpuLoadAverages = gpuLoadDataArrays.map(function(dataArray) { - return calculateAverage(dataArray.data); -}); +const cpuLoadAverages = cpuLoadDataArrays.map(dataArray => calculateAverage(dataArray.data)); +const gpuLoadAverages = gpuLoadDataArrays.map(dataArray => calculateAverage(dataArray.data)); // Create bar charts for average CPU and GPU load -createBarChart('cpuLoadSummaryChart', 'Average CPU Load', '%', cpuLoadDataArrays.map(function(dataArray) { return dataArray.label; }), cpuLoadAverages, colors, 100); -createBarChart('gpuLoadSummaryChart', 'Average GPU Load', '%', gpuLoadDataArrays.map(function(dataArray) { return dataArray.label; }), gpuLoadAverages, colors, 100); +createBarChart('cpuLoadSummaryChart', 'Average CPU Load', '%', cpuLoadDataArrays.map(dataArray => dataArray.label), cpuLoadAverages, colors, 100); +createBarChart('gpuLoadSummaryChart', 'Average GPU Load', '%', gpuLoadDataArrays.map(dataArray => dataArray.label), gpuLoadAverages, colors, 100); // Calculate and render min, max, and average FPS -var categories = []; -var minFPSData = []; -var avgFPSData = []; -var maxFPSData = []; - -fpsDataArrays.forEach(function(dataArray) { - var minFPS = calculatePercentile(dataArray.data, 1); - var avgFPS = calculateAverage(dataArray.data); - var maxFPS = calculatePercentile(dataArray.data, 97); +const categories = []; +const minFPSData = []; +const avgFPSData1 = []; +const maxFPSData = []; +fpsDataArrays.forEach(dataArray => { categories.push(dataArray.label); - minFPSData.push(minFPS); - avgFPSData.push(avgFPS); - maxFPSData.push(maxFPS); + minFPSData.push(calculatePercentile(dataArray.data, 1)); + avgFPSData1.push(calculateAverage(dataArray.data)); + maxFPSData.push(calculatePercentile(dataArray.data, 97)); }); Highcharts.chart('minMaxAvgChart', { - chart: { - type: 'bar', - backgroundColor: null - }, - title: { - text: 'Min/Avg/Max FPS', - style: { - color: '#FFFFFF', - fontSize: '16px' - } - }, - subtitle: { - text: 'More is better', - style: { - color: '#FFFFFF' - } - }, - xAxis: { - categories: categories, - title: { - text: null - }, - labels: { - style: { - color: '#FFFFFF' - } - } - }, - yAxis: { - min: 0, - title: { - text: 'FPS', - align: 'high', - style: { - color: '#FFFFFF' - } - }, - labels: { - overflow: 'justify', - style: { - color: '#FFFFFF' - } - }, - gridLineColor: 'rgba(255, 255, 255, 0.1)' - }, - tooltip: { - valueSuffix: ' FPS', - backgroundColor: '#1E1E1E', - borderColor: '#FFFFFF', - style: { - color: '#FFFFFF' - }, - formatter: function() { - return '' + this.series.name + ': ' + this.y.toFixed(2) + ' FPS'; - } - }, - plotOptions: { - bar: { - dataLabels: { - enabled: true, - style: { - color: '#FFFFFF' - }, - formatter: function() { - return this.y.toFixed(2) + ' fps'; - } - } - } - }, - legend: { - reversed: true, - itemStyle: { - color: '#FFFFFF' - } - }, - credits: { - enabled: false - }, - series: [{ - name: '97th', - data: maxFPSData, - color: '#00FF00' - }, { - name: 'AVG', - data: avgFPSData, - color: '#0000FF' - }, { - name: '1%', - data: minFPSData, - color: '#FF0000' - }] + ...commonChartOptions, + chart: {...commonChartOptions.chart, type: 'bar'}, + title: {...commonChartOptions.title, text: 'Min/Avg/Max FPS'}, + subtitle: {...commonChartOptions.subtitle, text: 'More is better'}, + xAxis: {...commonChartOptions.xAxis, categories: categories}, + yAxis: {...commonChartOptions.yAxis, title: {text: 'FPS', align: 'high', style: {color: '#FFFFFF'}}}, + tooltip: {...commonChartOptions.tooltip, valueSuffix: ' FPS', formatter: function() {return `${this.series.name}: ${this.y.toFixed(2)} FPS`;}}, + plotOptions: {bar: {dataLabels: {enabled: true, style: {color: '#FFFFFF'}, formatter: function() {return this.y.toFixed(2) + ' fps';}}}}, + legend: {...commonChartOptions.legend, reversed: true, enabled: true}, + series: [{name: '97th', data: maxFPSData, color: '#00FF00'}, {name: 'AVG', data: avgFPSData1, color: '#0000FF'}, {name: '1%', data: minFPSData, color: '#FF0000'}] }); // Calculate average FPS for each filename -var avgFPSData = fpsDataArrays.map(function(dataArray) { - return calculateAverage(dataArray.data); -}); +const avgFPSData2 = fpsDataArrays.map(dataArray => calculateAverage(dataArray.data)); // Calculate FPS as a percentage of the first element -var firstFPS = avgFPSData[0]; -var percentageFPSData = avgFPSData.map(function(fps) { - return (fps / firstFPS) * 100; -}); +const firstFPS = avgFPSData2[0]; +const percentageFPSData = avgFPSData2.map(fps => (fps / firstFPS) * 100); // Create bar chart for FPS percentage Highcharts.chart('avgChart', { - chart: { - type: 'bar', - backgroundColor: null - }, - title: { - text: 'Average FPS in %', - style: { - color: '#FFFFFF', - fontSize: '16px' - } - }, - xAxis: { - categories: fpsDataArrays.map(function(dataArray) { return dataArray.label; }), - title: { - text: null - }, - labels: { - style: { - color: '#FFFFFF' - } - } - }, - yAxis: { - min: 0, - title: { - text: 'Percentage (%)', - align: 'high', - style: { - color: '#FFFFFF' - } - }, - labels: { - overflow: 'justify', - style: { - color: '#FFFFFF' - } - }, - gridLineColor: 'rgba(255, 255, 255, 0.1)' - }, - tooltip: { - valueSuffix: ' %', - backgroundColor: '#1E1E1E', - borderColor: '#FFFFFF', - style: { - color: '#FFFFFF' - }, - formatter: function() { - return '' + this.point.category + ': ' + this.y.toFixed(2) + ' %'; - } - }, - plotOptions: { - bar: { - dataLabels: { - enabled: true, - style: { - color: '#FFFFFF' - }, - formatter: function() { - return this.y.toFixed(2) + ' %'; - } - } - } - }, - legend: { - enabled: false - }, - credits: { - enabled: false - }, - series: [{ - name: 'FPS Percentage', - data: percentageFPSData, - colorByPoint: true, - colors: colors - }] + ...commonChartOptions, + chart: {...commonChartOptions.chart, type: 'bar'}, + title: {...commonChartOptions.title, text: 'Average FPS in %'}, + xAxis: {...commonChartOptions.xAxis, categories: fpsDataArrays.map(dataArray => dataArray.label)}, + yAxis: {...commonChartOptions.yAxis, title: {text: 'Percentage (%)', align: 'high', style: {color: '#FFFFFF'}}}, + tooltip: {...commonChartOptions.tooltip, valueSuffix: ' %', formatter: function() {return `${this.point.category}: ${this.y.toFixed(2)} %`;}}, + plotOptions: {bar: {dataLabels: {enabled: true, style: {color: '#FFFFFF'}, formatter: function() {return this.y.toFixed(2) + ' %';}}}}, + legend: {enabled: false}, + series: [{name: 'FPS Percentage', data: percentageFPSData, colorByPoint: true, colors: colors}] }); // Function to filter out the top and bottom 3% of FPS values function filterOutliers(data) { data.sort((a, b) => a - b); - var start = Math.floor(data.length * 0.01); // Ignore bottom 1% - var end = Math.ceil(data.length * 0.97); // Ignore top 1% - return data.slice(start, end); + return data.slice(Math.floor(data.length * 0.01), Math.ceil(data.length * 0.97)); } // Function to count occurrences of each FPS value function countFPS(data) { - var counts = {}; - data.forEach(function(fps) { - var roundedFPS = Math.round(fps); + const counts = {}; + data.forEach(fps => { + const roundedFPS = Math.round(fps); counts[roundedFPS] = (counts[roundedFPS] || 0) + 1; }); - var fpsArray = Object.keys(counts).map(function(key) { - return [parseInt(key), counts[key]]; - }).sort(function(a, b) { - return a[0] - b[0]; - }); + let fpsArray = Object.keys(counts).map(key => [parseInt(key), counts[key]]).sort((a, b) => a[0] - b[0]); - // Combine closest FPS values until we have 100 or fewer points while (fpsArray.length > 100) { - var minDiff = Infinity; - var minIndex = -1; + let minDiff = Infinity; + let minIndex = -1; - // Find the pair with the smallest difference - for (var i = 0; i < fpsArray.length - 1; i++) { - var diff = fpsArray[i + 1][0] - fpsArray[i][0]; + for (let i = 0; i < fpsArray.length - 1; i++) { + const diff = fpsArray[i + 1][0] - fpsArray[i][0]; if (diff < minDiff) { minDiff = diff; minIndex = i; } } - // Combine the closest pair fpsArray[minIndex][1] += fpsArray[minIndex + 1][1]; fpsArray[minIndex][0] = (fpsArray[minIndex][0] + fpsArray[minIndex + 1][0]) / 2; fpsArray.splice(minIndex + 1, 1); @@ -507,118 +173,43 @@ function countFPS(data) { } // Calculate counts for each dataset after filtering outliers -var densityData = fpsDataArrays.map(function(dataArray) { - var filteredData = filterOutliers(dataArray.data); - return { - name: dataArray.label, - data: countFPS(filteredData) - }; -}); +const densityData = fpsDataArrays.map(dataArray => ({name: dataArray.label, data: countFPS(filterOutliers(dataArray.data))})); // Create the chart Highcharts.chart('densityChart', { - chart: { - type: 'areaspline', - backgroundColor: null - }, - title: { - text: 'FPS Density', - style: { - color: '#FFFFFF', - fontSize: '16px' - } - }, - xAxis: { - title: { - text: 'FPS', - style: { - color: '#FFFFFF' - } - }, - labels: { - style: { - color: '#FFFFFF' - } - } - }, - yAxis: { - title: { - text: 'Count', - style: { - color: '#FFFFFF' - } - }, - labels: { - style: { - color: '#FFFFFF' - } - }, - gridLineColor: 'rgba(255, 255, 255, 0.1)' - }, - tooltip: { - shared: true, - backgroundColor: '#1E1E1E', - borderColor: '#FFFFFF', - style: { - color: '#FFFFFF' - }, - formatter: function() { - var points = this.points; - var tooltipText = '' + points[0].series.name + ': ' + points[0].y + ' points at ~' + Math.round(points[0].x) + ' FPS'; - return tooltipText; - } - }, - plotOptions: { - areaspline: { - fillOpacity: 0.5, - marker: { - enabled: false - } - } - }, - legend: { - enabled: true, - itemStyle: { - color: '#FFFFFF' - } - }, - credits: { - enabled: false - }, + ...commonChartOptions, + chart: {...commonChartOptions.chart, type: 'areaspline'}, + title: {...commonChartOptions.title, text: 'FPS Density'}, + xAxis: {...commonChartOptions.xAxis, title: {text: 'FPS', style: {color: '#FFFFFF'}}, labels: {style: {color: '#FFFFFF'}}}, // Show X-axis labels in white + yAxis: {...commonChartOptions.yAxis, title: {text: 'Count', style: {color: '#FFFFFF'}}}, + tooltip: {...commonChartOptions.tooltip, shared: true, formatter: function() {return `${this.points[0].series.name}: ${this.points[0].y} points at ~${Math.round(this.points[0].x)} FPS`;}}, + plotOptions: {areaspline: {fillOpacity: 0.5, marker: {enabled: false}}}, + legend: {...commonChartOptions.legend, enabled: true}, series: densityData }); function calculateSpikes(data, threshold) { - if (data.length < 6) { - throw new Error("Data length must be greater than or equal to 6."); - } + if (data.length < 6) throw new Error("Data length must be greater than or equal to 6."); let spikeCount = 0; - // Helper function to calculate the moving average with a minimum of 6 points function movingAverage(arr, index) { - const windowSize = Math.max(6, Math.ceil(arr.length * 0.05)); // 5 % of the data + const windowSize = Math.max(6, Math.ceil(arr.length * 0.05)); const halfWindowSize = Math.floor(windowSize / 2); const start = Math.max(0, index - halfWindowSize); const end = Math.min(arr.length - 1, index + halfWindowSize); const actualWindowSize = end - start + 1; let sum = 0; - for (let i = start; i <= end; i++) { - sum += arr[i]; - } + for (let i = start; i <= end; i++) sum += arr[i]; return sum / actualWindowSize; } for (let i = 0; i < data.length; i++) { const currentPoint = data[i]; const movingAvg = movingAverage(data, i); - const change = Math.abs(currentPoint - movingAvg) / movingAvg * 100; - - if (change > threshold) { - spikeCount++; - } + if (change > threshold) spikeCount++; } return (spikeCount / data.length) * 100; @@ -627,93 +218,19 @@ function calculateSpikes(data, threshold) { function updateSpikesChart(threshold) { document.getElementById('spikeThresholdValue').innerText = threshold + '%'; - var spikePercentages = fpsDataArrays.map(function(dataArray) { - return calculateSpikes(dataArray.data, threshold); - }); + const spikePercentages = fpsDataArrays.map(dataArray => calculateSpikes(dataArray.data, threshold)); Highcharts.chart('spikesChart', { - chart: { - type: 'bar', - backgroundColor: null - }, - title: { - text: 'FPS Spikes', - style: { - color: '#FFFFFF', - fontSize: '16px' - } - }, - subtitle: { - text: 'Less is better', - style: { - color: '#FFFFFF', - fontSize: '12px' - } - }, - xAxis: { - categories: categories, - title: { - text: null - }, - labels: { - style: { - color: '#FFFFFF' - } - } - }, - yAxis: { - min: 0, - title: { - text: 'Percentage (%)', - align: 'high', - style: { - color: '#FFFFFF' - } - }, - labels: { - overflow: 'justify', - style: { - color: '#FFFFFF' - } - }, - gridLineColor: 'rgba(255, 255, 255, 0.1)' - }, - tooltip: { - valueSuffix: ' %', - backgroundColor: '#1E1E1E', - borderColor: '#FFFFFF', - style: { - color: '#FFFFFF' - }, - formatter: function() { - return '' + this.point.category + ': ' + this.y.toFixed(2) + ' %'; - } - }, - plotOptions: { - bar: { - dataLabels: { - enabled: true, - style: { - color: '#FFFFFF' - }, - formatter: function() { - return this.y.toFixed(2) + ' %'; - } - } - } - }, - legend: { - enabled: false - }, - credits: { - enabled: false - }, - series: [{ - name: 'Spike Percentage', - data: spikePercentages, - colorByPoint: true, - colors: colors - }] + ...commonChartOptions, + chart: {...commonChartOptions.chart, type: 'bar'}, + title: {...commonChartOptions.title, text: 'FPS Spikes'}, + subtitle: {...commonChartOptions.subtitle, text: 'Less is better'}, + xAxis: {...commonChartOptions.xAxis, categories: categories}, + yAxis: {...commonChartOptions.yAxis, title: {text: 'Percentage (%)', align: 'high', style: {color: '#FFFFFF'}}}, + tooltip: {...commonChartOptions.tooltip, valueSuffix: ' %', formatter: function() {return `${this.point.category}: ${this.y.toFixed(2)} %`;}}, + plotOptions: {bar: {dataLabels: {enabled: true, style: {color: '#FFFFFF'}, formatter: function() {return this.y.toFixed(2) + ' %';}}}}, + legend: {enabled: false}, + series: [{name: 'Spike Percentage', data: spikePercentages, colorByPoint: true, colors: colors}] }); }