1216 lines
No EOL
57 KiB
HTML
1216 lines
No EOL
57 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<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">
|
||
<title>Kaj Forney -- GIMM 260 Data Visualization Narrative</title>
|
||
|
||
<link rel="stylesheet" href="assets/css/main.css">
|
||
|
||
<script src="https://kit.fontawesome.com/55072ce5fd.js" crossorigin="anonymous"></script>
|
||
</head>
|
||
<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>
|
||
<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>
|
||
</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>
|
||
|
||
<div class="row reverseOnMobile">
|
||
<div class="col-2"></div>
|
||
<div class="col-3 bigIcon lastOnMobile">
|
||
<i class="fa-solid fa-wine-glass wobble"></i>
|
||
</div>
|
||
<div class="col-1"></div>
|
||
<div class="col-5 roundCorners card firstOnMobile">
|
||
<h2>WINE</h2>
|
||
<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>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="spacer"></div>
|
||
|
||
<div class="row">
|
||
<div class="col-1"></div>
|
||
<div class="col-5 roundCorners card">
|
||
<h2>Proton</h2>
|
||
<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>
|
||
</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>
|
||
<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>
|
||
</div>
|
||
<div class="col-1"></div>
|
||
<div class="col-5 roundCorners card">
|
||
<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>
|
||
</div>
|
||
<div class="col-1"></div>
|
||
</div>
|
||
|
||
<div class="spacer"></div>
|
||
|
||
<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>
|
||
|
||
<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>
|
||
|
||
<div id="footer-spacer"></div>
|
||
|
||
<div id="footer">
|
||
<div class="row">
|
||
<div class="col-6"></div>
|
||
<div class="col-6">
|
||
<img src="assets/img/logos/lunarpenguin.png" alt="Lunar Penguin Logo" id="footer-logo">
|
||
<span id="footer-text">Page created by <a href="https://lunarpenguin.net">Kaj Forney</a>, 2022.</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
</body>
|
||
</html> |