Open grcameron opened 4 years ago
Adding the conversation from slack here for reference:
Grant Cameron Jan 21st at 4:12 AM Hi folks, was wondering if anyone is aware of a fork for pa11y dashboard that offers a way to group and visualise pages (e.g. Site has many areas which has many pages)
Joey Ciechanowicz 1 month ago Hi Grant :wave: I'm not aware of any forks which add this feature. I hacked something together on a private board, but haven't had the time to formalise it and write it up with tests etc to be part of the full pa11y-dashboard
Grant Cameron 1 month ago Thanks Joey, is there anything I could do to help with that? I was going to hack something together myself, but maybe that time could be spent helping formalise something. If there is a plan to do that anyway? If not it's cool I can hack something together for our needs I think.
Joey Ciechanowicz 1 month ago Well, PR's are always always welcome :heart:
Joey Ciechanowicz 1 month ago To get something down proper, it's best to open an issue to discuss the change so as to gather feedback
Joey Ciechanowicz 1 month ago If you want to hack it together yourself, I can try dig out the code I threw together
Joey Ciechanowicz 1 month ago This goes in route and gathers the results, grouping by splitting the name on : route/wallboard.js
// This file is part of Pa11y Dashboard.
//
// Pa11y Dashboard is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pa11y Dashboard is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Pa11y Dashboard. If not, see <http://www.gnu.org/licenses/>.
'use strict';
module.exports = route;
function taskState(counts) {
if (counts.error > 0) {
return 'error';
}
if (counts.warning > 0) {
return 'warning';
}
if (counts.notice > 0) {
return 'notice';
}
return 'none';
}
// Route definition
function route(app) {
app.express.get('/wallboard', (request, response, next) => {
if (request.query.reload !== undefined) {
return response.render('reload', {
layout: false,
url: 'wallboard'
});
}
app.webservice.tasks.get({lastres: true}, (error, tasks) => {
if (error) {
return next(error);
}
const grouped = tasks.reduce((groups, task) => {
if (!task.last_result) {
return groups;
}
task.overallState = taskState(task.last_result.count);
const parts = task.name.split(':');
if (parts.length >= 2) {
const group = parts[0];
task.name = parts.slice(1).join(':');
if (!groups[group]) {
groups[group] = [];
}
groups[group].push(task);
} else {
groups.default.push(task);
}
return groups;
}, {default: []});
const groups = Object.entries(grouped).map(([groupName, groupedTasks]) => ({
groupName,
defaultGroup: groupName === 'default',
tasks: groupedTasks
})).filter(group => group.tasks.length > 0);
response.render('wallboard', {
groups,
layout: false
});
});
});
app.express.get('/wallboard-graph', (request, response, next) => {
if (request.query.reload !== undefined) {
return response.render('reload', {
layout: false,
url: 'wallboard-graph'
});
}
app.webservice.tasks.get({}, (error, tasks) => {
app.webservice.tasks.results({}, (error, results) => {
if (error) {
return next(error);
}
const tasksLookup = tasks.reduce((acc, curr) => {
acc[curr.id] = curr;
return acc;
}, {});
const modifiedResults = results.map(result => ({
date: result.date,
errors: result.count.error,
warnings: result.count.warning,
notices: result.count.notices,
name: tasksLookup[result.task].name
}));
response.render('wallboard-graph', {
data: modifiedResults,
layout: false
});
});
});
});
}
Collapse
Joey Ciechanowicz 1 month ago This goes in view/wallboard.html and renders /wallboard view/wallboard.html
<!DOCTYPE html>
<html lang="{{lang}}" class="no-javascript">
<head>
<meta charset="utf-8"/>
<title>Pa11y Wallboard</title>
<meta name="description" content="Pa11y Wallboard"/>
{{#if noindex}}
<meta name="robots" content="noindex"/>
{{/if}}
<link rel="icon" type="image/png" href="favicon.png"/>
<!-- For mobile devices. -->
<meta name="viewport" content="width=device-width"/>
<style type="text/css">
:root {
--error: #ed4b35;
--warning: #8b8b38;
--notice: #28628b;
}
* {
box-sizing: border-box;
}
body {
background: #3d3c3c;
padding: 0;
margin: 0;
font-family: Inconsolata, monospace;
color: #ada8a8;
}
.header {
width: 100%;
text-align: center;
}
.groups {
margin: 5px;
display: flex;
flex-wrap: wrap;
align-content: space-around;
}
.task {
background: #2a2929;
margin: 0 5px 1%;
width: 16%;
}
.task__header {
color: #fff;
width: 100%;
padding: 0 5px 0 5px;
min-height: 55px;
}
.task__header__group-name {
font-size: 1em;
margin: 0;
color: #ada8a8;
border-radius: 4px;
padding: 0 2px 0 2px
}
.task__header__name {
margin: 0;
}
.task--error {
border-top: 4px solid var(--error);
}
.task--warning {
border-top: 4px solid var(--warning);
}
.task--notice {
border-top: 4px solid var(--notice);
}
.task--none {
border-top: 4px solid #11c560;
}
.task__counts {
width: 100%;
display: flex;
}
.count {
width: 33.333%;
display: inline-block;
line-height: 40px;
text-align: center;
font-size: 1.5em;
color: white;
}
.count--error {
background: var(--error);
}
.count--warning {
background: var(--warning);
}
.count--notice {
background: var(--notice);
}
.group-name--1 {
background: #0069c5;
color: #fff;
}
.group-name--2 {
background: #6c757d;
color: #fff;
}
.group-name--3 {
background: #268942;
color: #fff;
}
.group-name--4 {
background: #b12f3d;
color: #fff;
}
.group-name--5 {
background: #b88707;
color: #fff;
}
.group-name--0 {
background: #fff;
color: #000;
}
</style>
</head>
<body>
<header class="header">
<h1 class="header__text">Accessibility</h1>
</header>
<section class="groups">
{{#each groups}}
{{#each tasks}}
<div class="task task--{{overallState}}">
<div class="task__header">
<h2 class="task__header__name">{{name}}</h2>
{{#unless ../defaultGroup}}
<h1 class="task__header__group-name group-name--{{mod @../index 6}}">
{{../groupName}}
</h1>
{{/unless}}
</div>
{{#last_result}}
<div class="task__counts">
<div class="count count--error">{{count.error}}</div>
<div class="count count--warning">{{count.warning}}</div>
<div class="count count--notice">{{count.notice}}</div>
</div>
{{/last_result}}
</div>
{{/each}}
{{/each}}
</section>
</body>
</html>
Collapse
Joey Ciechanowicz 1 month ago This goes in view/wallboard-graph.html and renders /wallboard-graph view/wallboard-graph.html
<!DOCTYPE html>
<html lang="{{lang}}" class="no-javascript">
<head>
<meta charset="utf-8"/>
<title>Pa11y Wallboard</title>
<meta name="description" content="Pa11y Wallboard"/>
{{#if noindex}}
<meta name="robots" content="noindex"/>
{{/if}}
<link rel="icon" type="image/png" href="favicon.png"/>
<!-- For mobile devices. -->
<meta name="viewport" content="width=device-width"/>
<script src="https://d3js.org/d3.v5.min.js"></script>
<style type="text/css">
:root {
--error: #ed4b35;
--warning: #8b8b38;
--notice: #28628b;
}
* {
box-sizing: border-box;
}
body {
background: #3d3c3c;
padding: 0;
margin: 0;
font-family: Inconsolata, monospace;
color: #ada8a8;
}
html, body, #container {
height: 100%;
}
.header {
width: 100%;
text-align: center;
}
#container {
width: 100%;
display: grid;
grid-template-columns: 100%;
grid-template-rows: 80% 20%;
}
#graph {
width: 100%;
height: 100%;
}
#labels {
margin: 0;
}
li {
display: inline-block;
padding-left: 8px;
font-size: 2rem;
}
li:before {
content: '\2022';
margin-right: 2px;
}
.axis {
color: #ada8a8;
stroke-width: 2;
font-size: 1.4rem;
}
path {
fill: none;
stroke-width: 2;
}
.legend {
font-size: 2rem;
}
</style>
</head>
<body>
<header class="header">
<h1 class="header__text">Converged Accessbility Errors</h1>
</header>
<div id="container">
<svg id="graph"></svg>
<ul id="labels"></ul>
</div>
<script>
const data = {{{json data}}};
// Set the dimensions of the canvas / graph
const margin = {top: 10, right: 50, bottom: 35, left: 60};
const containerDimensions = document.getElementById('graph').getBoundingClientRect();
console.log(containerDimensions);
const width = document.getElementById('graph').clientWidth - margin.left - margin.right;
const height = Math.floor(containerDimensions.height) - margin.top - margin.bottom;
// Parse the date / time
data.forEach(result => {
result.date = new Date(result.date);
});
data.sort((a, b) => {
return a.date > b.date ?
1
: a.date < b.date ?
-1
: 0;
});
const timeDomain = d3.extent(data, result => result.date);
timeDomain[1] = new Date(timeDomain[1].valueOf()).setDate(timeDomain[1].getDate() + 4);
// Set the ranges
const x = d3.scaleTime()
.domain(d3.extent(data, result => result.date))
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, d3.max(data, result => result.errors) + 1])
.range([height, 0])
// Define the line
const line = d3.line()
.x(result => x(result.date))
.y(result => y(result.errors));
// Adds the svg canvas
const svg = d3.select('#graph')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform',
'translate(' + margin.left + ',' + margin.top + ')');
const labels = d3.select('#labels');
// Nest the entries by symbol
const dataNest = d3.nest()
.key(result => result.name)
.entries(data);
// set the colour scale
const color = d3.scaleOrdinal(d3.schemeCategory10);
legendSpace = width / dataNest.length; // spacing for the legend
// Loop through each symbol / key
dataNest.forEach((nest, index) => {
svg.append('path')
.attr('class', 'line')
.style('stroke', color(nest.key))
.attr('d', line(nest.values));
// Add the Legend
labels.append('li')
.style('color', color(nest.key))
.text(nest.key);
});
// Add the X Axis
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.axisBottom(x));
// Add the Y Axis
svg.append('g')
.attr('class', 'axis')
.call(d3.axisLeft(y));
svg.selectAll('.dot')
.data(data)
.enter().append('circle')
.attr('class', 'dot')
.attr('cx', result => x(result.date))
.attr('cy', result => y(result.errors))
.style('fill', result => color(result.name))
.attr('r', 5)
</script>
</body>
</html>
Collapse
Joey Ciechanowicz 1 month ago Shove this in view/reload.html and then you can use /wallboard?reload=true or /wallboard-graph?reload=true view/reload.html
<!DOCTYPE html>
<html lang="{{lang}}" class="no-javascript">
<head>
<meta charset="utf-8"/>
<title>Pa11y Wallboard</title>
<meta name="description" content="Pa11y Wallboard"/>
{{#if noindex}}
<meta name="robots" content="noindex"/>
{{/if}}
<link rel="icon" type="image/png" href="favicon.png"/>
<!-- For mobile devices. -->
<meta name="viewport" content="width=device-width"/>
<style>
html, body, iframe { height: 100%; width: 100%}
* {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<iframe id="wallboard-frame" style="position: absolute; height: 100%; border: none" src="/{{url}}"></iframe>
<script type="text/javascript">
setInterval(() => {
document.getElementById('wallboard-frame').contentWindow.location.reload();
window.location.reload();
}, 5 * 60 * 1000);
</script>
</body>
</html>
Collapse
Grant Cameron 1 month ago Thanks so much Joey! I'll raise an issue to discuss further, and in the mean time I'll try out your changes, really appreciate it.
Grant Cameron 1 month ago For reference, created this issue: https://github.com/pa11y/pa11y-dashboard/issues/254 :star2: 1
Grant Cameron 1 month ago This is great, managed to get that going for the time being.
Joey Ciechanowicz 1 month ago fantastic!
Pa11y Dashboard is a great tool, but it's apparent that in our use case (100's of pages across different projects), that it would be really useful to have the following:
The reason for this is to so that I can easily display the progress of our accessibility improvements within the pa11y dashboard UI.
I spoke to @joeyciechanowicz over slack about this, they have a private repo where something similar has been done, but not tested/reviewed/formalised.