Customer Experience Mapping with transitions. Uses Brush and Transitions to allow you select different views and zoom in.
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;
}
.brush .extent {
stroke: #fff;
fill-opacity: .125;
shape-rendering: crispEdges;
}
.resize path {
fill: #666;
fill-opacity: .8;
stroke: #000;
stroke-width: 1.5px;
}
.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-6'>
<h2>Select day or steps view</h2>
<div id="selection1"></div>
<form id="selectview">
<label><input class = "formRadio" type="radio" name="mode" value="steps" checked> Steps</label>
<label><input class = "formRadio" type="radio" name="mode" value="days"> Days</label>
</form>
</div>
<div class='col-md-6'>
<h2>Hide/Show Channels</h2>
<div id="selection2"></div>
<form id="showhidechannels">
<label><input class = "formCheckbox" type="checkbox" name="channels"> Show Channels</label>
</form>
</div>
</div>
<div class='row'>
<div class='col-md-12'>
<h2>Journeys</h2>
<div id="journeys"></div>
</div>
</div>
<div class='row'>
<div class='col-md-12'>
<div id="journeysteps"></div>
</div>
</div>
<div class='row'>
<div class='col-md-12'>
<h2>Zoom</h2>
<div id="context"></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: 20, right: 50, bottom: 20, left: 50},
widewidth = 1200 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom
contextheight = 100 - margin.top - margin.bottom;
var personaColor = d3.scale.ordinal()
.range(colorbrewer.Set2[6]);
var selectedOption = "Steps";
var svg1 = d3.select("#journeys").append("svg")
.attr("width", widewidth + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var journeydetails = svg1.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var svg2 = d3.select("#context").append("svg")
.attr("class", "context")
.attr("width", widewidth + margin.left + margin.right)
.attr("height", contextheight + margin.top + margin.bottom);
var context = svg2.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// load ratings data
d3.csv("ratingsTom.csv", function(error, data3) {
data3.forEach(function(d) {
d.step = +d.step;
d.day = +d.day;
d.summary = +d.summary;
});
var maxDay = d3.max(data3, function(d) { return d.day; });
var allDayNumbers = [];
for (var i = 1; i < maxDay+1; i++) {
var num = +i;
allDayNumbers.push(num);
};
console.log(allDayNumbers);
var processwidth2 = function () {
var newWidth = 0;
var gap = 2, // pixels gap between the bars when Steps is chosen
dayWidth = 5, // pixels width for visible bars when Days is chosen
extent = brush.extent();
var selectedWidth = extent[1] - extent[0];
// when "Steps" is selected, the width is proportional to the number of steps (data3.length), and the extent of the brush (selectedWidth)
if (selectedOption === "Steps") { newWidth = ((widewidth/data3.length) * (data3.length/selectedWidth))- gap }
else newWidth = dayWidth;
return newWidth;
};
processwidth2; // initialise the width, which will based on Steps
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; }))]);
// x for brush context
var x2 = d3.scale.linear();
x2.range(x.range());
x2.domain(x.domain());
var xDay = d3.scale.linear()
.range([0, widewidth])
.domain([1, d3.max(data3, function(d) { return d.day; })]);
var brush = d3.svg.brush()
.x(x2)
.extent(x2.domain())
.on("brush", brushed);
var arc = d3.svg.arc()
.outerRadius(contextheight / 4)
.startAngle(0)
.endAngle(function(d, i) { return i ? -Math.PI : Math.PI; });
var y = d3.scale.linear()
.range([height, 0])
.domain([-4, 4]);
var y2 = d3.scale.linear()
.range([contextheight, 0]);
y2.domain(y.domain());
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("linear")
.x(function(d) { return x(d.step); })
.y(function(d) { return y(d.summary); });
var contextline = d3.svg.line()
.interpolate("linear")
.x(function(d) { return x2(d.step); })
.y(function(d) { return y2(d.summary); });
//.tension(0);
/*journeydetails.append("g")
.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("text-anchor", "end")
.attr("class", "yaxis label")
.style("fill", "#4575B4")
.text("Positive");
journeydetails.append("text")
.attr("y", y(0) + 8)
.attr("x", -5)
.attr("dy", ".71em")
.attr("text-anchor", "end")
.attr("class", "yaxis label")
.style("fill", "#D73027")
.text("Negative");
var processLines = journeydetails.selectAll(".process-line")
.data(data3)
.enter()
.append("line")
.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",processwidth2)
.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("class", "process-label")
.attr("x", function(d) { return x(d.step); })
.attr("y", -8)
.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 );
journeydetails.append("line")
.attr("class", "x axis")
.attr("x1", x(0) - margin.left)
.attr("y1", y(0))
.attr("x2", widewidth )
.attr("y2", y(0))
.attr("stroke", "grey")
.style("stroke-width",1);
datamap = data3.map( function (d) {
return {
persona: d.persona,
step: d.step,
day: d.day,
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")
.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 contextJourney = context.selectAll(".contextJourney")
.data(datanest, function(d) { return d.key })
.enter().append("g")
.attr("class", "contextJourney");
contextJourney.append("path")
.attr("class", "line")
.attr("d", function(d) { return contextline(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")
.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 5}
else return 10;} )
//.style("fill", function(d) { return personaColor(d.persona); })
.style("fill", function (d) {
if (d.comment === "NA") { return "white" }
if (d.summary > 0) {return "url(#happy)"}
else return "url(#sad)"; })
.style("fill-opacity", 1)
.style("stroke-width", 2)
//.style("stroke", function(d) { return personaColor(d.persona); })
.style("stroke", "grey")
.on('mouseout', tip.hide);
dots.filter(function (d) {return d.comment != "NA" ; })
.on('mouseover', tip.show);
var channelsopacity = 0;
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", channelsopacity);
//.attr("title", function d() {return d.channel;}) ;
touchpoints.append("title").text(function (d) {return d.channel;} );
var dayNumbers = personaJourney.selectAll(".dayNumbers")
.data(allDayNumbers)
.enter()
.append("text")
.attr("class", "dayNumbers")
.attr("x", function(d) { return x(d); })
.attr("y", height + 15)
.text( function (d) { return d; } )
.attr("font-family", "sans-serif")
.attr("font-size", "15px")
.attr("fill", "DarkGray")
.attr("text-anchor", "middle")
.attr("opacity", 0 );
// form controls and transitions
var t = 750; //duration time
var processwidth = (widewidth/data3.length)-2;
var processopacity = 0.1;
var daysopacity = 0;
d3.selectAll(".formRadio").on("change", change);
function change() {
brush.extent(x2.domain());
brushg.call(brush);
if (this.value === "steps") {
selectedOption = "Steps";
journeyline.x(function(d) { return x(d.step); });
contextline.x(function(d) { return x2(d.step); });
x.domain([.5, .5+(d3.max(data3, function(d) { return d.step; }))]);
processwidth = (widewidth/data3.length)-2;
processopacity = 0.1;
daysopacity = 0;
} else {
selectedOption = "Days";
journeyline.x(function(d) { return x(d.day); });
contextline.x(function(d) { return x2(d.day); });
x.domain([.5, .5+(d3.max(data3, function(d) { return d.day; }))]);
processwidth = 5;
processopacity = 0;
daysopacity = 0.7;
}
x2.domain(x.domain())
/*d3.select('#context').transition()
.duration(t)
.call(brush.extent(x2.domain()))
.call(brush.event);
brushg.call(brush);*/
journeyline.x(xvalue);
/*journeydetails.select(".xaxis")
.call(xAxis); */
journeydetails.selectAll("circle")
.transition().duration(t)
.attr("cx", xvalue);
journeydetails.selectAll(".process-line")
.transition().duration(t)
.style("stroke-width",processwidth)
.attr("x1", xvalue)
.attr("x2", xvalue)
.attr("stroke-opacity", function (d) { if (d.process === "na") {return processopacity}
else return 0.3;} );
journeydetails.selectAll(".process-label")
.transition().duration(t)
.attr("x", xvalue);
journeydetails.selectAll("path")
.transition().duration(t)
.attr("d", function(d) { return journeyline(d.values); });
journeydetails.selectAll(".touchpoints")
.transition().duration(t)
.attr("x", function (d) {
if (selectedOption === "Steps") {return x(d.step) - 10;}
else return x(d.day) - 10;
});
contextJourney.selectAll("path")
.transition().duration(t)
.attr("d", function(d) { return contextline(d.values); });
//animate the days differently
if (this.value === "steps") {
journeydetails.selectAll(".dayNumbers")
.transition().duration(t)
.attr("opacity", daysopacity )
.transition().duration(0)
.attr("x", function(d) { return x(d); });
} else {
journeydetails.selectAll(".dayNumbers")
.transition().duration(0)
.attr("x", function(d) { return x(d); })
.transition().duration(150).delay(t)
.attr("opacity", daysopacity );
}
}
// function to return the X value depending if Steps or Days is selected
var xvalue = function (d) {
var returnValue;
if (selectedOption === "Steps") { returnValue = x(d.step) }
else returnValue = x(d.day);
return returnValue;
//return d.day;
};
// end of form controls
// brush controls
var brushg = context.append("g")
.attr("class", "brush")
.call(brush);
brushg.selectAll(".resize").append("path")
.attr("transform", "translate(0," + contextheight / 2 + ")")
.attr("d", arc);
brushg.selectAll("rect")
.attr("height", contextheight);
function brushed() {
x.domain(brush.empty() ? x2.domain() : brush.extent());
personaJourney.select(".line").attr("d", function(d) { return journeyline(d.values); });
personaJourney.selectAll("circle").attr("cx", xvalue);
journeydetails.selectAll(".process-line")
.attr("x1", xvalue)
.attr("x2", xvalue)
.style("stroke-width",processwidth2);
journeydetails.selectAll(".process-label").attr("x", xvalue);
journeydetails.selectAll(".dayNumbers").attr("x", function(d) { return x(d); });
journeydetails.selectAll(".touchpoints")
.attr("x", function (d) {
if (selectedOption === "Steps") {return x(d.step) - 10;}
else return x(d.day) - 10;
});
//focus.select(".x.axis").call(xAxis);
}
// end of brush controls
d3.selectAll(".formCheckbox").on("change", changeChannelsVisibility);
function changeChannelsVisibility () {
console.log(this.value);
if (this.checked == true) {
channelsopacity = 0.4;
} else {
channelsopacity = 0;
}
touchpoints.transition().attr("opacity", channelsopacity);
};
}); // end of loading ratings data
</script>