├── .bowerrc ├── .gitignore ├── .gitmodules ├── .nojekyll ├── README.md ├── app.js ├── browser-stats.js ├── component.json ├── components ├── jets │ ├── .bower.json │ ├── .gitignore │ ├── .travis.yml │ ├── LICENSE │ ├── README.md │ ├── bower.json │ ├── jets.js │ ├── jets.min.js │ ├── package.json │ └── test │ │ ├── index.html │ │ └── test.js ├── jquery │ ├── component.json │ ├── composer.json │ ├── jquery.js │ └── jquery.min.js └── underscore │ ├── .gitignore │ ├── .npmignore │ ├── CNAME │ ├── CONTRIBUTING.md │ ├── LICENSE │ ├── README.md │ ├── Rakefile │ ├── component.json │ ├── docs │ ├── docco.css │ ├── favicon.ico │ ├── images │ │ ├── background.png │ │ └── underscore.png │ └── underscore.html │ ├── favicon.ico │ ├── index.html │ ├── index.js │ ├── package.json │ ├── test │ ├── arrays.js │ ├── chaining.js │ ├── collections.js │ ├── functions.js │ ├── index.html │ ├── objects.js │ ├── speed.js │ ├── utility.js │ └── vendor │ │ ├── jquery.js │ │ ├── jslitmus.js │ │ ├── qunit.css │ │ └── qunit.js │ ├── underscore-min.js │ └── underscore.js ├── data.json ├── favicon-32x32.png ├── favicon.ico ├── images ├── darkdenim3.png └── lightpaperfibers.png ├── index.html └── wfa192.png /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "components" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .DS_Store 3 | bower_components/ 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "caniuse"] 2 | path = caniuse 3 | url = https://github.com/Fyrd/caniuse.git 4 | branch = main -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | web-feature-distribution 2 | ============ 3 | 4 | i hacked up https://github.com/PaulKinlan/iwanttouse.com real good. 5 | 6 | ### dev 7 | 8 | run a local server and open index.html 9 | 10 | 11 | #### ~update data~ (not needed anymore) 12 | 13 | I used to serve the data locally, but now i just hotlink the data from unpkg which always keeps it up to date. 14 | 15 | ```sh 16 | git -C caniuse pull origin main 17 | cp caniuse/fulldata-json/data-1.0.json data.json 18 | ``` 19 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /* bling.js */ 2 | window.$ = document.querySelector.bind(document); 3 | window.$$ = document.querySelectorAll.bind(document); 4 | Node.prototype.on = window.on = function (name, fn) { 5 | this.addEventListener(name, fn); 6 | }; 7 | NodeList.prototype.__proto__ = Array.prototype; 8 | NodeList.prototype.on = NodeList.prototype.addEventListener = function (name, fn) { 9 | this.forEach(function (elem, i) { 10 | elem.on(name, fn); 11 | }); 12 | }; 13 | 14 | // honestly i couldn't tell you how much of this code is from me over the past 5 years and how much is kinlan's fault (https://github.com/PaulKinlan/iwanttouse.com) 15 | // but i can tell you that the code isn't elegant 16 | // so, i'm sorry. 17 | 18 | // 19 | // app 20 | // 21 | 22 | var bindFeatureDataList = function (features, required, onItem, sumCurrent) { 23 | return Object.keys(features).map(function (key) { 24 | return { 25 | id: key, 26 | title: BrowserStats.browsers.getFeature(key).title, 27 | }; 28 | }); 29 | }; 30 | 31 | 32 | /** 33 | * Computes the weighted-average of the score of the list of items. 34 | * stolen from lighthouse 35 | * @param {Array<{score: number|null, weight: number}>} items 36 | * @return {number|null} 37 | */ 38 | function arithmeticMean(items) { 39 | // Filter down to just the items with a weight as they have no effect on score 40 | items = items.filter(item => item.weight > 0); 41 | // If there is 1 null score, return a null average 42 | if (items.some(item => item.score === null)) return null; 43 | 44 | const results = items.reduce( 45 | (result, item) => { 46 | const score = item.score; 47 | const weight = item.weight; 48 | 49 | return { 50 | weight: result.weight + weight, 51 | sum: result.sum + /** @type {number} */ (score) * weight, 52 | }; 53 | }, 54 | {weight: 0, sum: 0} 55 | ); 56 | 57 | return results.sum / results.weight || 0; 58 | } 59 | 60 | 61 | document.on('DOMContentLoaded', function () { 62 | var deviceType = 'all'; 63 | 64 | BrowserStats.load(deviceType, function (browsers) { 65 | var features = browsers.features; 66 | 67 | updateDates(browsers); 68 | 69 | // Sum total usage per agent, cuz it comes broken down by version 70 | const agents = browsers.origCaniuseData.agents; 71 | 72 | Object.values(browsers.origCaniuseData.agents).forEach(agent => { 73 | agent.lite = Object.values(browsers.liteAgents).find(lA => lA.E === agent.browser); 74 | const thing = Object.entries(agent.lite.F).map(([key, date]) => { 75 | const versStr = browsers.liteBrowserVersions[key]; 76 | return [versStr, new Date(date * 1000)]; 77 | }); 78 | agent.versionDates = Object.fromEntries(thing); 79 | }); 80 | 81 | Object.values(agents).forEach(agent => { 82 | // just a sum. 83 | const totalUsageForAgent = Object.values(agent.usage_global).reduce((prev, curr) => { 84 | return prev + curr; 85 | }, 0) 86 | if (!isFinite(totalUsageForAgent)) { 87 | console.warn('invalid totalUsageForAgent', totalUsageForAgent, agent); 88 | } 89 | agent.usage_global_total = totalUsageForAgent; 90 | }); 91 | 92 | _.each(features, function (itm, idx) { 93 | itm.id = idx; 94 | }); 95 | 96 | 97 | /** 98 | * Most recent versions first 99 | * TODO: computationally this is very innefficient. 100 | */ 101 | function sortBrowserVersions(agentStats, agentName) { 102 | // this LUT provides a canonical order for all version ids, incl safari tech preview.. 103 | const agentVersions = agents[agentName].versions; 104 | 105 | return Object.entries(agentStats).sort(([versAStr, suppA], [versBStr, suppB]) => { 106 | const aIndex = agentVersions.findIndex(v => v === versAStr); 107 | const bIndex = agentVersions.findIndex(v => v === versBStr); 108 | return bIndex - aIndex; 109 | }); 110 | } 111 | 112 | function getNewlySupportedRecency(feat) { 113 | const allBrowserStats = Object.entries(feat.stats); 114 | const recencyPerAgent = allBrowserStats.map(([agentName, agentStats]) => { 115 | // If global share is under 1% round down to zero. this is cuz fyrd has a shortcut for old browsers (like android webview) 116 | // where he just marks latest version as supporting and theres no real history behind it. 117 | // this is kinda unfair but improves data quality. 118 | const weightViaBrowserShare = agents[agentName].usage_global_total < 1 ? 0 : agents[agentName].usage_global_total 119 | 120 | const sorted = sortBrowserVersions(agentStats, agentName); 121 | const allAreSupported = sorted.every(([vers, res]) => res.startsWith('y')); 122 | if (allAreSupported) { 123 | // It's always been supported. uninteresting. eg. 'ttf' 124 | return { 125 | agentName, 126 | weight: weightViaBrowserShare, 127 | score: 0, 128 | }; 129 | } 130 | let isPartialBump = false; 131 | 132 | const newlySupportedVersions = sorted.filter(([vers, res], i) => { 133 | const nextOlderVers = sorted[i + 1]; 134 | if (!nextOlderVers) return false; 135 | const nextOlderVerSupp = nextOlderVers[1]; 136 | if (nextOlderVerSupp.startsWith('n') && res.startsWith('y')) return true; 137 | const partialBump = nextOlderVerSupp.startsWith('a') && res.startsWith('y') || 138 | nextOlderVerSupp.startsWith('n') && res.startsWith('a'); 139 | if (partialBump && isPartialBump === false) { 140 | isPartialBump = true; 141 | } 142 | return isPartialBump; 143 | }); 144 | if (newlySupportedVersions.length > 1) { 145 | // Theres only 7 instances in the full dataset where this happens. 146 | // This == they added support. then removed it. then added it back. 147 | // They all are pretty old and uninteresting (shared web workers in safari, some opera stuff.. etc) 148 | // So ill simplify and just take the most recent. 149 | } 150 | const mostRecentVersionThatSupports = newlySupportedVersions.at(0)?.at(0); 151 | 152 | // If it's unsupported then then we dont include in the weighted avg 153 | if (!mostRecentVersionThatSupports) { 154 | return { 155 | agentName, 156 | weight: 0, 157 | score: 0, 158 | } 159 | } 160 | 161 | const mostRecentVersionThatSupportsRls = agents[agentName].versionDates[mostRecentVersionThatSupports]; 162 | 163 | const tenYearsAgo = new Date(new Date() - 1000 * 60 * 60 * 24 * 365 * 10); 164 | let pctRecentInLastTenYears = (mostRecentVersionThatSupportsRls - tenYearsAgo) / (new Date() - tenYearsAgo); 165 | // Dumb but w/e. w/o it text-indent is at the top cuz some recent safari fixed a minor bug. 166 | if (isPartialBump && pctRecentInLastTenYears > 0) { 167 | pctRecentInLastTenYears /= 2; 168 | } 169 | const recencyPct = pctRecentInLastTenYears; 170 | return { 171 | agentName, 172 | score: recencyPct, 173 | weight: weightViaBrowserShare, 174 | mostRecentVersionThatSupports, 175 | mostRecentVersionThatSupportsRls, 176 | } 177 | }); 178 | const maxScore = Math.max(... recencyPerAgent.map(a => a.weight ? a.score : 0)); 179 | const mostRecent = recencyPerAgent.find(a => a.score === maxScore); 180 | const mean = arithmeticMean(recencyPerAgent); 181 | return {mean, mostRecent}; 182 | } 183 | 184 | function updateShare(requiredFeatures) { 185 | if (!!requiredFeatures === false) return; 186 | return bindFeatureDataList(features, requiredFeatures, undefined, 100); 187 | } 188 | 189 | var urlFeats = getFeatureArrayFromString(''); 190 | var shareResults = updateShare(urlFeats); 191 | shareResults = _.groupBy(shareResults, function (f) { 192 | return f.id; 193 | }); 194 | 195 | // merge a few categories together 196 | browsers.featureCats.CSS = [...browsers.featureCats.CSS, ...browsers.featureCats.CSS3]; 197 | delete browsers.featureCats.CSS3; 198 | browsers.featureCats.Other = [...browsers.featureCats.Other, ...browsers.featureCats.PNG]; 199 | delete browsers.featureCats.PNG; 200 | 201 | let totalFeatures = 0; 202 | const hasParamAll = new URL(location.href).searchParams.has('all'); 203 | const hasParam99 = new URL(location.href).searchParams.has('99'); 204 | 205 | var categories = Object.keys(browsers.featureCats).sort(); 206 | 207 | const featureToCat = new Map(); 208 | Object.entries(browsers.featureCats).forEach(([catId, feats]) => { 209 | for (const feat of feats) featureToCat.set(feat, catId); 210 | }); 211 | const allFeatures = Array.from(featureToCat.keys()); 212 | 213 | 214 | var allHTML = allFeatures.map(function (feat) { 215 | feat.share = shareResults[feat.id][0]; 216 | feat.totalSupport = feat.usage_perc_a + feat.usage_perc_y; 217 | const {mean, mostRecent} = getNewlySupportedRecency(feat); 218 | feat.avgRecency = mean; 219 | feat.mostRecent = mostRecent; 220 | 221 | return feat; 222 | }) 223 | .filter(feat => { 224 | // Previously i filtered for features with a chrome_id.. (as a proxy for recent stuff?) but it misses out on some stuff... 225 | // now we filter for obviously supported stuff (but allowlist 'JS' stuff because _developit said so.) 226 | if (hasParamAll) return true; 227 | 228 | return feat.avgRecency > 0.01; // manually picked this threshold 229 | }) 230 | .sort(function (a, b) { 231 | return b.avgRecency - a.avgRecency; 232 | // return b.totalSupport - a.totalSupport; 233 | }) 234 | .map(function (feat) { 235 | var adjustedHue = adjustHue(feat.totalSupport); 236 | var color = `hsla(${adjustedHue}, 100%, 42%, 1)`; 237 | 238 | var partialColor = `hsla(${adjustedHue}, 90%, 39.6%, 1)`; 239 | const fullSupportPct = feat.usage_perc_y; 240 | const partialSupportPct = feat.usage_perc_a; 241 | 242 | var pct = `${escape( 243 | feat.totalSupport.toLocaleString(undefined, {maximumFractionDigits: 1}) 244 | )}%`; 245 | var title = feat.title 246 | .replace(`CSS3 `, ``) 247 | .replace(`CSS `, ``) 248 | .replace(`(rounded corners)`, ``); 249 | 250 | totalFeatures++; 251 | allfeats.push(feat); 252 | 253 | // const feats = browsers.featureCats[cat]; 254 | 255 | // const renamedCat = {'JS API': 'Browser APIs'}[cat] || cat; 256 | // const titleHTML = `

${renamedCat}