Built with blockbuilder.org
xxxxxxxxxx
<head>
<title>Likert Scale DataViz</title>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://d3js.org/queue.v1.min.js"></script>
<style>
/* style pour les glyphes */
.line {
fill: none;
stroke: #808080;
stroke-width: 2px;
}
.meansLine {
fill: none;
stroke: #0890e6;
stroke-width: 2px;
}
/* style pour le tooltip */
.hidden {
display: none;
}
div.tooltip {
color: #222;
background-color: #fff;
padding: .5em;
text-shadow: #f5f5f5 0 1px 0;
border-radius: 2px;
border: solid #222 1px;
opacity: 0.9;
position: absolute;
}
</style>
</head>
<body>
<script>
// TODO : add DIV for x axis
// mise en page
var margin = {top: 50, right: 70, bottom: 30, left: 70},
width = 1000 - margin.left - margin.right,
height = 5000 - margin.top - margin.bottom;
// nombre de choix de l'echelle
var questionScale = 5;
// tooltip
var tooltip = d3.select('body')
.append('div')
.attr('class', 'hidden tooltip');
// chargement asynchrone des données
queue()
// les CSV n'en sont pas le caractere delimiteur est '\t'
.defer(d3.tsv, "data_truncated_30.csv")
.await(processData);
function processData(error, data) {
if (error) throw error;
var transitionDuration = 100;
var headerNames = d3.keys(data[0]);
var questionNames = [];
var first = true;
var dataset = {};
var means = [];
var map = {};
// un objet pour decrire les points des glyphes
function GlyphPoint() {
};
GlyphPoint.prototype.toString = function () {
return this.question + ":" + this.level
};
// extraction des questions
for (var d = 1; d < data.length; d++) {
var res = [];
var regex = /[A-Z][0-9]{1,2}/;
for (var j = 0; j < headerNames.length; j++) {
var response = new GlyphPoint();
if (headerNames[j].match(regex) && data[d][headerNames[j]] !== undefined
&& !isNaN(data[d][headerNames[j]])
&& +data[d][headerNames[j]] <= questionScale) {
// mise en forme des donnees pour la représentation graphique
response["question"] = headerNames[j];
response["level"] = data[d][headerNames[j]];
res.push(response);
// recuperation des identifiants des colonnes correspondants a des questions
if (first) {
questionNames.push(headerNames[j]);
}
if (means[j]) {
means[j] += +data[d][headerNames[j]];
}
else {
means.push(+data[d][headerNames[j]]);
}
/*if (means[headerNames[j]]) {
means[headerNames[j]] = +means[headerNames[j]] + +data[d][headerNames[j]];
}
else {
means[headerNames[j]] = data[d][headerNames[j]];
}*/
// on map les points
/*if ((response.toString()) in map) {
var crossingGlyphId = map[response.toString()];
crossingGlyphId.push(d);
map[response.toString()] = crossingGlyphId;
}
else {
map[response.toString()] = [d];
}*/
}
else {
break;
}
}
first = false;
dataset[d] = res;
}
// calcul des moyennes
for (var m in means) {
means[m] = +means[m] / data.length;
}
var color = d3.scaleQuantize()
.domain([0, questionScale])
.range(['#d73027', '#fc8d59', '#fee08b', '#d9ef8b', '#91cf60', '#1a9850']);
// l'echelle des questions
var xScale = d3.scaleLinear()
.domain([0, questionScale])
.range([0, width]);
// l'echelle des reponses
var yScale = d3.scalePoint()
.domain(questionNames)
.range([0, height]);
yScale.invert = (function () {
var domain = yScale.domain()
var range = yScale.range()
var scale = d3.scaleQuantile().domain(range).range(domain)
return function (y) {
return scale(y)
}
})();
// l'axe des questions
var xAxis = d3.axisTop(xScale)
.tickSize(8);
// l'axe des reponses
var yAxis = d3.axisLeft(yScale)
.tickSize(6);
// placement de l'image
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xAxisG = g.append("g")
.attr("id", "xAxis")
.call(xAxis.ticks(questionScale));
/*
xAxisG.selectAll('.tick').each(function (d) {
d3.select(this).selectAll("text")
.attr("fill", color(+d));
});
*/
var yAxisG = g.append("g")
.attr("id", "yAxis")
.call(yAxis);
// legende de l'axe X
g.append("text")
.attr("id", "xAxisLegend")
.attr("y", -30)
.attr("x", width / 2)
.attr("dy", "0.1em")
.style("text-anchor", "middle")
.text("Likert Scale");
// legende de l'axe Y
g.append("text")
.attr("id", "yAxisLegend")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x", 0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Question");
// fonction pour tracer un glyphe
var line = d3.line()
.x(function (data) {
return xScale(data.level);
})
.y(function (data) {
return yScale(data.question);
});
var meansLine = d3.line()
.x(function (data) {
return xScale(data);
})
.y(function (data, i) {
return yScale(headerNames[i]);
});
// fonction pour l'opacite
var opacity = function () {
var n = (1 / data.length * questionScale);
return Math.max(0.01, n);
};
// une variable pour la selection
var selection = [];
var lastSelectionCoords = {};
// tracer les différents glyphes
for (var i = 1; i < data.length; i++) {
g.append("path")
.data([dataset[i]])
.attr("class", "line")
.attr("id", i)
.style("opacity", opacity())
.attr("d", line);
}
g.append("path")
.data([means])
.attr("class", "meansLine")
.attr("id", -1)
.attr("d", meansLine);
// tracer les points
for (var i = 1; i < data.length; i++) {
for (var j = 0; j < questionNames.length; j++) {
// ne dessiner qu'un cercle par point
var existingCircle = g.selectAll("circle")
.filter(function (d) {
return d.level == dataset[i][j].level && d.question == dataset[i][j].question;
})
.style("opacity", function () {
return +this.style.opacity + opacity();
})
.attr("glyphs", function () {
var glyphs = JSON.parse(this.getAttribute("glyphs"));
glyphs.push(i);
return JSON.stringify(glyphs);
});
if (existingCircle.empty()) {
var circle = g.append("circle")
.data([dataset[i][j]])
.attr("id", dataset[i][j].level + ";" + dataset[i][j].question)
.attr("selected", false)
// les glyphes contenant le point
.attr("glyphs", JSON.stringify([i]))
.attr("r", 5)
.attr("cx", function () {
return xScale(dataset[i][j].level);
})
.attr("cy", function () {
return yScale(dataset[i][j].question);
})
.style("stroke", color(dataset[i][j].level))
.style("stroke-width", "2px")
.style("cursor", "pointer")
.style("fill", "#fff")
.style("opacity", opacity())
.style("fill-opacity", 1)
.on("mouseover", function () {
var x = this.getAttribute("cx");
var y = this.getAttribute("cy");
var glyphs = JSON.parse(this.getAttribute("glyphs"));
// mettre en valeur la legende
xAxisG.selectAll('.tick').each(function (d, i) {
if (+d == xScale.invert(x)) {
d3.select(this).selectAll("text")
.transition().duration(transitionDuration)
.style("fill", color(+d))
.style("font-size", "21px");
}
});
yAxisG.selectAll('.tick').each(function (d, i) {
if (d == yScale.invert(y)) {
d3.select(this).selectAll("text")
.transition().duration(transitionDuration)
.style("fill", "#2547b2")
.style("font-size", "21px");
}
});
var containingGlyphs = JSON.parse(this.getAttribute("glyphs"));
// s'il existe pas deja un selection
if (selection.length <= 0) {
// mise en valeur du glyohe selectionne
g.selectAll(".line")
.style("opacity", function () {
return (containingGlyphs.includes(+this.id)) ? 1 : 0.1;
});
// mise en valeur des cercles
g.selectAll("circle")
.style("opacity", function () {
var glyphs = JSON.parse(this.getAttribute("glyphs"));
for (var i in glyphs) {
if (containingGlyphs.includes(glyphs[i])) {
return 1;
}
}
return 0.1;
});
}
else {
// TODO : pre-visualize restrained glyph
}
if (this.getAttribute("selected") == 'false') {
// mise en valeur de la selection
d3.select(this)
.transition().duration(transitionDuration)
.attr("r", 5 + (containingGlyphs.length / data.length) * 15);
}
// mise en place du tooltip
tooltip.classed('hidden', false)
.attr('style', 'left:' + (+x + margin.left + 15) +
'px; top:' + (+y + margin.top - 47) + 'px')
.html(function() {
var l = 5;
var s = glyphs.length + " / " + data.length + "<br>" + "[";
s += " ";
for (var i = 0; i < Math.min(glyphs.length, l); i++) {
s += glyphs[i] + " ";
}
if (glyphs.length > l) {
s += "... "
}
s += "]";
return s;
});
})
.on("mouseout", function () {
var x = this.getAttribute("cx");
var y = this.getAttribute("cy");
// cacher la tooltip
tooltip.classed('hidden', true);
// TODO : fix legend color reset
// reset de la legende
xAxisG.selectAll('.tick').each(function (d, i) {
if (d == xScale.invert(x)) {
d3.select(this).selectAll("text")
.style("fill", "black")
.transition().duration(transitionDuration)
.style("font-size", "10px");
}
});
yAxisG.selectAll('.tick').each(function (d, i) {
if (d == yScale.invert(y)) {
d3.select(this).selectAll("text")
.style("fill", "black")
.transition().duration(transitionDuration)
.style("font-size", "10px");
}
});
if (selection.length <= 0) {
g.selectAll(".line")
.style("opacity", opacity())
.style("stroke", "grey")
.style("stroke-width", "2px");
;
g.selectAll("circle")
.style("fill", "#fff")
.style("opacity", function () {
return JSON.parse(this.getAttribute("glyphs")).length * opacity();
});
}
else {
// TODO : clean pre-visualisation
}
if (this.getAttribute("selected") == 'false') {
// recoloration par defaut
d3.select(this)
.transition().duration(transitionDuration)
.attr("r", 5);
}
})
.on("click", function (d) {
var x = this.getAttribute("cx");
var containingGlyphs = JSON.parse(this.getAttribute("glyphs"));
if (this.getAttribute("selected") == 'false') {
if (selection.length > 0) {
var matchingGlyph = [];
var notMatchingGlyph = [];
for (var i in selection) {
if (containingGlyphs.indexOf(selection[i]) > -1) {
matchingGlyph.push(selection[i]);
}
else {
notMatchingGlyph.push(selection[i]);
}
}
// pas de match
if (matchingGlyph.length <= 0) {
selection = containingGlyphs;
// nettoyage de l'ancienne selection
g.selectAll(".line")
.style("opacity", function () {
return (containingGlyphs.includes(+this.id)) ? 1 : 0.1;
});
g.selectAll("circle")
.attr("r", 5)
.style("opacity", function () {
var glyphs = JSON.parse(this.getAttribute("glyphs"));
for (var i in glyphs) {
if (containingGlyphs.includes(glyphs[i])) {
return 1;
}
}
this.setAttribute("selected", false);
return 0.1;
})
.style("fill", "#fff");
}
// il y a un match
else {
selection = matchingGlyph;
g.selectAll(".line")
.filter(function () {
return notMatchingGlyph.includes(+this.id);
})
.style("opacity", 0.1);
g.selectAll("circle")
.style("opacity", function () {
var glyphs = JSON.parse(this.getAttribute("glyphs"));
for (var i in glyphs) {
if (matchingGlyph.includes(glyphs[i])) {
return 1;
}
}
this.setAttribute("selected", false);
return 0.1;
});
}
}
else {
selection = containingGlyphs;
}
d3.select(this)
.attr("r", 5 + (containingGlyphs.length / data.length) * 10)
.style("opacity", 1)
.style("fill", color(+xScale.invert(x)))
.attr("selected", true);
}
// si deja selectionner
else {
d3.select(this)
.attr("selected", false);
var glyphs = [];
d3.selectAll("circle")
.filter(function () {
return this.getAttribute("selected") == 'true';
})
.each(function () {
glyphs.push(JSON.parse(this.getAttribute("glyphs")))
});
var restrainedGlyph = [];
if (glyphs.length > 0) {
restrainedGlyph = glyphs.reduce(function (a, b) {
res = [];
for (var i in a) {
if (b.includes(a[i]) && !res.includes(a[i])) {
res.push(a[i]);
}
}
return res;
});
}
if (restrainedGlyph.length > 0) {
// mettre a jour la selection des glyphes
g.selectAll(".line")
.style("opacity", function () {
return (restrainedGlyph.includes(+this.id)) ? 1 : 0.1;
});
// mettre a jour la selection des points
d3.select(this)
.attr("r", 5)
.style("fill", "#fff");
g.selectAll("circle")
.style("opacity", function () {
var glyphs = JSON.parse(this.getAttribute("glyphs"));
for (var i in glyphs) {
if (restrainedGlyph.includes(glyphs[i])) {
return 1;
}
}
return 0.1;
});
}
else {
// coloration par defaut
g.selectAll(".line")
.style("opacity", opacity())
.style("stroke", "grey")
.style("stroke-width", "2px");
;
g.selectAll("circle")
.attr("r", 5)
.style("opacity", function () {
return JSON.parse(this.getAttribute("glyphs")).length * opacity();
})
.style("fill", "#fff")
.attr("selected", false);
}
selection = restrainedGlyph;
}
// mise a jour des coordonnees de selection
lastSelectionCoords.x = d3.event.pageX;
lastSelectionCoords.y = d3.event.pageY;
console.log("click on " + d + " coords " + d3.event.pageX + ";" + d3.event.pageY);
// TODO : emphasis on the selected line
});
}
}
}
svg.on("click", function () {
if (d3.event.pageX != lastSelectionCoords.x || d3.event.pageY != lastSelectionCoords.y) {
console.log("click in empty area" + " coords " + d3.event.pageX + ";" + d3.event.pageY);
selection = [];
lastSelectionCoords = {};
// coloration par defaut
g.selectAll(".line")
.style("opacity", opacity())
.style("stroke", "grey")
.style("stroke-width", "2px");
g.selectAll("circle")
.attr("r", 5)
.style("fill", "#fff")
.style("fill-opacity", 1)
.style("opacity", function () {
return JSON.parse(this.getAttribute("glyphs")).length * opacity();
})
.attr("selected", false);
}
});
}
</script>
</body>
Modified http://d3js.org/queue.v1.min.js to a secure url
https://d3js.org/d3.v4.js
https://d3js.org/queue.v1.min.js