This repository has been archived by the owner on Jul 3, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b94905f
commit a095190
Showing
7 changed files
with
382 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
article { | ||
padding-left: 10px; | ||
padding-right: 10px; | ||
} | ||
|
||
.tick { | ||
color: black; | ||
} | ||
|
||
.response { | ||
fill: #003399; | ||
} | ||
|
||
.legend.swatch { | ||
stroke: white; | ||
stroke-width: 2; | ||
} | ||
|
||
.legend.label { | ||
text-anchor: middle; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
export class Chart { | ||
constructor(config, data) { | ||
this.config = config | ||
this.data = data | ||
return this | ||
} | ||
|
||
generateXScale(accessor) { | ||
this.xScale = d3.scaleBand() | ||
.domain(this.data.responseSet.choices.map(accessor)) | ||
.range([this.config.margin.left, this.config.width - this.config.margin.right]) | ||
.paddingOuter(this.config.padding.outer) | ||
.paddingInner(this.config.padding.inner) | ||
} | ||
|
||
generateXLabels() { | ||
d3.select(`#${this.data.name}`) | ||
.append('svg') | ||
.attr('viewBox', [0, 0, this.config.width, this.config.height]) | ||
.append('g').attr('class', 'x ticks') | ||
.call(d3.axisBottom(this.xScale)) | ||
.selectAll('.tick text') | ||
.call(this.wrap, this.xScale.bandwidth()) | ||
this.config.margin.bottom += d3.max(d3.select(`#${this.data.name}`).selectAll('.tick text').nodes(), (text) => text.getBBox().height) | ||
} | ||
|
||
wrap(text, width) { | ||
text.each(function() { | ||
var text = d3.select(this), | ||
words = text.text().split(/\s+/).reverse(), | ||
word, | ||
line = [], | ||
lineNumber = 0, | ||
lineHeight = 1.1, | ||
y = text.attr('y'), | ||
dy = parseFloat(text.attr('dy')), | ||
tspan = text.text(null).append('tspan').attr('x', 0).attr('y', y).attr('dy', dy + 'em'); | ||
while (word = words.pop()) { | ||
line.push(word); | ||
tspan.text(line.join(' ')); | ||
if (tspan.node().getComputedTextLength() > width) { | ||
line.pop(); | ||
tspan.text(line.join(' ')); | ||
line = [word]; | ||
tspan = text.append('tspan').attr('x', 0).attr('y', y).attr('dy', ++lineNumber * lineHeight + dy +'em').text(word); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
|
||
export class BarChart extends Chart { | ||
constructor(config, data) { | ||
super(config, data) | ||
this.generateXScale(d => Object.keys(d)[0]) | ||
this.generateXLabels() | ||
this.yScale = d3.scaleLinear() | ||
.domain([0, this.data.responseSet.choices.reduce((acc, choice) => Object.values(choice)[0] > acc ? Object.values(choice)[0] : acc, 0)]).nice() | ||
.range([this.config.height - this.config.margin.bottom, this.config.margin.top]) | ||
d3.select(`#${this.data.name}`) | ||
.select('svg') | ||
.call(svg => svg | ||
.append('g') | ||
.attr('class', 'axes') | ||
.call(g => g | ||
.append('g') | ||
.attr('class', 'axis x') | ||
.attr('transform', `translate(0, ${this.config.height - this.config.margin.bottom})`) | ||
.append(() => d3.select(`#${this.data.name}`).select('.x.ticks').remove().node())) | ||
.call(g => g | ||
.append('g') | ||
.attr('class', 'axis y') | ||
.attr('transform', `translate(${this.config.margin.left}, 0)`) | ||
.append('g').attr('class', 'y ticks').call(d3.axisLeft(this.yScale)))) | ||
.call(svg => svg | ||
.append('g') | ||
.attr('class', 'responses') | ||
.selectAll('rect') | ||
.data(this.data.responseSet.choices) | ||
.join('rect') | ||
.attr('class', 'response') | ||
.attr('width', this.xScale.bandwidth()) | ||
.attr('height', d => this.yScale.range()[0] - this.yScale(Object.values(d)[0])) | ||
.attr('x', d => this.xScale(Object.keys(d)[0])) | ||
.attr('y', d => this.yScale(Object.values(d)[0]))) | ||
return this | ||
} | ||
} | ||
|
||
export class StackedBarChart extends Chart { | ||
constructor(config, data) { | ||
super(config, data) | ||
|
||
this.generateXScale(d => Object.values(d)[0]) | ||
this.generateXLabels() | ||
|
||
let keys = [...new Set(this.data.responseSet.choices.map(({ question, ...responses}) => { return { ...responses }}) | ||
.reduce((acc, responses) => acc = acc.concat(Object.keys(responses)),[]))] | ||
|
||
this.colour = d3.scaleOrdinal() | ||
.domain(keys.map((d, i)=> i)) | ||
.range(['#003399','#ffcc00']) | ||
|
||
d3.select(`#${this.data.name}`).select('svg') | ||
.append('g') | ||
.attr('class', 'legend') | ||
.attr('transform', `translate(${this.config.width - this.config.margin.right - this.config.legend.width}, ${this.config.margin.top})`) | ||
.selectAll('g') | ||
.data(this.colour.domain()) | ||
.join('g') | ||
.attr('transform', (d, i, arr) => `translate(${i * this.config.legend.width / arr.length}, 0)`) | ||
.call(g => g | ||
.append('rect') | ||
.attr('class', 'legend swatch') | ||
.attr('height', this.config.legend.height) | ||
.attr('width', (d, i, arr) => this.config.legend.width / arr.length) | ||
.style('fill', d => this.colour(d))) | ||
.call(g => g | ||
.append('text') | ||
.attr('transform', `translate(0, ${this.config.legend.height})`) | ||
.attr('class', 'legend label') | ||
.attr('x', (d, i, arr) => this.config.legend.width / arr.length / 2) | ||
.attr('y', '1em') | ||
.text(d => keys[d])) | ||
|
||
this.config.margin.top += d3.select('.legend').node().getBBox().height | ||
|
||
let stack = d3.stack() | ||
.keys(keys) | ||
.order(d3.stackOrderNone) | ||
.offset(d3.stackOffsetNone) | ||
|
||
let series = stack(this.data.responseSet.choices); | ||
|
||
this.yScale = d3.scaleLinear() | ||
.domain([0, d3.max(series, d => d3.max(d, d => d[1]))]).nice() | ||
.range([this.config.height - this.config.margin.bottom, this.config.margin.top]) | ||
|
||
d3.select(`#${this.data.name}`) | ||
.select('svg') | ||
.call(svg => svg | ||
.append('g') | ||
.attr('class', 'axes') | ||
.call(g => g | ||
.append('g') | ||
.attr('class', 'axis x') | ||
.attr('transform', `translate(0, ${this.config.height - this.config.margin.bottom})`) | ||
.append(() => d3.select(`#${this.data.name}`).select('.x.ticks').remove().node())) | ||
.call(g => g | ||
.append('g') | ||
.attr('class', 'axis y') | ||
.attr('transform', `translate(${this.config.margin.left}, 0)`) | ||
.append('g').attr('class', 'y ticks').call(d3.axisLeft(this.yScale)))) | ||
.call(svg => svg | ||
.append('g') | ||
.selectAll('g') | ||
.data(series) | ||
.join('g') | ||
.style('fill', (d, i) => this.colour(i)) | ||
.selectAll('rect') | ||
.data(d => d) | ||
.join('rect') | ||
.attr('x', (d, i) => this.xScale(this.data.responseSet.choices[i].question)) | ||
.attr('y', d => this.yScale(d[1])) | ||
.attr('height', d => this.yScale(d[0]) - this.yScale(d[1])) | ||
.attr('width', this.xScale.bandwidth())) | ||
return this | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { BarChart, StackedBarChart } from './charts.js'; | ||
import { Table } from './table.js'; | ||
|
||
var questionsFetch = fetch('resources/data/questions.json') | ||
.then(res => res.json()) | ||
|
||
var responsesFetch = fetch('https://api.github.com/repos/simonarnell/GDPRDPIAT/contents/_data/dpia?ref=staticman') | ||
.then(res => res.json()) | ||
.then(json => Promise.all(json.map(dpia => { | ||
return fetch(dpia.download_url) | ||
.then(res => res.json()) | ||
}))) | ||
|
||
var questions = [], responses = []; | ||
Promise.all([questionsFetch, responsesFetch]).then(vals => { | ||
vals[0].pages.map(page => page.questions.map(question => { | ||
questions.push(question) | ||
responses[question.name] = { | ||
...(()=>{ | ||
let choices = {} | ||
if(question.choices !== undefined) { | ||
choices = question.choices.reduce((acc, choice) => { | ||
acc[choice.value] = 0 | ||
return acc | ||
}, {}) | ||
} else if(question.type == 'matrix') { | ||
choices = question.rows.reduce((acc, row) => { | ||
acc[row.value] = question.columns.reduce((acc, column) => { | ||
acc[column.value] = 0 | ||
return acc | ||
}, {}) | ||
return acc | ||
}, {}) | ||
} | ||
return choices | ||
})(), | ||
...vals[1].map(response => { | ||
let formattedResponse = []; | ||
if(response[question.name] !== undefined) { | ||
if(question.type == 'checkbox') formattedResponse = response[question.name].split(',') | ||
else formattedResponse = [response[question.name]] | ||
} else if(question.type == 'matrix') formattedResponse = question.rows.map(row => response[question.name + String.fromCharCode(97 + (row.value - 1))]) | ||
return formattedResponse | ||
}).reduce((acc, responses, idx) => { | ||
if(idx == 0 && question.type == 'matrix') | ||
acc = question.rows.reduce((acc, row) => { | ||
acc[row.value] = question.columns.reduce((acc, column) => { | ||
acc[column.value] = 0 | ||
return acc | ||
}, {}) | ||
return acc | ||
}, {}) | ||
let countedResponse = responses.reduce((acc, response, idx) => { | ||
if(question.type == 'matrix') { | ||
if(response == undefined) return acc | ||
if(!acc[idx+1]) acc[idx+1] = {}; | ||
acc[idx+1][response] = !!acc[idx+1][response] ? acc[idx+1][response] + 1 : 1 | ||
} else acc[response] = !!acc[response] ? acc[response] + 1 : 1 | ||
return acc | ||
}, {}) | ||
Object.keys(countedResponse).forEach(item => { | ||
if(question.type == 'matrix') { | ||
if(!acc[item]) acc[item] = {} | ||
Object.keys(countedResponse[item]).forEach(response => acc[item][response] = !!acc[item] ? acc[item][response] + countedResponse[item][response] : 1) | ||
} else acc[item] = !!acc[item] ? acc[item] + countedResponse[item] : 1 | ||
}) | ||
return acc | ||
}, {}) | ||
} | ||
})) | ||
questions.map(({choices, columns, name, rows, title, type}) => { | ||
let config = { | ||
height: 540, | ||
width: 1920, | ||
margin: { | ||
top: 10, | ||
left: 20, | ||
right: 10, | ||
bottom: 10 | ||
}, | ||
padding: { | ||
inner: 0.03, | ||
outer: 0.03 | ||
}, | ||
legend: { | ||
width: 240, | ||
height: 50 | ||
} | ||
} | ||
let responseSet = {} | ||
if(type == 'matrix') { | ||
responseSet.choices = rows.map(row => { | ||
return { | ||
question: row.text, | ||
...columns.reduce((acc, column) => { | ||
acc[column.text] = responses[name][row.value][column.value] | ||
return acc | ||
}, {}) | ||
} | ||
}) | ||
} else if (choices !== undefined) { | ||
responseSet.choices = choices.map(choice => { | ||
return { [choice.text]: responses[name][choice.value] } | ||
}) | ||
} else return | ||
let section = document.createElement('section') | ||
section.id = name | ||
document.getElementById('results').appendChild(section) | ||
new Table({ name, title }) | ||
if(type == 'matrix') new StackedBarChart(config, { name, type, responseSet }) | ||
else new BarChart(config, { name, type, responseSet }) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
export class Table { | ||
constructor(data) { | ||
this.data = data; | ||
let table = document.createElement('table') | ||
let tr = document.createElement('tr') | ||
let td = document.createElement('td') | ||
let h3 = document.createElement('h3') | ||
let match = this.data.name.match(/(question)(([0-9])+[a-zA-Z]*)/) | ||
h3.textContent = `${match[1].slice(0,1).toUpperCase()}${match[1].slice(1, match[1].length)} ${match[2]}` | ||
td.appendChild(h3) | ||
tr.appendChild(td) | ||
table.appendChild(tr) | ||
tr = document.createElement('tr') | ||
td = document.createElement('td') | ||
let h4 = document.createElement('h4') | ||
h4.textContent = this.data.title | ||
td.appendChild(h4) | ||
tr.appendChild(td) | ||
table.appendChild(tr) | ||
document.getElementById(this.data.name).appendChild(table) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
<!doctype html> | ||
<html> | ||
<head> | ||
<title>General Data Protection Regulation - Data Protection Impact Assessment Tool - Results</title> | ||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> | ||
<link rel="stylesheet" href="resources/css/gdprdpiat.css"> | ||
<link rel="stylesheet" href="resources/css/results.css"> | ||
<script src="https://d3js.org/d3.v5.min.js"></script> | ||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-127874216-1"></script> | ||
<script> | ||
window.dataLayer = window.dataLayer || []; | ||
function gtag(){dataLayer.push(arguments);} | ||
gtag('js', new Date()); | ||
gtag('config', 'UA-127874216-1'); | ||
</script> | ||
<script type="application/ld+json"> | ||
{ | ||
"@context":"http://schema.org", | ||
"@type":"WebSite", | ||
"name":"General Data Protection Regulation (GDPR) Data Protection Impact Assessment (DPIA) Tool", | ||
"author": { | ||
"@type": "Person", | ||
"name": "Simon Arnell", | ||
"url": "https://simonarnell.github.io/" | ||
}, | ||
"description":"A GDPR Data Protection Impact Assessment (DPIA) tool to assist organisations to evaluate data protection risks with respect to the EU's General Data Protection Regulation. 🇪🇺", | ||
"mentions" : { | ||
"@type": "Legislation", | ||
"name": "General Data Protection Regulation", | ||
"legislationType":"regulation", | ||
"legislationIdentifier": "2016/679", | ||
"sameAs": "https://en.wikipedia.org/wiki/General_Data_Protection_Regulation" | ||
}, | ||
"url": "https://simonarnell.github.io/GDPRDPIAT/", | ||
"sameAs": "https://github.com/simonarnell/GDPRDPIAT" | ||
} | ||
</script> | ||
<script type="module" src="resources/js/results.js"></script> | ||
</head> | ||
<body> | ||
<header class="header"> | ||
<a class="forkme" href="https://github.com/simonarnell/GDPRDPIAT"><img width="149" height="149" src="https://github.blog/wp-content/uploads/2008/12/forkme_left_darkblue_121621.png?resize=149%2C149" class="attachment-full size-full" alt="Fork me on GitHub" data-recalc-dims="1"></a> | ||
<h1>EU General Data Protection Regulation (GDPR) Data Protection Impact Assessment (DPIA) Tool</h1> | ||
</header> | ||
<main id="dpiat"> | ||
<article id="results"> | ||
<h2>Results</h2> | ||
</article> | ||
</main> | ||
<footer class="footer">Copyright (c) 2020 <a href="https://simonarnell.github.io/">Simon Arnell</a> - Licensed under the <a href="https://github.com/simonarnell/GDPRDPIAT/blob/master/LICENSE.md">MIT License</a></footer> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters