// general helper functions String.prototype.capitalize = function () { return this.charAt(0).toUpperCase() + this.slice(1); }; Array.prototype.average = function() { let sum = this.reduce(function(a, b) {return parseInt(a) + parseInt(b)}); return sum / this.length; }; // translate a decimal value on a scale of 1-5 to an integer on a scale of 0-100, rounded to the nearest 10. function fiveScaleToDecile(input) { return Math.round(((input-1) * 25) / 10) * 10; } // analysis helper functions // algorithm from Handbook of Mathematical Functions (Abramowitz & Stegun), // formula 7.1.26 function normDist(x, mean, standardDeviation) { const a1 = 0.254829592; const a2 = -0.284496736; const a3 = 1.421413741; const a4 = -1.453152027; const a5 = 1.061405429; const p = 0.3275911; let val = (mean - x) / (Math.sqrt(2) * standardDeviation); let sign = (val < 0) ? -1 : 1; var xabs = Math.abs(val); var t = 1.0 / (1.0 + xabs * p); var y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-xabs * xabs); var erf = sign * y; return (1 - erf) / 2; } function clamp(value, max) { return Math.max(Math.min(value, max), 1); } function scalePerf(value) { return 20 * (value - 1); } const indicators = { 'leadtime': { 'label': 'Lead time', 'ticks': [ { v: 0, f: '>6m' }, { v: 20, f: '1-6m' }, { v: 40, f: '1w - 1m' }, { v: 60, f: '1d - 1w' }, { v: 80, f: '<1d' }, { v: 100, f: '<1h' } ] }, 'deployfreq': { 'label': 'Deploy frequency', 'ticks': [ { v: 0, f: '>6m' }, { v: 20, f: '1-6m' }, { v: 40, f: '1w - 1m' }, { v: 60, f: '1d - 1w' }, { v: 80, f: '<1d' }, { v: 100, f: 'on demand' } ] }, 'ttr': { 'label': 'Time to restore', 'ticks': [ { v: 0, f: '>6m' }, { v: 20, f: '1-6m' }, { v: 40, f: '1w - 1m' }, { v: 60, f: '1d - 1w' }, { v: 80, f: '<1d' }, { v: 100, f: '<1h' } ] }, 'chgfail': { 'label': 'Change fail rate', 'ticks': [ { v: 0, f: '76-100%' }, { v: 20, f: '61-75%' }, { v: 40, f: '46-60%' }, { v: 60, f: '31-45%' }, { v: 80, f: '16-30%' }, { v: 100, f: '0-15%' } ] } }; ; constants = { '2022': { 'mean': 3.8187, 'stddev': 1.0642, 'baselines': { 'all': { 'leadtime': 3.24, 'deployfreq': 3.3, 'ttr': 4.21, 'chgfail': 4.53 }, 'education': { 'leadtime': 3.14, 'deployfreq': 3.05, 'ttr': 4.21, 'chgfail': 4.75 }, 'energy': { 'leadtime': 3.24, 'deployfreq': 3.29, 'ttr': 4.19, 'chgfail': 4.65 }, 'finance': { 'leadtime': 3.38, 'deployfreq': 3.49, 'ttr': 4.6, 'chgfail': 4.95 }, 'government': { 'leadtime': 3.22, 'deployfreq': 3.09, 'ttr': 4.26, 'chgfail': 5.23 }, 'healthcare': { 'leadtime': 3.15, 'deployfreq': 3.22, 'ttr': 4.19, 'chgfail': 4.55 }, 'industrials': { 'leadtime': 3, 'deployfreq': 3.1, 'ttr': 4.12, 'chgfail': 4.53 }, 'insurance': { 'leadtime': 3.08, 'deployfreq': 2.79, 'ttr': 3.71, 'chgfail': 3.97 }, 'media': { 'leadtime': 3.18, 'deployfreq': 3.64, 'ttr': 3.82, 'chgfail': 4.56 }, 'nonprofit': { 'leadtime': 3.13, 'deployfreq': 3.23, 'ttr': 3.3, 'chgfail': 4.22 }, 'other': { 'leadtime': 3.46, 'deployfreq': 3.61, 'ttr': 4.53, 'chgfail': 5.08 }, 'retail': { 'leadtime': 3.48, 'deployfreq': 3.62, 'ttr': 4.74, 'chgfail': 4.86 }, 'technology': { 'leadtime': 3.22, 'deployfreq': 3.25, 'ttr': 4.09, 'chgfail': 4.29 }, 'telecoms': { 'leadtime': 2.9, 'deployfreq': 2.94, 'ttr': 4.02, 'chgfail': 4.34 } } }, '2021': { 'mean': 3.9495, 'stddev': 1.0547, 'baselines': { 'all': { 'leadtime': 3.27, 'deployfreq': 3.33, 'ttr': 4.28, 'chgfail': 4.9 }, 'education': { 'leadtime': 2.81, 'deployfreq': 3.08, 'ttr': 3.62, 'chgfail': 4.92 }, 'energy': { 'leadtime': 2.77, 'deployfreq': 2.83, 'ttr': 3.68, 'chgfail': 4.6 }, 'finance': { 'leadtime': 3.15, 'deployfreq': 3.22, 'ttr': 4.25, 'chgfail': 4.92 }, 'government': { 'leadtime': 3.04, 'deployfreq': 3.06, 'ttr': 4.51, 'chgfail': 4.94 }, 'healthcare': { 'leadtime': 3.41, 'deployfreq': 3.5, 'ttr': 4.56, 'chgfail': 5.1 }, 'industrials': { 'leadtime': 2.76, 'deployfreq': 2.9, 'ttr': 3.92, 'chgfail': 4.68 }, 'insurance': { 'leadtime': 3.17, 'deployfreq': 2.98, 'ttr': 4.1, 'chgfail': 4.89 }, 'media': { 'leadtime': 3.88, 'deployfreq': 4.1, 'ttr': 4.7, 'chgfail': 4.85 }, 'nonprofit': { 'leadtime': 3.27, 'deployfreq': 3.52, 'ttr': 4.19, 'chgfail': 4.87 }, 'other': { 'leadtime': 2.94, 'deployfreq': 3.2, 'ttr': 4.24, 'chgfail': 4.95 }, 'retail': { 'leadtime': 3.48, 'deployfreq': 3.67, 'ttr': 4.26, 'chgfail': 4.74 }, 'technology': { 'leadtime': 3.55, 'deployfreq': 3.44, 'ttr': 4.46, 'chgfail': 4.94 }, 'telecoms': { 'leadtime': 3.04, 'deployfreq': 3.21, 'ttr': 4.17, 'chgfail': 5.18 } } } } ; /** * Compute performance data */ // constants are included from `constants.js` via Hugo pipelines // helpers are included from `helpers.js` via Hugo pipelines // user data analysis functions function getUserPerformanceIndicators(urlParams) { let userPerformanceIndicators = {}; Object.keys(indicators).forEach(indicator => { userPerformanceIndicators[indicator] = urlParams.get(indicator); }); return userPerformanceIndicators; } function getProfileAndPercentile(constants, userPerformanceIndicators) { let average = 0; Object.keys(indicators).forEach(indicator => { indicator_value = userPerformanceIndicators[indicator]; let unnormalized = clamp(parseInt(indicator_value), 6); average += unnormalized; }) average = average / Object.keys(indicators).length; let percentile = Math.round(100 * normDist(average, constants.mean, constants.stddev)); // TODO: #42 this doesn't have to be a struct since it's returning a single value return { percentile: percentile } } function decoratePagewithProfileAndPercentage(userProfileAndPercentile) { Array.from(document.getElementsByClassName('profile-title')).forEach(element => { element.innerText = element.innerText.toLowerCase().replace('unknown', userProfileAndPercentile.profile); }) Array.from(document.getElementsByClassName('color-by-profile')).forEach(element => { element.classList.add(userProfileAndPercentile.profile); }) document.getElementById('percentile').innerText = userProfileAndPercentile.percentile; let spectrum_width = 570; let marker_position = userProfileAndPercentile.percentile * spectrum_width / 100 + 15; document.getElementById('your-performance').style.left = marker_position + 'px'; let percentile_rounded = Math.round(userProfileAndPercentile.percentile / 10) * 10; if (percentile_rounded == 0) { percentile_rounded = 10 }; document.getElementById('percentile-banner').classList.add('percentile_' + percentile_rounded); document.getElementById('percentile-banner').innerText = userProfileAndPercentile.percentile; console.log('send `results` event to GA (disabled if `gtag` not found [e.g. when developing locally])') if (typeof gtag !== 'undefined') { gtag('event', 'quick_check_results'); } } function drawComparisonChart(constants, indicator, user_score, industry, show_legend) { let baselines = constants.baselines; industry_baseline = baselines[industry]; user_score = scalePerf(user_score); let dataTable = new google.visualization.DataTable(); dataTable.addColumn('string', 'Aspect of Software Delivery Performance'); dataTable.addColumn('number', 'Your performance'); var pluralIndustry = industry == 'all' ? 'ies' : 'y'; var industryString = `${industry} industr${pluralIndustry} performance`.capitalize() dataTable.addColumn('number', industryString); dataTable.addRows([[indicators[indicator].label, user_score, scalePerf(industry_baseline[indicator])]]); var options = { bars: 'horizontal', bar: { groupWidth: '30%' }, height: show_legend ? 120 : 70, width: screen.width <= 480 ? 350 : '', chartArea: { top: 0, left: 120, right: 20, height: 40 }, enableInteractivity: false, series: { 0: { color: '#0F346F' }, 1: { type: 'line', color: '#afb2b6', lineWidth: 0, pointSize: 12, pointShape: 'diamond' } }, hAxis: { minValue: 1, maxValue: 6, ticks: indicators[indicator]['ticks'] }, legend: { position: show_legend ? 'bottom' : 'off' } }; let target_div = (industry == 'all') ? 'all' : 'industry'; let chart = new google.visualization.BarChart(document.getElementById('perf-' + target_div + '-' + indicator)); // move the "you" icons into place icon_id = 'perf-' + target_div + '-' + indicator + '-marker'; google.visualization.events.addListener( chart, 'ready', function () { var cli = chart.getChartLayoutInterface(); yourXpos = cli.getXLocation(dataTable.getValue(0, 1)); document.getElementById(icon_id).style.left = yourXpos - 12 + 'px'; } ); chart.draw(dataTable, options); } function getRadioValue(name) { var elements = document.getElementsByName(name); for (var i = 0; i < elements.length; i++) { if (elements[i].checked) { return parseInt(elements[i].value); } } } function viewPerformance() { for (let key of Object.keys(capabilityQuestions)) { capability = key.substr(0, key.indexOf('-')); capabilityAnswers[capability].push(getRadioValue(key)); } let average = (array) => array.reduce((a, b) => a + b) / array.length; let queryString = ""; for (var i = 0; i < capabilitiesSelected.length; i++) { capability = capabilitiesSelected[i]; queryString += "&" + capability + "=" + average(capabilityAnswers[capability]).toFixed(2); } let newURL = window.location.search + queryString; window.location.replace(newURL); } function renderCapabilityGraph(capability, data, capability_index, focus, profile) { let element = `cap-graph-${capability_index}`; const urlParams = new URLSearchParams(window.location.search); let dataTable = new google.visualization.DataTable(); let value = parseFloat(urlParams.get(capability)) / 4.0; let mean = data["mean"] / 100; let profileMean = data["profile-mean"] / 100; let backgroundColor = focus ? '#E8F0FE' : 'white'; dataTable.addColumn('string', 'Capability'); dataTable.addColumn('number', 'Your performance'); dataTable.addColumn('number', 'Average (all industries)'); dataTable.addColumn('number', `${profile} performer avg.`.capitalize()); dataTable.addRows([[data["title"], value, mean, profileMean]]); var options = { bars: 'horizontal', bar: { groupWidth: '40%' }, height: 120, chartArea: { top: 0, left: 0, right: 20, height: 40 }, enableInteractivity: false, series: { 0: { color: colors['bar'] }, 1: { type: 'line', color: colors['average'], lineWidth: 0, pointSize: 8, pointShape: 'diamond' }, 2: { type: 'line', color: colors[profile], lineWidth: 0, pointSize: 8, pointShape: 'circle' } }, hAxis: { minValue: 0, maxValue: 1, ticks: [0, .25, .5, .75, 1], format: 'percent' }, vAxis: { textPosition: 'none' }, legend: { position: 'bottom' }, backgroundColor: backgroundColor }; let chart = new google.visualization.BarChart(document.getElementById(element)); // move the "you" icon into place google.visualization.events.addListener( chart, 'ready', function () { var cli = chart.getChartLayoutInterface(); yourXpos = cli.getXLocation(dataTable.getValue(0, 1)); document.getElementById(`capability-marker-${capability_index}`).style.left = yourXpos - 12 + 'px'; } ); chart.draw(dataTable, options); } // compute metrics and render display (function () { // load the constants (research outputs) for this year let thisyear = document.querySelector('.quickcheck_results_container').dataset.resultsYear; let constants_thisyear = constants[thisyear]; // load charting library google.charts.load('current', { packages: ['corechart', 'bar'] }); // TODO: #38 test for presence of all URL Params and fail gracefully if any are missing. const urlParams = new URLSearchParams(window.location.search); // COMPUTE USER SCORES let industry = urlParams.get('industry'); let userPerformanceIndicators = getUserPerformanceIndicators(urlParams); let userProfileAndPercentile = getProfileAndPercentile(constants_thisyear, userPerformanceIndicators); // UPDATE PAGE BASED ON USER PROFILE decoratePagewithProfileAndPercentage(userProfileAndPercentile); // When charting library is loaded, render charts google.charts.setOnLoadCallback(function () { // for each indicator, draw comparison chart Object.keys(indicators).forEach((indicator, index, arr) => { // draw 'all industries' drawComparisonChart( constants_thisyear, indicator, userPerformanceIndicators[indicator], 'all', arr[index + 1] ? false : true // show legend on last graph ); // draw 'your industry' drawComparisonChart( constants_thisyear, indicator, userPerformanceIndicators[indicator], industry, arr[index + 1] ? false : true // show legend on last graph ); }); }) }());