2022-12-12 18:23:22 -07:00
<!doctype html>
2022-10-25 10:09:29 -06:00
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
2022-12-12 18:23:22 -07:00
< meta name = "viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
< meta http-equiv = "X-UA-Compatible" content = "ie=edge" >
2022-12-13 01:08:09 -07:00
< title > Kaj Forney -- GIMM 260 Data Visualization Narrative< / title >
2022-12-12 18:23:22 -07:00
2022-12-18 13:09:27 -07:00
< link rel = "stylesheet" href = "assets/css/main.css" >
2022-12-12 18:23:22 -07:00
< script src = "https://kit.fontawesome.com/55072ce5fd.js" crossorigin = "anonymous" > < / script >
2022-10-25 10:09:29 -06:00
< / head >
2022-12-12 18:23:22 -07:00
< body class = "parallax" >
< div class = "row" >
< div class = "col-3" > < / div >
< div class = "col-6" >
< h1 class = "titlePulse" >
< i class = "fa-solid fa-gamepad" > < / i >
Gaming on Linux
< i class = "fa-brands fa-steam" > < / i >
< / h1 >
< / div >
< div class = "col-4" > < / div >
< / div >
< div class = "row" >
< div class = "col-1" > < / div >
< div class = "col-5 roundCorners card" >
< h2 > Introduction< / h2 >
2022-12-18 13:09:27 -07:00
< p > I have been a user of Linux for the past 14 years. In that time, I've observed a notable improvement in the ease of running games on the system, particularly games that were originally made for Windows.< / p >
< p > This page will display a sample of Linux game compatibility data.< / p >
2022-12-12 18:23:22 -07:00
< / div >
< div class = "col-1" > < / div >
< div class = "col-3 bigIcon" >
< i class = "fa-brands fa-linux wobble" > < / i >
< / div >
< div class = "col-2" > < / div >
< / div >
< div class = "spacer" > < / div >
2022-12-18 13:09:27 -07:00
< div class = "row reverseOnMobile" >
2022-12-12 18:23:22 -07:00
< div class = "col-2" > < / div >
2022-12-18 13:09:27 -07:00
< div class = "col-3 bigIcon lastOnMobile" >
2022-12-12 18:23:22 -07:00
< i class = "fa-solid fa-wine-glass wobble" > < / i >
< / div >
< div class = "col-1" > < / div >
2022-12-18 13:09:27 -07:00
< div class = "col-5 roundCorners card firstOnMobile" >
2022-12-12 18:23:22 -07:00
< h2 > WINE< / h2 >
2022-12-18 13:09:27 -07:00
< p > The WINE Is Not an Emulator (WINE) project is the foundation for any method of running Windows applications on Linux, originally created in 1993 to support Windows 3.1 applications.< / p >
< p > Essentially, it translates Windows system calls into Linux calls, without the performance penalties of full emulation.< / p >
2022-12-12 18:23:22 -07:00
< / div >
< / div >
2022-12-18 13:09:27 -07:00
< div class = "spacer" > < / div >
2022-12-12 18:23:22 -07:00
< div class = "row" >
< div class = "col-1" > < / div >
< div class = "col-5 roundCorners card" >
< h2 > Proton< / h2 >
2022-12-18 13:09:27 -07:00
< p > Proton is a tool built on top of WINE by Valve and CodeWeavers, initially released in 2018 and integrated into Steam.< / p >
< p > The primary difference between Proton and base WINE is that Proton incorporates multiple libraries designed to improve rendering performance.< / p >
< p > It is Proton that makes it possible to run Windows games with relative ease on Valve's Steam Deck console.< / p >
2022-12-12 18:23:22 -07:00
< / div >
< div class = "col-1" > < / div >
< div class = "col-4" >
< div class = "youtube-container" >
< iframe class = "youtube-iframe" src = "https://www.youtube-nocookie.com/embed/_OAqvtlgfGA" title = "YouTube video player" frameborder = "0" allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen > < / iframe >
< / div >
< / div >
< div class = "col-1" > < / div >
< / div >
< div class = "spacer" > < / div >
< div class = "row" >
< div class = "col-1" > < / div >
2022-12-18 13:09:27 -07:00
< div class = "col-4 overflow" >
< script type = "module" id = "overviewIcicle" >
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
let parsedData = {
"name": "Total Reports",
"children": [
{"name": "Working",
"children": [
{"name": "Windowing Faults", "size": 0},
{"name": "Performance Faults", "size": 0},
{"name": "Save Game Faults", "size": 0},
{"name": "Graphical Faults", "size": 0},
{"name": "Audio Faults", "size": 0},
{"name": "Input Faults", "size": 0},
{"name": "Stability Faults", "size": 0},
{"name": "Significant Bugs", "size": 0},
{"name": "No Issues", "size": 0}
]},
{"name": "Nonfunctional", "size": 0}
]
};
const jsonData = d3.json("http://localhost:8888");
//const jsonData = d3.json("gamedata.json");
jsonData.then(function (data){
let dataset = data.reports;
processData(dataset);
});
function processData(data) {
for(let i = 0; i < data.length ; i + + ) {
if(data[i].verdict == 0)
{
parsedData.children[1].size++;
}
else if(data[i].verdict == 1) {
if(data[i].windowingFaults == 1) {
parsedData.children[0].children[0].size++;
continue;
}
if(data[i].performanceFaults == 1) {
parsedData.children[0].children[1].size++;
continue;
}
if(data[i].saveGameFaults == 1) {
parsedData.children[0].children[2].size++;
continue;
}
if(data[i].graphicalFaults == 1) {
parsedData.children[0].children[3].size++;
continue;
}
if(data[i].audioFaults == 1) {
parsedData.children[0].children[4].size++;
continue;
}
if(data[i].inputFaults == 1) {
parsedData.children[0].children[5].size++;
continue;
}
if(data[i].stabilityFaults == 1) {
parsedData.children[0].children[6].size++;
continue;
}
if(data[i].significantBugs == 1) {
parsedData.children[0].children[7].size++;
continue;
}
if((
data[i].windowingFaults == 0 & &
data[i].performanceFaults == 0 & &
data[i].saveGameFaults == 0 & &
data[i].graphicalFaults == 0 & &
data[i].audioFaults == 0 & &
data[i].inputFaults == 0 & &
data[i].stabilityFaults == 0 & &
data[i].significantBugs == 0
) || (data[i].verdictOOB == 1)
) {
parsedData.children[0].children[8].size++;
}
}
};
let chart = Icicle(parsedData, {
value: d => d.size, // size of each node (file); null for internal nodes (folders)
label: d => d.name, // display name for each cell
title: (d, n) => `${n.ancestors().reverse().map(d => d.data.name).join(".")}\n${n.value.toLocaleString("en")}`, // hover text
color: d3.scaleOrdinal(d3.schemeBlues[3])
})
document.getElementById("visOverview").appendChild(chart);
}
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/icicle
function Icicle(data, { // data is either tabular (array of objects) or hierarchy (nested objects)
path, // as an alternative to id and parentId, returns an array identifier, imputing internal nodes
id = Array.isArray(data) ? d => d.id : null, // if tabular data, given a d in data, returns a unique identifier (string)
parentId = Array.isArray(data) ? d => d.parentId : null, // if tabular data, given a node d, returns its parent’ s identifier
children, // if hierarchical data, given a d in data, returns its children
format = ",", // format specifier string or function for values
value, // given a node d, returns a quantitative value (for area encoding; null for count)
sort = (a, b) => d3.descending(a.value, b.value), // how to sort nodes prior to layout
label, // given a node d, returns the name to display on the rectangle
title, // given a node d, returns its hover text
link, // given a node d, its link (if any)
linkTarget = "_blank", // the target attribute for links (if any)
width = 500, // outer width, in pixels
height = 500, // outer height, in pixels
margin = 0, // shorthand for margins
marginTop = margin, // top margin, in pixels
marginRight = margin, // right margin, in pixels
marginBottom = margin, // bottom margin, in pixels
marginLeft = margin, // left margin, in pixels
padding = 1, // cell padding, in pixels
round = false, // whether to round to exact pixels
color = d3.interpolateRainbow, // color scheme, if any
fill = "#ccc", // fill for node rects (if no color encoding)
fillOpacity = 0.6, // fill opacity for node rects
} = {}) {
// If id and parentId options are specified, or the path option, use d3.stratify
// to convert tabular data to a hierarchy; otherwise we assume that the data is
// specified as an object {children} with nested objects (a.k.a. the “flare.json”
// format), and use d3.hierarchy.
const root = path != null ? d3.stratify().path(path)(data)
: id != null || parentId != null ? d3.stratify().id(id).parentId(parentId)(data)
: d3.hierarchy(data, children);
// Compute the values of internal nodes by aggregating from the leaves.
value == null ? root.count() : root.sum(d => Math.max(0, value(d)));
// Compute formats.
if (typeof format !== "function") format = d3.format(format);
// Sort the leaves (typically by descending value for a pleasing layout).
if (sort != null) root.sort(sort);
// Compute the partition layout. Note that x and y are swapped!
d3.partition()
.size([height - marginTop - marginBottom, width - marginLeft - marginRight])
.padding(padding)
.round(round)
(root);
// Construct a color scale.
if (color != null) {
color = d3.scaleSequential([0, root.children.length - 1], color).unknown(fill);
root.children.forEach((child, i) => child.index = i);
}
const svg = d3.create("svg")
.attr("viewBox", [-marginLeft, -marginTop, width, height])
.attr("preserveAspectRatio", "xMinYMin meet")
.classed("svg-content", true)
//.attr("width", width)
//.attr("height", height)
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.attr("font-family", "sans-serif")
.attr("font-size", 10);
const cell = svg
.selectAll("a")
.data(root.descendants())
.join("a")
.attr("xlink:href", link == null ? null : d => link(d.data, d))
.attr("target", link == null ? null : linkTarget)
.attr("transform", d => `translate(${d.y0},${d.x0})`);
cell.append("rect")
.attr("width", d => d.y1 - d.y0)
.attr("height", d => d.x1 - d.x0)
.attr("fill", color ? d => color(d.ancestors().reverse()[1]?.index) : fill)
.attr("fill-opacity", fillOpacity);
const text = cell.filter(d => d.x1 - d.x0 > 10).append("text")
.attr("x", 4)
.attr("y", d => Math.min(9, (d.x1 - d.x0) / 2))
.attr("dy", "0.32em");
if (label != null) text.append("tspan")
.text(d => label(d.data, d));
text.append("tspan")
.attr("fill-opacity", 0.7)
.attr("dx", label == null ? null : 3)
.text(d => format(d.value));
if (title != null) cell.append("title")
.text(d => title(d.data, d));
return svg.node();
}
< / script >
< svg id = "visOverview" height = "500" width = "500" class = "overflow" > < / svg >
2022-12-12 18:23:22 -07:00
< / div >
< div class = "col-1" > < / div >
< div class = "col-5 roundCorners card" >
2022-12-18 13:09:27 -07:00
< h2 > Overview< / h2 >
< p > The chart on the left provides a general overview of the data.< / p >
< p > Of the 3,533 compatibility reports gathered, only 856 reported games tested as being entirely nonfunctional.< / p >
< p > Of the 2,677 reports of working gameplay, 1,510 had no issues whatsoever, with the game running perfectly out of the box.< / p >
2022-12-12 18:23:22 -07:00
< / div >
< div class = "col-1" > < / div >
< / div >
< div class = "spacer" > < / div >
2022-12-18 13:09:27 -07:00
< div class = "row" >
< div class = "col-1" > < / div >
< div class = "col-5 roundCorners card" >
< h2 > Typical Game Compatibility< / h2 >
< p > Both Valve and the community at large make changes to Proton as needed to support new games.< / p >
< p > This data visualization illustrates both the speed at which a newly released game can now become Linux-compatible, and the occasional periods of instability some games go through following major updates, using the Definitive Edition release of Age of Empires II as an example.< / p >
< p > In general, however, single-player games tend to be more stable, as can be seen below with Outer Wilds and Cuphead.< / p >
< p > The percentage of reports in green indicate that the game was playable, while the reports in orange indicate that the game was entirely unplayable.< / p >
< / div >
< div class = "col-1" > < / div >
< div class = "col-4 overflow" >
< script type = "module" id = "aoe2" >
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
const jsonData = d3.json("http://localhost:8888");
//const jsonData = d3.json("gamedata.json");
jsonData.then(function (data){
let dataset = data.reports;
processData(dataset);
});
function processData(data) {
console.log(data);
let parsedAOE2Data = [];
let indexWorking = 0;
let indexBroken = 1;
let prevDate = 14;
parsedAOE2Data[indexWorking] = new Object();
parsedAOE2Data[indexWorking].date = new Date("2019-11-14");
parsedAOE2Data[indexWorking].title = "Working";
parsedAOE2Data[indexWorking].number = 0;
parsedAOE2Data[indexBroken] = new Object();
parsedAOE2Data[indexBroken].date = new Date("2019-11-14");
parsedAOE2Data[indexBroken].title = "Borked";
parsedAOE2Data[indexBroken].number = 0;
for(let i = 0; i < data.length ; i + + ) {
let currentReport = data[i];
let currentDate = new Date(data[i].timestamp * 1000);
let dateString = currentDate.getFullYear().toString() + "-" + currentDate.getMonth().toString() + "-" + currentDate.getDate().toString();
if(data[i].game_title != "Age of Empires II: Definitive Edition")
{continue;} else {
console.log("AOE2!");
if(currentDate.getDate() != prevDate) {
prevDate = currentDate.getDate();
indexBroken = indexBroken + 2;
indexWorking = indexWorking + 2;
parsedAOE2Data[indexWorking] = new Object();
parsedAOE2Data[indexWorking].date = currentDate;
parsedAOE2Data[indexWorking].title = "Working";
parsedAOE2Data[indexWorking].number = 0;
parsedAOE2Data[indexBroken] = new Object();
parsedAOE2Data[indexBroken].date = currentDate;
parsedAOE2Data[indexBroken].title = "Borked";
parsedAOE2Data[indexBroken].number = 0;
}
if(data[i].verdict == 0) {
parsedAOE2Data[indexBroken].number++;
}
if(data[i].verdict == 1) {
parsedAOE2Data[indexWorking].number++;
}
}
}
console.log(parsedAOE2Data);
document.getElementById("visAOE2").appendChild(StackedAreaChart(parsedAOE2Data, {
x: d => d.date,
y: d => d.number,
z: d => d.title,
yLabel: "Percentage of Reports--Age of Empires II: Definitive Edition",
width: 700,
height: 250
}
));
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/normalized-stacked-area-chart
function StackedAreaChart(data, {
x = ([x]) => x, // given d in data, returns the (ordinal) x-value
y = ([, y]) => y, // given d in data, returns the (quantitative) y-value
z = () => 1, // given d in data, returns the (categorical) z-value
marginTop = 20, // top margin, in pixels
marginRight = 30, // right margin, in pixels
marginBottom = 30, // bottom margin, in pixels
marginLeft = 40, // left margin, in pixels
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
xType = d3.scaleUtc, // type of x-scale
xDomain, // [xmin, xmax]
xRange = [marginLeft, width - marginRight], // [left, right]
yType = d3.scaleLinear, // type of y-scale
yDomain, // [ymin, ymax]
yRange = [height - marginBottom, marginTop], // [bottom, top]
zDomain, // array of z-values
offset = d3.stackOffsetExpand, // stack offset method
order = d3.stackOrderNone, // stack order method
yLabel, // a label for the y-axis
xFormat, // a format specifier string for the x-axis
yFormat = "%", // a format specifier string for the y-axis
colors = d3.schemeDark2, // an array of colors for the (z) categories
} = {}) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
const Z = d3.map(data, z);
// Compute default x- and z-domains, and unique the z-domain.
if (xDomain === undefined) xDomain = d3.extent(X);
if (zDomain === undefined) zDomain = Z;
zDomain = new d3.InternSet(zDomain);
// Omit any data not present in the z-domain.
const I = d3.range(X.length).filter(i => zDomain.has(Z[i]));
// Compute a nested array of series where each series is [[y1, y2], [y1, y2],
// [y1, y2], …] representing the y-extent of each stacked rect. In addition,
// each tuple has an i (index) property so that we can refer back to the
// original data point (data[i]). This code assumes that there is only one
// data point for a given unique x- and z-value.
const series = d3.stack()
.keys(zDomain)
.value(([x, I], z) => Y[I.get(z)])
.order(order)
.offset(offset)
(d3.rollup(I, ([i]) => i, i => X[i], i => Z[i]))
.map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));
// Compute the default y-domain. Note: diverging stacks can be negative.
if (yDomain === undefined) yDomain = d3.extent(series.flat(2));
// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);
const color = d3.scaleOrdinal(zDomain, colors);
const xAxis = d3.axisBottom(xScale).ticks(width / 80, xFormat).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 50, yFormat);
const area = d3.area()
.x(({i}) => xScale(X[i]))
.y0(([y1]) => yScale(y1))
.y1(([, y2]) => yScale(y2));
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.append("g")
.selectAll("path")
.data(series)
.join("path")
.attr("fill", ([{i}]) => color(Z[i]))
.attr("d", area)
.append("title")
.text(([{i}]) => Z[i]);
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis)
.call(g => g.select(".domain").remove());
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line")
.filter(d => d === 0 || d === 1)
.clone()
.attr("x2", width - marginLeft - marginRight))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel));
return Object.assign(svg.node(), {scales: {color}});
}
}
< / script >
< h2 > Age of Empires II: Definitive Edition< / h2 >
< svg width = "500" height = "500" id = "visAOE2" class = "overflow" > < / svg >
< / div >
< div class = "col-1" > < / div >
< / div >
< div class = "row" >
< div class = "col-1" > < / div >
< div class = "col-4" >
< script type = "module" id = "OuterWilds" >
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
const jsonData = d3.json("http://localhost:8888");
jsonData.then(function (data){
let dataset = data.reports;
processData(dataset);
});
function processData(data) {
let parsedOuterWildsData = [];
let indexWorking = 0;
let indexBroken = 1;
let prevDate = 14;
parsedOuterWildsData[indexWorking] = new Object();
parsedOuterWildsData[indexWorking].date = new Date("2020-06-18");
parsedOuterWildsData[indexWorking].title = "Working";
parsedOuterWildsData[indexWorking].number = 0;
parsedOuterWildsData[indexBroken] = new Object();
parsedOuterWildsData[indexBroken].date = new Date("2020-06-18");
parsedOuterWildsData[indexBroken].title = "Borked";
parsedOuterWildsData[indexBroken].number = 0;
for(let i = 0; i < data.length ; i + + ) {
let currentReport = data[i];
let currentDate = new Date(data[i].timestamp * 1000);
let dateString = currentDate.getFullYear().toString() + "-" + currentDate.getMonth().toString() + "-" + currentDate.getDate().toString();
if(data[i].game_title != "Outer Wilds")
{continue;} else {
if(currentDate.getDate() != prevDate) {
prevDate = currentDate.getDate();
indexBroken = indexBroken + 2;
indexWorking = indexWorking + 2;
parsedOuterWildsData[indexWorking] = new Object();
parsedOuterWildsData[indexWorking].date = currentDate;
parsedOuterWildsData[indexWorking].title = "Working";
parsedOuterWildsData[indexWorking].number = 0;
parsedOuterWildsData[indexBroken] = new Object();
parsedOuterWildsData[indexBroken].date = currentDate;
parsedOuterWildsData[indexBroken].title = "Borked";
parsedOuterWildsData[indexBroken].number = 0;
}
if(data[i].verdict == 0) {
parsedOuterWildsData[indexBroken].number++;
}
if(data[i].verdict == 1) {
parsedOuterWildsData[indexWorking].number++;
}
}
}
document.getElementById("visOW").appendChild(StackedAreaChart(parsedOuterWildsData, {
x: d => d.date,
y: d => d.number,
z: d => d.title,
yLabel: "Number of Reports--Outer Wilds",
width: 700,
height: 250
}
));
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/normalized-stacked-area-chart
function StackedAreaChart(data, {
x = ([x]) => x, // given d in data, returns the (ordinal) x-value
y = ([, y]) => y, // given d in data, returns the (quantitative) y-value
z = () => 1, // given d in data, returns the (categorical) z-value
marginTop = 20, // top margin, in pixels
marginRight = 30, // right margin, in pixels
marginBottom = 30, // bottom margin, in pixels
marginLeft = 40, // left margin, in pixels
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
xType = d3.scaleUtc, // type of x-scale
xDomain, // [xmin, xmax]
xRange = [marginLeft, width - marginRight], // [left, right]
yType = d3.scaleLinear, // type of y-scale
yDomain, // [ymin, ymax]
yRange = [height - marginBottom, marginTop], // [bottom, top]
zDomain, // array of z-values
offset = d3.stackOffsetExpand, // stack offset method
order = d3.stackOrderNone, // stack order method
yLabel, // a label for the y-axis
xFormat, // a format specifier string for the x-axis
yFormat = "%", // a format specifier string for the y-axis
colors = d3.schemeDark2, // an array of colors for the (z) categories
} = {}) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
const Z = d3.map(data, z);
// Compute default x- and z-domains, and unique the z-domain.
if (xDomain === undefined) xDomain = d3.extent(X);
if (zDomain === undefined) zDomain = Z;
zDomain = new d3.InternSet(zDomain);
// Omit any data not present in the z-domain.
const I = d3.range(X.length).filter(i => zDomain.has(Z[i]));
// Compute a nested array of series where each series is [[y1, y2], [y1, y2],
// [y1, y2], …] representing the y-extent of each stacked rect. In addition,
// each tuple has an i (index) property so that we can refer back to the
// original data point (data[i]). This code assumes that there is only one
// data point for a given unique x- and z-value.
const series = d3.stack()
.keys(zDomain)
.value(([x, I], z) => Y[I.get(z)])
.order(order)
.offset(offset)
(d3.rollup(I, ([i]) => i, i => X[i], i => Z[i]))
.map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));
// Compute the default y-domain. Note: diverging stacks can be negative.
if (yDomain === undefined) yDomain = d3.extent(series.flat(2));
// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);
const color = d3.scaleOrdinal(zDomain, colors);
const xAxis = d3.axisBottom(xScale).ticks(width / 80, xFormat).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 50, yFormat);
const area = d3.area()
.x(({i}) => xScale(X[i]))
.y0(([y1]) => yScale(y1))
.y1(([, y2]) => yScale(y2));
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.append("g")
.selectAll("path")
.data(series)
.join("path")
.attr("fill", ([{i}]) => color(Z[i]))
.attr("d", area)
.append("title")
.text(([{i}]) => Z[i]);
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis)
.call(g => g.select(".domain").remove());
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line")
.filter(d => d === 0 || d === 1)
.clone()
.attr("x2", width - marginLeft - marginRight))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel));
return Object.assign(svg.node(), {scales: {color}});
}
}
< / script >
< h2 > Outer Wilds< / h2 >
< svg width = "500" height = "500" id = "visOW" class = "overflow" > < / svg >
< / div >
< div class = "col-2" > < / div >
< div class = "col-4" >
< script type = "module" id = "cuphead" >
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
const jsonData = d3.json("http://localhost:8888");
jsonData.then(function (data){
let dataset = data.reports;
processData(dataset);
});
function processData(data) {
let parsedCupheadData = [];
let indexWorking = 0;
let indexBroken = 1;
let prevDate = 14;
parsedCupheadData[indexWorking] = new Object();
parsedCupheadData[indexWorking].date = new Date("2019-11-01");
parsedCupheadData[indexWorking].title = "Working";
parsedCupheadData[indexWorking].number = 0;
parsedCupheadData[indexBroken] = new Object();
parsedCupheadData[indexBroken].date = new Date("2019-11-01");
parsedCupheadData[indexBroken].title = "Borked";
parsedCupheadData[indexBroken].number = 0;
for(let i = 0; i < data.length ; i + + ) {
let currentReport = data[i];
let currentDate = new Date(data[i].timestamp * 1000);
let dateString = currentDate.getFullYear().toString() + "-" + currentDate.getMonth().toString() + "-" + currentDate.getDate().toString();
if(data[i].game_title != "Cuphead")
{continue;} else {
if(currentDate.getDate() != prevDate) {
prevDate = currentDate.getDate();
indexBroken = indexBroken + 2;
indexWorking = indexWorking + 2;
parsedCupheadData[indexWorking] = new Object();
parsedCupheadData[indexWorking].date = currentDate;
parsedCupheadData[indexWorking].title = "Working";
parsedCupheadData[indexWorking].number = 0;
parsedCupheadData[indexBroken] = new Object();
parsedCupheadData[indexBroken].date = currentDate;
parsedCupheadData[indexBroken].title = "Borked";
parsedCupheadData[indexBroken].number = 0;
}
if(data[i].verdict == 0) {
parsedCupheadData[indexBroken].number++;
}
if(data[i].verdict == 1) {
parsedCupheadData[indexWorking].number++;
}
}
}
document.getElementById("visCuphead").appendChild(StackedAreaChart(parsedCupheadData, {
x: d => d.date,
y: d => d.number,
z: d => d.title,
yLabel: "Number of Reports--Cuphead",
width: 700,
height: 250
}
));
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/normalized-stacked-area-chart
function StackedAreaChart(data, {
x = ([x]) => x, // given d in data, returns the (ordinal) x-value
y = ([, y]) => y, // given d in data, returns the (quantitative) y-value
z = () => 1, // given d in data, returns the (categorical) z-value
marginTop = 20, // top margin, in pixels
marginRight = 30, // right margin, in pixels
marginBottom = 30, // bottom margin, in pixels
marginLeft = 40, // left margin, in pixels
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
xType = d3.scaleUtc, // type of x-scale
xDomain, // [xmin, xmax]
xRange = [marginLeft, width - marginRight], // [left, right]
yType = d3.scaleLinear, // type of y-scale
yDomain, // [ymin, ymax]
yRange = [height - marginBottom, marginTop], // [bottom, top]
zDomain, // array of z-values
offset = d3.stackOffsetExpand, // stack offset method
order = d3.stackOrderNone, // stack order method
yLabel, // a label for the y-axis
xFormat, // a format specifier string for the x-axis
yFormat = "%", // a format specifier string for the y-axis
colors = d3.schemeDark2, // an array of colors for the (z) categories
} = {}) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
const Z = d3.map(data, z);
// Compute default x- and z-domains, and unique the z-domain.
if (xDomain === undefined) xDomain = d3.extent(X);
if (zDomain === undefined) zDomain = Z;
zDomain = new d3.InternSet(zDomain);
// Omit any data not present in the z-domain.
const I = d3.range(X.length).filter(i => zDomain.has(Z[i]));
// Compute a nested array of series where each series is [[y1, y2], [y1, y2],
// [y1, y2], …] representing the y-extent of each stacked rect. In addition,
// each tuple has an i (index) property so that we can refer back to the
// original data point (data[i]). This code assumes that there is only one
// data point for a given unique x- and z-value.
const series = d3.stack()
.keys(zDomain)
.value(([x, I], z) => Y[I.get(z)])
.order(order)
.offset(offset)
(d3.rollup(I, ([i]) => i, i => X[i], i => Z[i]))
.map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));
// Compute the default y-domain. Note: diverging stacks can be negative.
if (yDomain === undefined) yDomain = d3.extent(series.flat(2));
// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);
const color = d3.scaleOrdinal(zDomain, colors);
const xAxis = d3.axisBottom(xScale).ticks(width / 80, xFormat).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 50, yFormat);
const area = d3.area()
.x(({i}) => xScale(X[i]))
.y0(([y1]) => yScale(y1))
.y1(([, y2]) => yScale(y2));
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.append("g")
.selectAll("path")
.data(series)
.join("path")
.attr("fill", ([{i}]) => color(Z[i]))
.attr("d", area)
.append("title")
.text(([{i}]) => Z[i]);
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis)
.call(g => g.select(".domain").remove());
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line")
.filter(d => d === 0 || d === 1)
.clone()
.attr("x2", width - marginLeft - marginRight))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel));
return Object.assign(svg.node(), {scales: {color}});
}
}
< / script >
< h2 > Cuphead< / h2 >
< svg width = "500" height = "500" id = "visCuphead" class = "overflow" > < / svg >
< / div >
< div class = "col-1" > < / div >
< / div >
< div class = "row reverseOnMobile" >
< div class = "col-1" > < / div >
< div class = "col-4 overflow lastOnMobile" >
< script type = "module" id = "apex" >
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
const jsonData = d3.json("http://localhost:8888");
jsonData.then(function (data){
let dataset = data.reports;
processData(dataset);
});
function processData(data) {
console.log(data);
let parsedApexData = [];
let indexWorking = 0;
let indexBroken = 1;
let prevDate = 14;
parsedApexData[indexWorking] = new Object();
parsedApexData[indexWorking].date = new Date("2020-11-04");
parsedApexData[indexWorking].title = "Working";
parsedApexData[indexWorking].number = 0;
parsedApexData[indexBroken] = new Object();
parsedApexData[indexBroken].date = new Date("2020-11-04");
parsedApexData[indexBroken].title = "Borked";
parsedApexData[indexBroken].number = 0;
for(let i = 0; i < data.length ; i + + ) {
let currentReport = data[i];
let currentDate = new Date(data[i].timestamp * 1000);
let dateString = currentDate.getFullYear().toString() + "-" + currentDate.getMonth().toString() + "-" + currentDate.getDate().toString();
if(data[i].game_title != "Apex Legends")
{continue;} else {
console.log("Apex!");
if(currentDate.getDate() != prevDate) {
prevDate = currentDate.getDate();
indexBroken = indexBroken + 2;
indexWorking = indexWorking + 2;
parsedApexData[indexWorking] = new Object();
parsedApexData[indexWorking].date = currentDate;
parsedApexData[indexWorking].title = "Working";
parsedApexData[indexWorking].number = 0;
parsedApexData[indexBroken] = new Object();
parsedApexData[indexBroken].date = currentDate;
parsedApexData[indexBroken].title = "Borked";
parsedApexData[indexBroken].number = 0;
}
if(data[i].verdict == 0) {
parsedApexData[indexBroken].number++;
}
if(data[i].verdict == 1) {
parsedApexData[indexWorking].number++;
}
}
}
console.log(parsedApexData);
document.getElementById("visApex").appendChild(StackedAreaChart(parsedApexData, {
x: d => d.date,
y: d => d.number,
z: d => d.title,
yLabel: "Percentage of Reports--Apex Legends",
width: 500,
height: 250
}
));
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/normalized-stacked-area-chart
function StackedAreaChart(data, {
x = ([x]) => x, // given d in data, returns the (ordinal) x-value
y = ([, y]) => y, // given d in data, returns the (quantitative) y-value
z = () => 1, // given d in data, returns the (categorical) z-value
marginTop = 20, // top margin, in pixels
marginRight = 30, // right margin, in pixels
marginBottom = 30, // bottom margin, in pixels
marginLeft = 40, // left margin, in pixels
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
xType = d3.scaleUtc, // type of x-scale
xDomain, // [xmin, xmax]
xRange = [marginLeft, width - marginRight], // [left, right]
yType = d3.scaleLinear, // type of y-scale
yDomain, // [ymin, ymax]
yRange = [height - marginBottom, marginTop], // [bottom, top]
zDomain, // array of z-values
offset = d3.stackOffsetExpand, // stack offset method
order = d3.stackOrderNone, // stack order method
yLabel, // a label for the y-axis
xFormat, // a format specifier string for the x-axis
yFormat = "%", // a format specifier string for the y-axis
colors = d3.schemeDark2, // an array of colors for the (z) categories
} = {}) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
const Z = d3.map(data, z);
// Compute default x- and z-domains, and unique the z-domain.
if (xDomain === undefined) xDomain = d3.extent(X);
if (zDomain === undefined) zDomain = Z;
zDomain = new d3.InternSet(zDomain);
// Omit any data not present in the z-domain.
const I = d3.range(X.length).filter(i => zDomain.has(Z[i]));
// Compute a nested array of series where each series is [[y1, y2], [y1, y2],
// [y1, y2], …] representing the y-extent of each stacked rect. In addition,
// each tuple has an i (index) property so that we can refer back to the
// original data point (data[i]). This code assumes that there is only one
// data point for a given unique x- and z-value.
const series = d3.stack()
.keys(zDomain)
.value(([x, I], z) => Y[I.get(z)])
.order(order)
.offset(offset)
(d3.rollup(I, ([i]) => i, i => X[i], i => Z[i]))
.map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));
// Compute the default y-domain. Note: diverging stacks can be negative.
if (yDomain === undefined) yDomain = d3.extent(series.flat(2));
// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);
const color = d3.scaleOrdinal(zDomain, colors);
const xAxis = d3.axisBottom(xScale).ticks(width / 80, xFormat).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 50, yFormat);
const area = d3.area()
.x(({i}) => xScale(X[i]))
.y0(([y1]) => yScale(y1))
.y1(([, y2]) => yScale(y2));
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.append("g")
.selectAll("path")
.data(series)
.join("path")
.attr("fill", ([{i}]) => color(Z[i]))
.attr("d", area)
.append("title")
.text(([{i}]) => Z[i]);
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis)
.call(g => g.select(".domain").remove());
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line")
.filter(d => d === 0 || d === 1)
.clone()
.attr("x2", width - marginLeft - marginRight))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel));
return Object.assign(svg.node(), {scales: {color}});
}
}
< / script >
< h2 > Apex Legends< / h2 >
< svg width = "500" height = "500" id = "visApex" class = "overflow" > < / svg >
< / div >
< div class = "col-1" > < / div >
< div class = "col-5 roundCorners card firstOnMobile" >
< h2 > Anti-Cheat< / h2 >
< p > Some games utilize anti-cheat modules that are designed to interface directly with the Windows kernel.< / p >
< p > As a result, these games would normally be entirely incompatible with Linux.< / p >
< p > However, Valve has been working with the developers of two common kernel-level anticheat modules (EasyAntiCheat and BattlEye) to allow games using these to run under Proton.< / p >
< p > Now, as long as the developer permits it, these games will run perfectly well on Linux through Proton.< / p >
< p > An example of this can be seen here, with Apex Legends.< / p >
< / div >
< div class = "col-1" > < / div >
< / div >
< div class = "spacer" > < / div >
< div class = "row" >
< div class = "col-2" > < / div >
< div class = "col-8 roundCorners card" >
< h2 > Conclusion< / h2 >
< p > Even from this small sample of data, I would conclude that the state of gaming on Linux has periods of instability, but is overall continuing to improve.< / p >
< p > With even large-scale multiplayer games such as Apex Legends taking action to be more Linux-friendly, I believe the future of Linux as a gaming platform is brighter than ever.< / p >
< / div >
< div class = "col-2" > < / div >
< / div >
< div class = "spacer" > < / div >
< div class = "row" >
< div class = "col-2" > < / div >
< div class = "col-8 roundCorners card" >
< h2 > DATA COLLECTION< / h2 >
< p > All the data I used was originally collected by < a href = "https://www.protondb.com" > ProtonDB< / a > , a crowdsourced database of user reports of game compatibility with Linux.< / p >
< p > The data I used is the complete set of ProtonDB user reports from October 2019 to October 2022, for a selection of ten different games:< / p >
< ul >
< li > Age of Empires II: Definitive Edition< / li >
< li > Apex Legends< / li >
< li > Cuphead< / li >
< li > Cyberpunk 2077< / li >
< li > Ori and the Blind Forest: Definitive Edition< / li >
< li > Outer Wilds< / li >
< li > Risk of Rain 2< / li >
< li > Subnautica< / li >
< li > The Elder Scrolls V: Skyrim< / li >
< li > VRChat< / li >
< / ul >
< p > Data from ProtonDB is made available under the Open Database License: < a href = "http://opendatacommons.org/licenses/odbl/1.0/" > Link< / a > . Any rights in individual contents of the database are licensed under the Database Contents License: < a href = "http://opendatacommons.org/licenses/dbcl/1.0/" > Link< / a > . < / p >
< / div >
< div class = "col-2" > < / div >
< / div >
< div class = "spacer" > < / div >
2022-12-12 18:23:22 -07:00
2022-12-13 01:08:09 -07:00
< div class = "row" >
< div class = "col-4" > < / div >
< div class = "col-4 roundCorners card" >
< label > Show Only Working?< / label >
< input id = 'working' type = 'checkbox' / >
< br >
< label > Game Title< / label >
< input id = 'gameTitle' type = 'text' / >
< br >
< label > Linux Distro< / label >
< input id = 'os' type = 'text' / >
< br >
< label > CPU Model< / label >
< input id = 'cpu' type = 'text' / >
< br >
< label > GPU Model< / label >
< input id = 'gpu' type = 'text' / >
< br >
< label > Sort By Time Submitted< / label >
< select id = 'sortTime' >
< option value = '' > < / option >
< option value = 'ASC' > Ascending< / option >
< option value = 'DESC' > Descending< / option >
< / select >
< br >
< button id = 'search' > View Reports< / button >
< / div >
< div class = "col-4" > < / div >
< script >
const isEmpty = (obj) => Object.keys(obj).length === 0;
document.getElementById('search').addEventListener('click', (event) => {
const dataUrl = 'http://localhost:8888';
let ajaxParameters = {};
if (document.querySelector('#working:checked')) {
ajaxParameters.working = 1;
}
if (document.getElementById('gameTitle').value.length !== 0) {
ajaxParameters.gameTitle = document.getElementById('gameTitle').value;
}
if (document.getElementById('os').value.length !== 0) {
ajaxParameters.os = document.getElementById('os').value;
}
if (document.getElementById('cpu').value.length !== 0) {
ajaxParameters.cpu = document.getElementById('cpu').value;
}
if (document.getElementById('gpu').value.length !== 0) {
ajaxParameters.gpu = document.getElementById('gpu').value;
}
if (document.getElementById('sortTime').value.length !== 0) {
ajaxParameters.sortTime = document.getElementById('sortTime').value;
}
function niceOutput(a) {
if(a == 1) {
return "Yes";
} else {
return "No";
}
}
const xhttp = new XMLHttpRequest();
xhttp.onload = function() {
let displayTable = '< table > ' +
'< thead > ' +
'< tr > ' +
'< th > Game Title< / th > ' +
'< th > Submitted< / th > ' +
'< th > Working?< / th > ' +
'< th > Distro< / th > ' +
'< th > CPU< / th > ' +
'< th > GPU< / th > ' +
'< th > Notes< / th > ' +
'< / tr > ' +
'< / thead > ' +
'< tbody > ';
if (typeof this.responseText !== 'undefined' & & this.responseText.length > 0) {
let ajaxResult = JSON.parse(this.responseText);
if (typeof ajaxResult.reports !== 'undefined') {
for (let x = 0; x < ajaxResult.reports.length ; x + + ) {
displayTable += '< tr > ' +
'< td > ' + ajaxResult.reports[x].game_title + '< / td > ' +
'< td > ' + new Date(ajaxResult.reports[x].timestamp * 1000).toLocaleDateString() + '< / td > ' +
'< td > ' + niceOutput(ajaxResult.reports[x].verdict) + '< / td > ' +
'< td > ' + ajaxResult.reports[x].systemInfo_os + '< / td > ' +
'< td > ' + ajaxResult.reports[x].systemInfo_cpu + '< / td > ' +
'< td > ' + ajaxResult.reports[x].systemInfo_gpu + '< / td > ' +
'< td > ' + ajaxResult.reports[x].notes_summary + '< / td > ' +
'< / tr > ';
}
}
}
displayTable += '< / tbody > < / table > ';
document.getElementById('tableOutput').innerHTML = displayTable;
}
xhttp.open("GET", dataUrl + (!isEmpty(ajaxParameters) ? '?' + new URLSearchParams(ajaxParameters) : ''));
xhttp.send();
});
< / script >
< / div >
< div class = "row" >
< div class = "col-1" > < / div >
< div class = "col-10 overflow" id = "tableOutput" > < / div >
< div class = "col-1" > < / div >
< / div >
2022-12-12 18:23:22 -07:00
< div id = "footer-spacer" > < / div >
< div id = "footer" >
< div class = "row" >
< div class = "col-6" > < / div >
< div class = "col-6" >
2022-12-18 13:09:27 -07:00
< img src = "assets/img/logos/lunarpenguin.png" alt = "Lunar Penguin Logo" id = "footer-logo" >
2022-12-12 18:23:22 -07:00
< span id = "footer-text" > Page created by < a href = "https://lunarpenguin.net" > Kaj Forney< / a > , 2022.< / span >
< / div >
< / div >
< / div >
2022-10-25 10:09:29 -06:00
< / body >
2022-12-12 18:23:22 -07:00
< / html >