Customer Experience Mapping - WIP, so please ignore the attrocious coding.
xxxxxxxxxx
<html>
<meta charset="utf-8">
<title>Customer Experience Mapping</title>
<script src="jquery-1.11.0.min.js"></script>
<script src="bootstrap.min.js"></script>
<link rel="stylesheet" type="text/css" href="bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="tooltip-styles.css">
<style>
table, th, td {
border: 2px solid white;
padding:10px;
}
.axis path,
.axis line {
fill: none;
stroke: grey;
shape-rendering: crispEdges;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 5px;
}
</style>
<body>
<div class='container' id='main-container'>
<div class='content'>
<div class='container' style='font: 12px sans-serif;'>
<div class='row'>
<div class='col-md-12' >
<h1>Customer Experience Mapping</h1>
</div>
</div>
<div class='row'>
<div class='col-md-3' id="page-title">
<h2>Receive Invoice</h2>
<p id="average-rating">Average Rating 0</p>
</div>
<div class='col-md-9'>
<h2>Moments of Truth</h2>
<div id="mots"></div>
</div>
</div>
<div class='row'>
<div class='col-md-3'>
<!--<h2>Journey Overview</h2>-->
<div id="journeyoverview"></div>
</div>
<div class='col-md-9'>
<!--<h2>Steps</h2>-->
<div id="journeysteps"></div>
</div>
</div>
<div class='row'>
<div class='col-md-3'>
<!--<h2>Personas</h2>-->
<div id="personadetails">
</div>
</div>
<div class='col-md-9'>
<!--<h2>Journeys</h2>-->
<div id="journeys"></div>
</div>
</div>
<div class='row'>
<div class='col-md-3'>
<!--<h2>Averages</h2>-->
<div id="heatmapaverages"></div>
</div>
<div class='col-md-9'>
<!--<h2>Details Heatmap</h2>-->
<div id="heatmap"></div>
</div>
</div>
<div class='row'>
<div class='col-md-3'>
<!--<h2>Not used</h2>-->
<div id="notused"></div>
</div>
<div class='col-md-9'>
<!--<h2>Legend</h2>-->
<div id="heatmaplegend"></div>
</div>
</div>
</div>
</div>
</div>
<svg id="mySvg" width="80" height="80">
<defs id="mdef">
<pattern id="happy" x="0" y="0" height="20" width="20">
<image x="0" y="0" width="20" height="20" xlink:href="happy.jpg"></image>
</pattern>
<pattern id="sad" x="0" y="0" height="20" width="20">
<image x="0" y="0" width="20" height="20" xlink:href="sad.jpg"></image>
</pattern>
</defs>
</svg>
<script src="d3.min.js" charset="utf-8"></script>
<script src="colorbrewer.js" charset="utf-8"></script>
<script src="d3-tip.js" charset="utf-8"></script>
<script>
var margin = {top: 10, right: 0, bottom: 10, left: 0},
widewidth = 900 - margin.left - margin.right,
narrowwidth = 270 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom
heatheight = 130 - margin.top - margin.bottom;
var personaColor = d3.scale.ordinal()
.range(colorbrewer.Set2[6]);
var personadetails = d3.select("#personadetails").append("svg")
.attr("width", narrowwidth + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var motdetails = d3.select("#mots").append("svg")
.attr("width", widewidth + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var journeydetails = d3.select("#journeys").append("svg")
.attr("width", widewidth + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var heatmapAVG = d3.select("#heatmapaverages").append("svg")
.attr("width", narrowwidth + margin.left + margin.right)
.attr("height", heatheight + margin.top + margin.bottom);
var heatmap = d3.select("#heatmap").append("svg")
.attr("width", widewidth + margin.left + margin.right)
.attr("height", heatheight + margin.top + margin.bottom);
var heatmapLegend = d3.select("#heatmaplegend").append("svg")
.attr("width", widewidth + margin.left + margin.right)
.attr("height", heatheight + margin.top + margin.bottom);
// load ratings data
d3.csv("mots.csv", function(error, data2) {
/* var nestedMOTS = d3.nest()
.key(function(d) {return d.persona;})
.sortKeys(d3.ascending)
.entries(data2);*/
//console.log(data2);
var motwidth = widewidth/data2.length;
var mot = motdetails.selectAll("g")
.data(data2)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(" + i * motwidth + ", 0)"; });
mot.append("rect")
.attr("width", motwidth)
.attr("height", height)
.attr("rx", 20)
.attr("ry", 20)
.style("fill", "grey")
.attr("opacity", 0.1)
.style("stroke-width", 10)
.style("stroke", "white");
mot.append("foreignObject")
.attr("y", 20)
.attr("x", 20)
.attr("height", height - 40)
.attr("width", motwidth - 40)
.attr("transform", null)
.append("xhtml:div")
.attr("class", "motinfo")
.html(function(d) {
var title = d.mot;
var decision = d.decision;
var reason = d.reason;
var htmlString = "<b>" + title + "</b><br><br>" + decision + "<br><br>" + reason;
return htmlString;
})
.style("height", height - 60)
.style("width", motwidth - 60);
}); // end of loading MOTs data
//journey descriptions
d3.csv("ratingsTom.csv", function(error, data0) {
data0.sort(function (a, b) {return d3.ascending(a.step, b.step);} );
var descriptionwidth = widewidth/data0.length;
var colwidth = 100/data0.length +"%"; //%
var table = d3.select("#journeysteps")
.append("table")
.attr("width",widewidth);
var tbody = table.append("tbody");
var row = tbody.append("tr");
var columns = row.selectAll("td")
.data(data0)
.enter()
.append("td")
.attr("width", colwidth)
.attr("bgcolor", "white")
.attr("valign", "bottom")
//.attr("padding", 10)
//.attr("border", "1px solid white")
.text(function (d) {
return d.touchpoint;
});
}); //end of journey descriptions
/*d3.csv("data/personas.csv", function(error, data1) {
// sort the list of personas into alphabetical order just in case
data1.sort(function (a,b) {return d3.ascending(a.persona, b.persona);});
var personaHeight = height / data1.length;
personaColor.domain = data1.map(function (d){ return d.persona;});
var persona = personadetails.selectAll("g")
.data(data1)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0, " + i * personaHeight + ")"; });
persona.append("rect")
.attr("width", narrowwidth)
.attr("height", personaHeight)
.style("fill", function(d) {return personaColor(d.persona); });
persona.append("text")
.attr("x", 30)
.attr("y", personaHeight/2)
.text( function(d) { return d.persona + ": " + d.description ; })
.attr("dy", ".35em")
.attr("fill", "black");
persona.append("svg:image")
.attr("xlink:href", "images/happy.jpg")
.attr("y", (personaHeight/2) - 10)
.attr("width", "20")
.attr("height", "20");
});*/ //end of CSV loading persona details
// load ratings data
d3.csv("ratings.csv", function(error, data3) {
data3.forEach(function(d) {
d.step = +d.step;
d.summary = +d.summary;
});
personaColor.domain = data3.map(function (d){ return d.persona;});
var x = d3.scale.linear()
.range([0, widewidth])
.domain([.5, .5+(d3.max(data3, function(d) { return d.step; }))]);
var y = d3.scale.linear()
.range([height, 0])
.domain([-4, 4]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickFormat("")
.tickSize(0,6,0);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var journeyline = d3.svg.line()
.interpolate("cardinal")
.x(function(d) { return x(d.step); })
.y(function(d) { return y(d.summary); });
journeydetails.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + y(0) + ")")
.call(xAxis);
/*journeydetails.append("g")
.attr("class", "y axis")
.call(yAxis);*/
journeydetails.append("text")
.attr("y", y(0) -12 )
.attr("x", 5)
.attr("dy", ".71em")
.attr("class", "yaxis label")
.style("fill", "#4575B4")
.text("Positive");
journeydetails.append("text")
.attr("y", y(0) + 8)
.attr("x", 5)
.attr("dy", ".71em")
.attr("class", "yaxis label")
.style("fill", "#D73027")
.text("Negative");
var processLines = journeydetails.selectAll(".process-line")
.data(data3)
.enter()
.append("line")
.filter(function (d) {return d.persona == "Tom" ; })
.attr("class", ".process-line")
.attr("x1", function(d) { return x(d.step); })
.attr("y1", y(-4))
.attr("x2", function(d) { return x(d.step); })
.attr("y2", y(4))
.attr("stroke", "grey")
.style("stroke-width",(widewidth/11)-2)
.attr("stroke-opacity", function (d) { if (d.process === "na") {return 0.1}
else return 0.3;} );
var processLabels = journeydetails.selectAll(".process-label")
.data(data3)
.enter()
.append("text")
.filter(function(d) { return d.process != "na" })
.attr("x", function(d) { return x(d.step); })
.attr("y", y(3.5))
.text( function (d) { return d.process; } )
.attr("font-family", "sans-serif")
.attr("font-size", "15px")
.attr("fill", "DarkGray")
.attr("text-anchor", "middle")
.attr("opacity", 0.7 );
datamap = data3.map( function (d) {
return {
persona: d.persona,
step: d.step,
summary: d.summary };
});
datanest = d3.nest().key(function(d) { return d.persona; }).entries(datamap);
journeydetails.append("linearGradient")
.attr("id", "line-gradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", 0).attr("y1", y(-3.5))
.attr("x2", 0).attr("y2", y(3.5))
.selectAll("stop")
.data([
{offset: "0%", color: "#D73027"},
{offset: "40%", color: "#D73027"},
{offset: "50%", color: "#FFFFBF"},
{offset: "60%", color: "#4575B4"},
{offset: "100%", color: "#4575B4"}
])
.enter().append("stop")
.attr("offset", function(d) { return d.offset; })
.attr("stop-color", function(d) { return d.color; });
var personaJourney = journeydetails.selectAll(".personaJourney")
.data(datanest, function(d) { return d.key })
.enter().append("g")
.filter(function (d) {return d.key == "Tom" ; })
.attr("class", "personaJourney");
personaJourney.append("path")
.attr("class", "line")
.attr("d", function(d) { return journeyline(d.values); })
//.style("stroke", function(d) { return personaColor(d.key); })
.style("stroke", "url(#line-gradient)")
.style("stroke-opacity", 1)
.style("stroke-linecap", "round");
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) { return "'" + d.comment + "'"; })
personaJourney.call(tip);
var dots = personaJourney.selectAll("circle")
.data(data3)
.enter()
.append("circle")
.filter(function (d) {return d.persona == "Tom" ; })
.attr("class", "comments")
.attr("xlink:href", "#")
.attr("cx", function (d) { return x(d.step); })
.attr("cy", function (d) { return y(d.summary); })
.attr("r", function (d) { if (d.comment === "NA") {return 0}
else return 10;} )
//.style("fill", function(d) { return personaColor(d.persona); })
.style("fill", function (d) {
if (d.summary > 0) {return "url(#happy)"}
else return "url(#sad)"; })
//.style("fill", "white" )
.style("fill-opacity", 1)
.style("stroke-width", 2)
//.style("stroke", function(d) { return personaColor(d.persona); })
.style("stroke", "grey")
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
var touchpoints = personaJourney.selectAll(".touchpoints")
.data(data3)
.enter()
.append("svg:image")
.filter(function (d) {return d.persona == "Tom" ; })
.attr("xlink:href", function (d) {
var filename = d.channel + ".png";
return filename; })
.attr("class", "touchpoints")
.attr("x", function (d) { return x(d.step)-10; })
.attr("y", y(3))
.attr("width", "20")
.attr("height", "20")
.attr("opacity", "0.4");
//.attr("title", function d() {return d.channel;}) ;
touchpoints.append("title").text(function (d) {return d.channel;} );
}); // end of loading ratings data
// heatmap averages
d3.csv("ratings-tom.csv", function(error, data5) {
var averages = [
{"category":"Timeliness", "avg":0}, //0
{"category":"Competence", "avg":0}, //1
{"category":"Professionalism", "avg":0}, //2
{"category":"Proficiency", "avg":0}, //3
{"category":"Management", "avg":0}, //4
{"category":"Convenience", "avg":0}, //5
];
var timelinessAVG = d3.nest()
.key(function(d) {
return d.persona;
})
.sortKeys(d3.ascending)
.rollup(function(d){
return d3.mean(d, function(g) {
return +g.timeliness;
});
})
.entries(data5);
var competenceAVG = d3.nest()
.key(function(d) {
return d.persona;
})
.sortKeys(d3.ascending)
.rollup(function(d){
return d3.mean(d, function(g) {
return +g.competence;
});
})
.entries(data5);
var professionalismAVG = d3.nest()
.key(function(d) {
return d.persona;
})
.sortKeys(d3.ascending)
.rollup(function(d){
return d3.mean(d, function(g) {
return +g.professionalism;
});
})
.entries(data5);
var proficiencyAVG = d3.nest()
.key(function(d) {
return d.persona;
})
.sortKeys(d3.ascending)
.rollup(function(d){
return d3.mean(d, function(g) {
return +g.proficiency;
});
})
.entries(data5);
var managementAVG = d3.nest()
.key(function(d) {
return d.persona;
})
.sortKeys(d3.ascending)
.rollup(function(d){
return d3.mean(d, function(g) {
return +g.management;
});
})
.entries(data5);
var convenienceAVG = d3.nest()
.key(function(d) {
return d.persona;
})
.sortKeys(d3.ascending)
.rollup(function(d){
return d3.mean(d, function(g) {
return +g.convenience;
});
})
.entries(data5);
averages[0].avg = timelinessAVG[0].values.toFixed(2);
averages[1].avg = competenceAVG[0].values.toFixed(2);
averages[2].avg = professionalismAVG[0].values.toFixed(2);
averages[3].avg = proficiencyAVG[0].values.toFixed(2);
averages[4].avg = managementAVG[0].values.toFixed(2);
averages[5].avg = convenienceAVG[0].values.toFixed(2);
console.log(averages);
var categoriesHeight = heatheight / averages.length;
var categories = heatmapAVG.selectAll("g")
.data(averages)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0, " + i * categoriesHeight + ")"; });
categories.append("rect")
.attr("width", narrowwidth)
.attr("height", categoriesHeight)
.style("fill", "grey")
.style("opacity", 0.1)
.style("stroke-width", 1)
.style("stroke", "#FFF");
categories.append("text")
.attr("x", 15)
.attr("y", categoriesHeight/2)
.text( function(d) { return d.category; })
.attr("dy", ".35em")
.attr("fill", "black");
categories.append("text")
.attr("x", 120)
.attr("y", categoriesHeight/2)
.text( function(d) { return "(avg " + d.avg + ")"; })
.attr("dy", ".35em")
.attr("fill", "black");
}); //end of heatmap averages
// load heatmap data
d3.csv("ratings-tom.csv", function(error, data4) {
data4.forEach(function(d) {
d.step = +d.step;
});
var categories = ["timeliness", "competence", "professionalism", "proficiency", "management", "convenience"];
//console.log(categories);
var heatx = d3.scale.linear()
.range([0, widewidth])
.domain([.5, .5+(d3.max(data4, function(d) { return d.step; }))]);
var heaty = d3.scale.ordinal()
.domain(categories)
.range([0, 1, 2, 3, 4, 5, 6]);
var colorbox = d3.scale.ordinal()
.range(["#D73027", "#FC8D59", "#FEE090", "#FFFFBF", "#E0F3F8", "#91BFDB", "#4575B4"]) //colorbrewer Red Yellow Blue
.domain(["-3", "-2", "-1", "0", "1", "2", "3"]);
// draw timeliness boxes - x, y, width, height
var BOXWIDTH = widewidth/11;
var BOXHEIGHT = heatheight/6;
var BOXRADIUS = 0;
var BOXFILTER = "Tom";
var BOXX = function (d) { return heatx(d.step) - BOXWIDTH/2; };
var BOXSTROKEWIDTH = 1;
var BOXSTROKE = "#FFF";
var BOXFILL = function (d) {return colorbox(d.timeliness);};
var boxesTimeliness = heatmap.selectAll(".timelinessBoxes")
.data(data4)
.enter()
.append("rect")
.filter(function (d) {return d.persona = BOXFILTER ; } )
.attr("x", BOXX )
.attr("y", (BOXHEIGHT * heaty("timeliness")) )
.attr("width", BOXWIDTH)
.attr("height", BOXHEIGHT)
.attr("rx", BOXRADIUS)
.attr("ry", BOXRADIUS)
.style("stroke-width", BOXSTROKEWIDTH)
.style("stroke", BOXSTROKE)
.style("fill", function (d) {return colorbox(d.timeliness);})
.append("svg:title")
.text(function(d) { return d.timeliness; });
var boxesCompetence = heatmap.selectAll(".competenceBoxes")
.data(data4)
.enter()
.append("rect")
.filter(function (d) {return d.persona = BOXFILTER ; } )
.attr("x", BOXX )
.attr("y", (BOXHEIGHT * heaty("competence")) )
.attr("width", BOXWIDTH)
.attr("height", BOXHEIGHT)
.attr("rx", BOXRADIUS)
.attr("ry", BOXRADIUS)
.style("stroke-width", BOXSTROKEWIDTH)
.style("stroke", BOXSTROKE)
.style("fill", function (d) {return colorbox(d.competence);})
.append("svg:title")
.text(function(d) { return d.timeliness; });
var boxesprofessionalism = heatmap.selectAll(".professionalismBoxes")
.data(data4)
.enter()
.append("rect")
.filter(function (d) {return d.persona = BOXFILTER ; } )
.attr("x", BOXX )
.attr("y", (BOXHEIGHT * heaty("professionalism")) )
.attr("width", BOXWIDTH)
.attr("height", BOXHEIGHT)
.attr("rx", BOXRADIUS)
.attr("ry", BOXRADIUS)
.style("stroke-width", BOXSTROKEWIDTH)
.style("stroke", BOXSTROKE)
.style("fill", function (d) {return colorbox(d.professionalism);})
.append("svg:title")
.text(function(d) { return d.timeliness; });
var boxesProficiency = heatmap.selectAll(".proficiencyBoxes")
.data(data4)
.enter()
.append("rect")
.filter(function (d) {return d.persona = BOXFILTER ; } )
.attr("x", BOXX )
.attr("y", (BOXHEIGHT * heaty("proficiency")) )
.attr("width", BOXWIDTH)
.attr("height", BOXHEIGHT)
.attr("rx", BOXRADIUS)
.attr("ry", BOXRADIUS)
.style("stroke-width", BOXSTROKEWIDTH)
.style("stroke", BOXSTROKE)
.style("fill", function (d) {return colorbox(d.proficiency);})
.append("svg:title")
.text(function(d) { return d.timeliness; });
var boxesManagement = heatmap.selectAll(".managementBoxes")
.data(data4)
.enter()
.append("rect")
.filter(function (d) {return d.persona = BOXFILTER ; } )
.attr("x", BOXX )
.attr("y", (BOXHEIGHT * heaty("management")) )
.attr("width", BOXWIDTH)
.attr("height", BOXHEIGHT)
.attr("rx", BOXRADIUS)
.attr("ry", BOXRADIUS)
.style("stroke-width", BOXSTROKEWIDTH)
.style("stroke", BOXSTROKE)
.style("fill", function (d) {return colorbox(d.management);})
.append("svg:title")
.text(function(d) { return d.timeliness; });
var boxesConvenience = heatmap.selectAll(".convenienceBoxes")
.data(data4)
.enter()
.append("rect")
.filter(function (d) {return d.persona = BOXFILTER ; } )
.attr("x", BOXX )
.attr("y", (BOXHEIGHT * heaty("convenience")) )
.attr("width", BOXWIDTH)
.attr("height", BOXHEIGHT)
.attr("rx", BOXRADIUS)
.attr("ry", BOXRADIUS)
.style("stroke-width", BOXSTROKEWIDTH)
.style("stroke", BOXSTROKE)
.style("fill", function (d) {return colorbox(d.convenience);})
.append("svg:title")
.text(function(d) { return d.timeliness; });
/*var categoryLabels = heatmap.selectAll(".categoryLabels")
.data(categories)
.enter()
.append("text")
.attr("x", 0)
.attr("y", function (d) {return ((heatheight/6) * heaty(d)) + (heatheight/12); } )
.text( function (d) { return d; } )
.attr("font-family", "sans-serif")
.attr("font-size", "14px")
.attr("fill", "black")
.attr("text-anchor", "start")
.attr("opacity", 0.7 );*/
var heatLegend = heatmapLegend.selectAll(".heatLegend")
.data(colorbox.domain())
.enter()
.append("rect")
.attr("class", "heatLegend")
//.attr("x", 0)
//.attr("y", function (d, i) {return ((heatheight/7) * i);} )
.attr("x", function (d, i) {return (-10 * i) + 200; })
.attr("y", 0 )
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d) {return colorbox(d);});
heatmapLegend.append("text")
.attr("y", 3)
.attr("x", 200 + 12)
.attr("dy", ".71em")
.attr("class", "legend label")
.style("fill", colorbox(-3))
.text("Negative");
heatmapLegend.append("text")
.attr("y", 3)
.attr("x", 200 - 62)
.attr("dy", ".71em")
.attr("class", "legend label")
.attr("text-anchor", "end")
.style("fill", colorbox(3))
.text("Positive");
heatmapLegend.append("text")
.attr("y", 3)
.attr("x", 90)
.attr("dy", ".71em")
.attr("class", "legend label")
.attr("text-anchor", "end")
.style("fill", "grey")
.text("Legend:");
});// end of load heatmap data
</script>