D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
tomshanley
Full window
Github gist
Customer Experience Mapping
Customer Experience Mapping - WIP, so please ignore the attrocious coding.
<!DOCTYPE html> <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>