Skip to content

Instantly share code, notes, and snippets.

@logycon
Last active August 29, 2015 14:01
Show Gist options
  • Save logycon/274cfbe3a1e14acbd4b4 to your computer and use it in GitHub Desktop.
Save logycon/274cfbe3a1e14acbd4b4 to your computer and use it in GitHub Desktop.
Opportunity Map
<html>
<head>
<title>Opportunity Map</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.5/d3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/themes/smoothness/jquery-ui.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js"></script>
<script src="opportunitymap.js"></script>
<style>
body {
font: 12px 'Lucida Grande', 'Lucida Sans Unicode', Helvetica, Arial, Verdana, sans-serif;
}
path {
stroke: steelblue;
stroke-width: 2;
fill: none;
}
.axis path, .axis line {
fill: none;
shape-rendering: crispEdges;
stroke: grey;
stroke-width: 1;
}
.y .tick {
stroke: #ddd;
stroke-width: 0;
stroke-dasharray: 3;
}
.x .tick {
stroke: #ddd;
stroke-width: 0;
}
.axis .grid-line {
stroke: black;
shape-rendering: crispEdges;
stroke-opacity: .2;
}
.axis .grid-line-y {
stroke: black;
shape-rendering: crispEdges;
stroke-opacity: .2;
}
.axis .grid-line-x {
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-size:10px;
}
.importance {
fill: white;
}
</style>
</head>
<body>
<div id="chart" style="width:100%; margin:auto;">
<div id="chartDiv" style=" width:500px; height:400px; min-height: 400px;">
</div>
</div>
<script type="text/javascript">
var data = [{ "id": "qh01_10", "text": "Lowest running costs (e.g., ink/toner, energy usage, etc.)", "ri": 140.53316889227148, "attrIndex": "10", "columns": [{ "caption": "BROTHER", "value": 46.498277841561425 }, { "caption": "CANON", "value": 50.959629384513569 }, { "caption": "EPSON", "value": 48.355263157894733 }, { "caption": "HP", "value": 58.925831202046034 }, { "caption": "KONICA MINOLTA", "value": 39.94252873563218 }, { "caption": "KYOCERA MITA", "value": 43.478260869565219 }, { "caption": "LENOVO", "value": 53.393665158371043 }, { "caption": "LEXMARK", "value": 38.323353293413177 }, { "caption": "RICOH GROUP", "value": 46.50112866817156 }, { "caption": "SAMSUNG", "value": 53.003533568904594 }, { "caption": "SHARP", "value": 36.486486486486484 }, { "caption": "TOSHIBA", "value": 51.149425287356323 }, { "caption": "XEROX", "value": 44.498186215235791 }, { "caption": "Professional/Specialty", "value": 43.918918918918919 }, { "caption": "Neighborhood Shops", "value": 32.38095238095238 }, { "caption": "Online Printing", "value": 43.137254901960787 }] }, { "id": "qh01_11", "text": "Lowest price per page for printing", "ri": 139.40147789386282, "attrIndex": "11", "columns": [{ "caption": "BROTHER", "value": 45.600991325898391 }, { "caption": "CANON", "value": 48.491083676268858 }, { "caption": "EPSON", "value": 46.453407510431155 }, { "caption": "HP", "value": 53.981328940142781 }, { "caption": "KONICA MINOLTA", "value": 40.384615384615387 }, { "caption": "KYOCERA MITA", "value": 39.268292682926834 }, { "caption": "LENOVO", "value": 53.977272727272727 }, { "caption": "LEXMARK", "value": 33.195020746887963 }, { "caption": "RICOH GROUP", "value": 45.913461538461533 }, { "caption": "SAMSUNG", "value": 50.653983353151013 }, { "caption": "SHARP", "value": 33.480176211453745 }, { "caption": "TOSHIBA", "value": 54.285714285714285 }, { "caption": "XEROX", "value": 40.46094750320102 }, { "caption": "Professional/Specialty", "value": 41.914191419141915 }, { "caption": "Neighborhood Shops", "value": 36.655948553054664 }, { "caption": "Online Printing", "value": 41.218637992831539 }] }, { "id": "qh01_4", "text": "Easy to set up and get started", "ri": 138.53575866602085, "attrIndex": "4", "columns": [{ "caption": "BROTHER", "value": 56.338028169014088 }, { "caption": "CANON", "value": 70.795306388526726 }, { "caption": "EPSON", "value": 65.791245791245785 }, { "caption": "HP", "value": 80.822622107969153 }, { "caption": "KONICA MINOLTA", "value": 49.462365591397848 }, { "caption": "KYOCERA MITA", "value": 51.538461538461533 }, { "caption": "LENOVO", "value": 63.46153846153846 }, { "caption": "LEXMARK", "value": 49.805447470817121 }, { "caption": "RICOH GROUP", "value": 56.03644646924829 }, { "caption": "SAMSUNG", "value": 64.483906770255274 }, { "caption": "SHARP", "value": 46.443514644351467 }, { "caption": "TOSHIBA", "value": 57.847533632286996 }, { "caption": "XEROX", "value": 61.169590643274852 }, { "caption": "Professional/Specialty", "value": 55.183413078149925 }, { "caption": "Neighborhood Shops", "value": 45.701357466063349 }, { "caption": "Online Printing", "value": 50.927487352445191 }] }, { "id": "qh01_31", "text": "All of the things I need are in one device (can scan, copy, print, fax, etc. all in one)", "ri": 137.7032629903826, "attrIndex": "31", "columns": [{ "caption": "BROTHER", "value": 64.397321428571431 }, { "caption": "CANON", "value": 71.572962728995577 }, { "caption": "EPSON", "value": 65.298013245033118 }, { "caption": "HP", "value": 79.927007299270073 }, { "caption": "KONICA MINOLTA", "value": 55.633802816901415 }, { "caption": "KYOCERA MITA", "value": 60.839160839160847 }, { "caption": "LENOVO", "value": 62.337662337662337 }, { "caption": "LEXMARK", "value": 57.630979498861045 }, { "caption": "RICOH GROUP", "value": 62.035225048923678 }, { "caption": "SAMSUNG", "value": 69.024651661307615 }, { "caption": "SHARP", "value": 50.434782608695649 }, { "caption": "TOSHIBA", "value": 63.898916967509024 }, { "caption": "XEROX", "value": 62.30248306997742 }, { "caption": "Professional/Specialty", "value": 60.355029585798817 }, { "caption": "Neighborhood Shops", "value": 50.496453900709213 }, { "caption": "Online Printing", "value": 49.323308270676691 }] }, { "id": "qh01_46", "text": "Designed and built to last for a long time", "ri": 134.76829585553176, "attrIndex": "46", "columns": [{ "caption": "BROTHER", "value": 60.809102402022752 }, { "caption": "CANON", "value": 65.246142542248336 }, { "caption": "EPSON", "value": 59.699248120300751 }, { "caption": "HP", "value": 73.972602739726028 }, { "caption": "KONICA MINOLTA", "value": 55.151515151515149 }, { "caption": "KYOCERA MITA", "value": 53.038674033149171 }, { "caption": "LENOVO", "value": 69.186046511627907 }, { "caption": "LEXMARK", "value": 50.888888888888886 }, { "caption": "RICOH GROUP", "value": 59.893048128342244 }, { "caption": "SAMSUNG", "value": 63.102998696219039 }, { "caption": "SHARP", "value": 55.102040816326522 }, { "caption": "TOSHIBA", "value": 71.24183006535948 }, { "caption": "XEROX", "value": 64.00996264009963 }, { "caption": "Professional/Specialty", "value": 50.094161958568741 }, { "caption": "Neighborhood Shops", "value": 42.18181818181818 }, { "caption": "Online Printing", "value": 50.816326530612244 }] }, { "id": "qh01_9", "text": "Lowest initial price (e.g., the purchase price of the printer)", "ri": 127.96630526534283, "attrIndex": "9", "columns": [{ "caption": "BROTHER", "value": 54.853620955315876 }, { "caption": "CANON", "value": 59.595141700404866 }, { "caption": "EPSON", "value": 58.730158730158735 }, { "caption": "HP", "value": 65.645371577574963 }, { "caption": "KONICA MINOLTA", "value": 42.198581560283685 }, { "caption": "KYOCERA MITA", "value": 45.370370370370374 }, { "caption": "LENOVO", "value": 58.793969849246231 }, { "caption": "LEXMARK", "value": 52.2077922077922 }, { "caption": "RICOH GROUP", "value": 51.063829787234042 }, { "caption": "SAMSUNG", "value": 55.81737849779087 }, { "caption": "SHARP", "value": 39.896373056994818 }, { "caption": "TOSHIBA", "value": 49.032258064516128 }, { "caption": "XEROX", "value": 46.493902439024396 }, { "caption": "Professional/Specialty", "value": 47.422680412371129 }, { "caption": "Neighborhood Shops", "value": 39.376218323586741 }, { "caption": "Online Printing", "value": 44.086021505376344 }] }, { "id": "qh01_5", "text": "Intuitive and simple to use", "ri": 125.86570400608871, "attrIndex": "5", "columns": [{ "caption": "BROTHER", "value": 65.4232424677188 }, { "caption": "CANON", "value": 71.765663140764843 }, { "caption": "EPSON", "value": 69.047619047619051 }, { "caption": "HP", "value": 80.686973428386267 }, { "caption": "KONICA MINOLTA", "value": 52.095808383233532 }, { "caption": "KYOCERA MITA", "value": 57.232704402515722 }, { "caption": "LENOVO", "value": 71.541501976284579 }, { "caption": "LEXMARK", "value": 56.084656084656082 }, { "caption": "RICOH GROUP", "value": 58.638743455497377 }, { "caption": "SAMSUNG", "value": 65.620736698499314 }, { "caption": "SHARP", "value": 58.522727272727273 }, { "caption": "TOSHIBA", "value": 59.259259259259252 }, { "caption": "XEROX", "value": 62.0 }, { "caption": "Professional/Specialty", "value": 61.610486891385762 }, { "caption": "Neighborhood Shops", "value": 47.058823529411761 }, { "caption": "Online Printing", "value": 52.147239263803677 }] }, { "id": "qh01_44", "text": "Reliable completion of print jobs without stopping (no jams, disconnections, errors) ", "ri": 125.80750778385118, "attrIndex": "44", "columns": [{ "caption": "BROTHER", "value": 60.210210210210214 }, { "caption": "CANON", "value": 69.124024284475283 }, { "caption": "EPSON", "value": 61.743119266055047 }, { "caption": "HP", "value": 74.677187948350081 }, { "caption": "KONICA MINOLTA", "value": 50.322580645161288 }, { "caption": "KYOCERA MITA", "value": 57.101449275362313 }, { "caption": "LENOVO", "value": 65.587044534412954 }, { "caption": "LEXMARK", "value": 52.159468438538205 }, { "caption": "RICOH GROUP", "value": 62.162162162162161 }, { "caption": "SAMSUNG", "value": 61.390532544378694 }, { "caption": "SHARP", "value": 58.709677419354833 }, { "caption": "TOSHIBA", "value": 61.340206185567013 }, { "caption": "XEROX", "value": 62.0 }, { "caption": "Professional/Specialty", "value": 67.741935483870961 }, { "caption": "Neighborhood Shops", "value": 54.794520547945204 }, { "caption": "Online Printing", "value": 57.20524017467249 }] }, { "id": "qh01_1", "text": "The printed output looks exactly like what I expect (accurate color match, crisp black text, formatting )", "ri": 124.42000276759158, "attrIndex": "1", "columns": [{ "caption": "BROTHER", "value": 63.970588235294116 }, { "caption": "CANON", "value": 77.1356783919598 }, { "caption": "EPSON", "value": 68.9922480620155 }, { "caption": "HP", "value": 82.635552505147558 }, { "caption": "KONICA MINOLTA", "value": 59.016393442622949 }, { "caption": "KYOCERA MITA", "value": 56.609195402298852 }, { "caption": "LENOVO", "value": 61.5 }, { "caption": "LEXMARK", "value": 54.838709677419352 }, { "caption": "RICOH GROUP", "value": 62.857142857142854 }, { "caption": "SAMSUNG", "value": 66.852367688022284 }, { "caption": "SHARP", "value": 54.385964912280706 }, { "caption": "TOSHIBA", "value": 65.060240963855421 }, { "caption": "XEROX", "value": 66.807313642756682 }, { "caption": "Professional/Specialty", "value": 68.543689320388353 }, { "caption": "Neighborhood Shops", "value": 47.227191413237925 }, { "caption": "Online Printing", "value": 53.815261044176708 }] }, { "id": "qh01_2", "text": "Seamlessly works with all of my devices (PCs, Laptops, Smartphone, tablets)", "ri": 117.23018404483499, "attrIndex": "2", "columns": [{ "caption": "BROTHER", "value": 60.52202283849919 }, { "caption": "CANON", "value": 73.473282442748086 }, { "caption": "EPSON", "value": 66.698748796920114 }, { "caption": "HP", "value": 79.395385839299919 }, { "caption": "KONICA MINOLTA", "value": 54.140127388535028 }, { "caption": "KYOCERA MITA", "value": 58.90804597701149 }, { "caption": "LENOVO", "value": 63.670411985018724 }, { "caption": "LEXMARK", "value": 50.0 }, { "caption": "RICOH GROUP", "value": 61.413043478260867 }, { "caption": "SAMSUNG", "value": 70.391061452513966 }, { "caption": "SHARP", "value": 50.314465408805034 }, { "caption": "TOSHIBA", "value": 59.83606557377049 }, { "caption": "XEROX", "value": 59.810126582278478 }, { "caption": "Professional/Specialty", "value": 60.38461538461538 }, { "caption": "Neighborhood Shops", "value": 43.399638336347195 }, { "caption": "Online Printing", "value": 57.380457380457386 }] }];
$(window).on("resize", function () {
// loadOpportunityMap();
});
function loadOpportunityMap() {
$("#chartDiv").html("");
var om = new OpportunityMap();
om.init("#chartDiv", data, { mode: "count" });
om.setTitle("title", "class", "subTitle1", "subTitle2");
om.display();
}
loadOpportunityMap();
</script>
</body>
</html>
d3.selection.prototype.moveToFront = function () {
return this.each(function () {
this.parentNode.appendChild(this);
});
}
function OpportunityMap() {
var instance = {};
var parent_hash, originalData, data, options;
var height, width;
var svg, chartArea, chartBody, chartLegend;
var margin = { left: 30, top: 50, bottom: 10, right: 30 };
var boxHeight = 35;
var maxImportance, minImportance, averageImportance;
var maxScore, minScore, averageScore;
var zoomQuadrant = 0;
var colors, colors10, colors20;
var x, y;
var xAxis, yAxis;
var tooltip;
var legendDrawn = false;
var selectedAttributeIndex = -1;
var tooltipSet = false;
var gradient;
var drag_label;
var useLogScale = false;
instance.prepareData = function () {
var topValue = 100;
if (options.mode == "average") topValue = 5;
data = [];
for (var i = 0; i < originalData.length; i++) {
var d = originalData[i];
var attr = new Object();
attr.id = d.id.replace("_", "-");
attr.text = d.text;
attr.importance = +d.ri;
attr.attrIndex = d.attrIndex;
attr.brands = [];
for (var j = 0; j < d.columns.length; j++) {
var b = d.columns[j];
brand = new Object();
brand.text = b.caption;
brand.rating = +b.value;
brand.brandValue = +b.value;
brand.unmetValue = topValue - brand.brandValue;
attr.brands.push(brand);
}
attr.averageValue = d3.mean(attr.brands, function (d) { return d.brandValue; });
attr.unmetValue = topValue - attr.averageValue;
data.push(attr);
}
//console.log(data);
// unmet need
/*
var avgUnmet = d3.mean(data, function (d) { return topValue - attr.averageValue; });
var avgMet = d3.mean(data, function (d) { return attr.averageValue; });
data.forEach(function (d, i) {
d.unmetValue = (topValue - d.averageValue) / avgUnmet * 100;
// brands unmet need
d.brands.forEach(function (b, i) {
b.unmetValue = (topValue - b.brandValue) / avgUnmet * 100;
});
});*/
// normalize importance
//var averageImportance = d3.mean(data, function (d) { return d.importance; });
//console.log(averageImportance);
//options.mode = "average";
numAttributes = data.length;
maxImportance = d3.max(data, function (d) { return d.importance; });
minImportance = d3.min(data, function (d) { return d.importance; });
averageImportance = d3.mean(data, function (d) { return d.importance; });
maxImportance = maxImportance + 10; // * 1.05,
minImportance = minImportance - 10; //0.95;
averageScore = d3.mean(data, function (d) { return d.unmetValue; })
minScore = d3.min(data, function (d) { return d.unmetValue; });
maxScore = d3.max(data, function (d) { return d.unmetValue; });
//console.log(minScore + " - " + averageScore + " - " + maxScore);
minScore = minScore * 0.95;
maxScore = maxScore * 1.05;
}
instance.changeZoomQuadrant = function (newValue) {
if (zoomQuadrant == newValue) zoomQuadrant = 0; else zoomQuadrant = newValue;
//console.log("zoom is " + newValue);
//console.log(parent_hash);
$(parent_hash).html("");
instance.init(parent_hash, originalData, options);
instance.display();
}
/*
* initialize all variables
*/
instance.init = function (_parent_hash, _data, _options) {
parent_hash = _parent_hash;
originalData = _data;
options = _options;
instance.prepareData();
var parent = $(parent_hash);
height = parent.height();
width = parent.width();
if (width < 300) width = 600;
if (height < 200) height = 400;
var gap = 0.02;
// score is on X-Axis
if (useLogScale) {
x = d3.scale.log()
.domain([Math.exp(minScore), Math.exp(maxScore)])
.range([0, width - margin.left - margin.right]);
}
else {
var scoreDomain = [minScore, maxScore];
if ((zoomQuadrant == 1) || (zoomQuadrant == 4)) scoreDomain = [minScore, averageScore * (1 + gap)];
if ((zoomQuadrant == 2) || (zoomQuadrant == 3)) scoreDomain = [averageScore * (1 - gap), maxScore];
x = d3.scale.linear()
.domain(scoreDomain)
.range([0, width - margin.left - margin.right]);
}
xAxis = d3.svg.axis().scale(x).orient("middle").ticks(10);
// importance is on Y-Axis
var importanceDomain = [minImportance, maxImportance];
if ((zoomQuadrant == 1) || (zoomQuadrant == 2)) importanceDomain = [averageImportance * (1 - gap), maxImportance];
if ((zoomQuadrant == 3) || (zoomQuadrant == 4)) importanceDomain = [minImportance, averageImportance * (1 + gap)];
y = d3.scale.linear()
.domain(importanceDomain)
.range([height - (margin.top + margin.bottom + boxHeight), boxHeight]);
yAxis = d3.svg.axis()
.scale(y)
.orient("right")
.ticks(10);
colors = d3.scale.linear().domain([minImportance, maxImportance * 1.05]).range(["red", "green"]);
colors10 = d3.scale.category10();
colors20 = d3.scale.category20();
instance.drawBase();
return instance;
}
/*
* Draw base shapes, axes, etc.
*/
instance.drawBase = function () {
svg = d3.select(parent_hash).append("svg")
.attr("height", height)
.attr("width", width)
.attr("preserveAspectRatio", "xMidYMid meet");
gradient = svg.append("svg:defs")
.append("svg:linearGradient")
.attr("id", "gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "0%")
.attr("y2", "100%")
.attr("spreadMethod", "pad");
gradient.append("svg:stop")
.attr("offset", "0%")
.attr("stop-color", "red")
.attr("stop-opacity", 1);
gradient.append("svg:stop")
.attr("offset", "50%")
.attr("stop-color", "yellow")
.attr("stop-opacity", 1);
gradient.append("svg:stop")
.attr("offset", "100%")
.attr("stop-color", "green")
.attr("stop-opacity", 1);
// main G
mainG = svg.append("g")
.classed("mainG", true)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var horizontalOffset = margin.right + margin.left;
var borderPath = mainG.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", height - margin.top - margin.bottom)
.attr("width", width - horizontalOffset)
.style("stroke", "#ccc")
.style("fill", "none")
.style("stroke-width", 1);
// append y axis
mainG.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + x(averageScore) + ", 0)")
.call(yAxis.tickFormat(function (d) {
if (useLogScale)
return Math.log(d).toFixed(0);
else return d.toFixed(0);
}));
mainG.append("g").
append("text")
.attr("transform", "translate(" + (x(averageScore) - 5) + ", " + (margin.top - margin.bottom) + ") rotate(-90) translate(-100, 0)")
.style({ "font-size": "10px" })
.text("Important Attributes");
mainG.append("g").
append("text")
.attr("transform", "translate(" + (x(averageScore) - 5) + ", " + (margin.top - margin.bottom) + ") rotate(-90) translate(-" + (height - margin.bottom - margin.top - 80) + ", 0)")
.style({ "font-size": "10px" })
.text("Less Important Attributes");
// vertical line for unmet need 100
mainG.append("line")
.attr("x1", function () { return x(100); })
.attr("y1", function () {
return boxHeight;
})
.attr("x2", function () { return x(100); })
.attr("y2", function () { // top of the line
return height - margin.top - margin.bottom - boxHeight;
})
.style("stroke", "red")
.style("stroke-width", 2)
.style("stroke-dasharray", ("3, 3"))
.style("opacity", "0.2")
.moveToFront();
// append x axis
var xAxisY = y(averageImportance);
mainG.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + xAxisY + ")")
.style({ "stroke-width": 0 })
.call(xAxis.tickFormat(function (d) {
if (d < 0) return "";
if (options.mode == "average") return d.toFixed(2);
return d.toFixed(0);
}));
mainG.append("g")
.append("text")
.attr("transform", "translate(5, " + (xAxisY - 5) + ")")
.style({ "font-size": "10px" })
.text("Met Needs");
mainG.append("g")
.append("text")
.attr("transform", "translate(" + (x(maxScore) - 70) + ", " + (xAxisY - 5) + ")")
.style({ "font-size": "10px" })
.text("Unmet Needs");
// create drawing areas
chartArea = mainG.append("g")
.classed("chartArea", true)
.attr("transform", "translate(0," + (margin.top - margin.bottom) + ")");
chartBody = chartArea.append("g")
.classed("chartBody", true)
.attr("transform", "translate(0,0)");
chartLegend = chartArea.append("g")
.classed("chartLegend", true)
.attr("transform", function () {
var x = width - margin.right - 10;
var y = 0;
return "translate(" + x + ", " + y + ")";
})
// ********************
// add mouseover events
d3.selectAll(".brandchart-tooltip").remove();
tooltip = d3.select("body")
.append("div")
.classed("brandchart-tooltip", true)
.classed("draggable", true)
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.style({
"font-family": "'Lucida Grande', 'Lucida Sans Unicode', Helvetica, Arial, Verdana, sans-serif;",
"background-color": "gold",
"font-size": "10px",
"border": "1px solid #aaa",
"border-radius": "4px",
"padding": "11px",
"max-width": "350px",
"word-wrap": "break-word",
"cursor": "pointer",
"cursor": "hand"
});
instance.drawBoxes();
$(".draggable").draggable();
return instance;
}
/*
* Add descriptive boxes around the perimeter of the draw area
*/
instance.drawBoxes = function () {
// bottom left
var bottomLeft = mainG.append("g")
.attr("transform", function () {
return "translate(5, " + (height - margin.bottom - margin.top - 20) + ")"
});
bottomLeft.append("rect")
.attr("x", -5)
.attr("y", -15)
.attr("width", x(averageScore))
.attr("height", boxHeight)
.attr("fill", "#FF6600")
.attr("opacity", 1);
if ((zoomQuadrant == 0) || (zoomQuadrant == 4)) {
bottomLeft.append("text")
.text("Indifferent")
.style({ "font-size": "12px", "font-weight": "bold", "fill": "white", "cursor": "pointer", "cursor": "hand" })
.on("click", function () {
instance.changeZoomQuadrant(4);
});
bottomLeft.append("text")
.text("Less Important, Met Needs")
.attr("dy", 15)
.attr("width", (width - margin.left - margin.right - 20) / 2)
.attr("text-anchor", "left")
.style({ "text-align": "right", "font-size": "11px", "font-weight": "normal", "fill": "white" });
}
// bottom Right
var bottomRight = mainG.append("g")
.attr("transform", function () {
return "translate(" + (x(averageScore) + 5) + ", " + (height - margin.bottom - margin.top - 20) + ")"
});
bottomRight.append("rect")
.attr("x", -5)
.attr("y", -15)
.attr("width", (x(maxScore) - x(averageScore)))
.attr("height", 35)
.attr("fill", "#016FBE")
.attr("opacity", 1);
if ((zoomQuadrant == 0) || (zoomQuadrant == 3)) {
bottomRight.append("text")
.text("Market Development")
.attr("text-anchor", "right")
.attr("dx", function () {
var boxWidth = x(maxScore) - x(averageScore)
var textLength = this.getComputedTextLength();
var dx = boxWidth - this.getComputedTextLength() - 32;
return dx;
})
.style({ "font-size": "12px", "font-weight": "bold", "fill": "white", "cursor": "pointer", "cursor": "hand" })
.on("click", function () {
instance.changeZoomQuadrant(3);
});
bottomRight.append("text")
.text("Less Important, Unmet Needs")
.attr("dy", 15)
.attr("dx", function () {
var boxWidth = x(maxScore) - x(averageScore)
var textLength = this.getComputedTextLength();
var dx = boxWidth - this.getComputedTextLength();
return dx;
})
.attr("text-anchor", "right")
.style({ "text-align": "right", "font-size": "11px", "font-weight": "normal", "fill": "white" });
}
// topRight
var topRight = mainG.append("g")
.attr("transform", function () {
return "translate(" + (x(averageScore) + 5) + ", 15)"
});
topRight.append("rect")
.attr("x", -5)
.attr("y", -15)
.attr("width", x(maxScore) - x(averageScore))
.attr("height", 35)
.attr("fill", "#04AD4E")
.attr("opacity", 1);
if ((zoomQuadrant == 0) || (zoomQuadrant == 2)) {
topRight.append("text")
.text("Brand Evolution")
.attr("text-anchor", "right")
.attr("dx", function () {
var boxWidth = x(maxScore) - x(averageScore)
var textLength = this.getComputedTextLength();
var dx = boxWidth - this.getComputedTextLength() - 28;
return dx;
})
.style({ "font-size": "12px", "font-weight": "bold", "fill": "WHITE", "cursor": "pointer", "cursor": "hand" })
.on("click", function () {
instance.changeZoomQuadrant(2);
});
topRight.append("text")
.text("Important, Unmet Needs")
.attr("dy", 15)
.attr("dx", function () {
var boxWidth = x(maxScore) - x(averageScore)
var textLength = this.getComputedTextLength();
var dx = boxWidth - this.getComputedTextLength();
return dx;
})
.attr("text-anchor", "right")
.style({ "text-align": "right", "font-size": "11px", "font-weight": "normal", "fill": "white" });
}
// topleft
var topLeft = mainG.append("g")
.attr("transform", function () {
return "translate(5,15)"
});
topLeft.append("rect")
.attr("x", -5)
.attr("y", -15)
.attr("width", x(averageScore))
.attr("height", 35)
.attr("fill", "#BB0200")
.attr("opacity", 1);
if ((zoomQuadrant == 0) || (zoomQuadrant == 1)) {
topLeft.append("text")
.text("Stay in the Game")
.style({ "font-size": "12px", "font-weight": "bold", "fill": "white", "cursor": "pointer", "cursor": "hand" })
.on("click", function () {
instance.changeZoomQuadrant(1);
})
;
topLeft.append("text")
.text("Important, Met Needs")
.attr("dy", 15)
.attr("width", (width - margin.left - margin.right - 20) / 2)
.attr("text-anchor", "left")
.style({ "text-align": "right", "font-size": "11px", "font-weight": "normal", "fill": "white" });
}
}
instance.updateTooltip = function () {
// update tooltip
if (selectedAttributeIndex >= 0) {
var a = d3.select(".attribute-" + selectedAttributeIndex)
var title = a.attr("title");
var importance = a.attr("imp");
var avgScore = a.attr("avgScore");
var attrId = a.attr("attrId");
var code = "<table style='font-size:10px;'>"
code += "<tr><td colspan='2' style='font-weight:bold; font-size:11px;'>A" + attrId + " - " + title + "</td></tr>";
code += "<tr><td>Importance</td><td>" + parseFloat(importance).toFixed(0) + "</td></tr>";
var ratingTitle = "Unmet Need";
var rating = parseFloat(avgScore).toFixed(2);
if (options.mode == "count") rating = parseFloat(avgScore).toFixed(0) + "%";
code += "<tr><td>" + ratingTitle + "</td><td>" + rating + "</td></tr>";
code += "</table>";
tooltip.html(code);
}
else {
}
}
instance.getScaleColor = function (cur, min, max, colorArray) {
if (min == 0) min = 1;
var scale = (useLogScale) ? d3.scale.log().domain([min, max]) : d3.scale.linear().domain([min, max]);
scale.domain([0, .5, 1].map(scale.invert));
scale.range(colorArray);
var val = scale(cur);
return scale(cur);
}
instance.getCircleQuadrant = function (circle, d) {
if ((d.unmetValue >= averageScore) && (d.importance >= averageImportance)) {
return 2;
}
if ((d.unmetValue >= averageScore) && (d.importance <= averageImportance)) {
return 3;
}
if ((d.unmetValue < averageScore) && (d.importance > averageImportance)) {
return 1;
}
return 4;
}
instance.getCircleColor = function (circle, d) {
if ((d.unmetValue >= averageScore) && (d.importance >= averageImportance)) {
return "#04AD4E"; // green
}
if ((d.unmetValue >= averageScore) && (d.importance <= averageImportance)) {
return "#016FBE"; // blue
}
if ((d.unmetValue < averageScore) && (d.importance > averageImportance)) {
return "#BB0200"; // red
}
return "#FF6600"; // orange
//return instance.getScaleColor(d.unmetValue, minScore, maxScore, ["green", "yellow", "red"])
}
instance.setupAttributeClicks = function () {
var attributeIndexClicked = function (index) {
tooltip.style("visibility", "hidden");
if (index == selectedAttributeIndex) {
// deselect
selectedAttributeIndex = -1;
tooltip.style("visibility", "hidden");
fnDeselectAttribute(index);
}
else {
if (selectedAttributeIndex >= 0) {
// deselect
selectedAttributeIndex = -1;
tooltip.style("visibility", "hidden");
d3.selectAll(".attribute")
.call(fnDeselectAttributes);
}
selectedAttributeIndex = index;
if (!tooltipSet) {
tooltip.style("top", (event.pageY - 20) + "px").style("left", (event.pageX + 15) + "px");
}
tooltipSet = true;
tooltip.style("visibility", "visible");
instance.updateTooltip();
fnSelectAttribute(index);
}
}
d3.selectAll(".attributeText")
.on("click", function () {
var index = d3.select(this).attr("attributeIndex");
attributeIndexClicked(index);
});
d3.selectAll(".attribute")
.on("click", function () {
var index = d3.select(this).attr("attributeIndex");
attributeIndexClicked(index);
});
return instance;
}
// http://bl.ocks.org/mbostock/7555321
function textwrap(text, width) {
text.each(function () {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
//lineHeight = 1, // ems
lineHeight = 10, // px
y = text.attr("y"),
x = text.attr("x"),
dy = parseFloat(text.attr("dy")),
dx = parseFloat(text.attr("dx"));
if (dx == undefined) dx = 10;
if (dy == undefined) dy = 0;
var attrIndex = text.attr("attrIndex");
var spanClass = "attrSpan-" + attrIndex;
tspan = text.text(null)
.append("tspan")
.attr("class", spanClass)
.attr("x", x)
.attr("y", y)
.attr("dx", 15)
.attr("lineNumber", lineNumber)
;
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
lineNumber = lineNumber + 1;
tspan = text.append("tspan")
.attr("class", spanClass)
.attr("x", x)
.attr("y", y)
.attr("dy", (lineNumber) * lineHeight + "px")
.attr("dx", 15)
.attr("lineNumber", lineNumber)
.text(word);
}
}
text.attr("numLines", lineNumber + 1);
});
}
// drag handler for the attribute label
function doDragLabel(d) {
var lbl = d3.select(this);
var opacity = parseFloat(lbl.attr("opacity"));
if (opacity != 1) return; // do not drag if not in full view
var attrIndex = lbl.attr("attrIndex");
var spanClass = "attrSpan-" + attrIndex;
var numLines = parseInt(lbl.attr("numLines"));
var newX = d3.event.x - 15;
var newY = d3.event.y;
var circleX = parseFloat(lbl.attr("circleX"));
var circleY = parseFloat(lbl.attr("circleY"));
var text = lbl.text();
d3.selectAll("." + spanClass)
.attr("y", newY)
.attr("x", newX);
var attrIndex = lbl.attr("attrIndex");
var lineClass = "attrLine-" + attrIndex;
var lineClass2 = "attrLine-" + lbl.attr("attrId")
chartBody.selectAll("." + lineClass).remove();
chartBody.append("line")
.attr("class", lineClass + " " + lineClass2)
.attr("x1", newX + 125.0 / 2)
.attr("y1", newY)
.attr("x2", circleX)
.attr("y2", circleY)
.style("stroke", "green")
.style("stroke-width", 2)
.style("stroke-dasharray", ("3, 3"))
.style("opacity", "0.2");
}
// ***************
// initial display
instance.display = function () {
// labels dragging
drag_label = d3.behavior.drag().on("drag", doDragLabel);
var numAttributes = data.length;
chartBody.selectAll("circle").remove();
chartBody.selectAll("attrText").remove();
chartBody.selectAll("line").remove();
var filteredData;
if (zoomQuadrant == 0) filteredData = data;
else {
filteredData = data.filter(function (d) {
return instance.getCircleQuadrant(null, d) == zoomQuadrant;
})
}
//console.log(data);
//console.log(filteredData);
// draw attribute labels
lines = []
var attrText = chartBody
.selectAll(".attrText")
.data(filteredData)
.enter()
.append("text")
.text(function (d, i) {
return d.text;
})
.attr("circleX", function (d) { return x(d.unmetValue); })
.attr("circleY", function (d) { return y(d.importance) - boxHeight; })
.attr("attrIndex", function (d, i) { return i; })
.attr("attrId", function (d, i) { return d.id; })
.attr("class", function (d, i) {
return "attrText attrText-" + i + " attrText-" + d.id;
})
.style({ "cursor": "pointer", "cursor": "hand" })
.style({ "font": "10px sans-serif" })
.attr("y",
function (d) {
return y(d.importance) - boxHeight;
})
.attr("x",
function (d) {
return x(d.unmetValue);
})
.attr("opacity", function (d, i) {
if ((zoomQuadrant == 0) || instance.getCircleQuadrant(this, d) == zoomQuadrant) return 0.05; else return 0;
})
.call(textwrap, 125)
.call(drag_label);
// draw the attribute circles
var elem = chartBody
.selectAll(".attribute")
.data(data);
var elemEnter = elem.enter()
.append("g")
.attr("transform", function (d) {
var _y = y(d.importance) - boxHeight;
var _x = x(d.unmetValue);
return "translate(" + _x + "," + _y + ")"
})
var circle = elemEnter
.append("circle")
.attr("class", function (d, i) {
return "attribute attribute-" + i + " attribute-" + d.id;
})
.attr("attributeId", function (d) { return d.id; })
.attr("attributeIndex", function (d, i) { return i; })
.attr("title", function (d) { return d.text; })
.attr("attrId", function (d) { return d.id; })
.attr("imp", function (d) { return d.importance; })
.attr("avgScore", function (d) { return d.unmetValue; })
.style({ "stroke": "#FFFF00", "stroke-width": 2 })
.style({ "cursor": "pointer", "cursor": "hand" })
.style("fill", function (d, i) {
return instance.getCircleColor(this, d);
})
.attr("r", 14)
.attr("opacity", function (d, i) {
if ((zoomQuadrant == 0) || instance.getCircleQuadrant(this, d) == zoomQuadrant) return 1; else return 0;
})
.on("click", function (d, i) {
instance.circleClick(d, i);
});
var text = elemEnter
.append("text")
.text(function (d, i) { return "A" + d.attrIndex; })
.attr("attributeId", function (d) { return d.id; })
.attr("attributeIndex", function (d, i) { return i; })
.attr("class", function (d, i) {
return "attributeText attributeText-" + i + " attributeText-" + d.id;
})
.style({ "cursor": "pointer", "cursor": "hand" })
.attr("text-anchor", "middle")
.attr("fill", "white")
.attr("stroke-width", 0)
.attr("dy", 3)
.attr("opacity", function (d, i) {
if ((zoomQuadrant == 0) || instance.getCircleQuadrant(this, d) == zoomQuadrant) return 1; else return 0;
})
.style({ "font-size": "9px", "font-weight": "bold" })
.on("click", function (d, i) {
instance.circleClick(d, i);
});
instance.updateTooltip();
// select attribute func
fnSelectAttribute = function (index) {
var attr = d3.select(".attribute-" + index);
attr.moveToFront();
attr
.transition()
.duration(500)
.ease("linear")
.style({ "stroke": "red", "stroke-width": 2 })
.style("fill", function (d, i) {
return instance.getCircleColor(this, d);
});
attrText = d3.select(".attributeText-" + index);
attrText.moveToFront();
}
// deselect attribute func
fnDeselectAttribute = function (index) {
var cls = ".attribute-" + index;
var attr = d3.select(cls);
attr
.transition()
.duration(500)
.ease("linear")
.style({ "stroke": "#FFFF00", "stroke-width": 2 })
.style("fill", function (d, i) {
return instance.getCircleColor(this, d);
});
}
fnDeselectAttributes = function (attrs) {
attrs
.transition()
.duration(500)
.ease("linear")
.style({ "stroke": "#FFFF00", "stroke-width": 2 })
.style("fill", function (d, i) {
return instance.getCircleColor(this, d);
});
}
//instance.setupAttributeClicks();
return instance;
}
instance.circleClick = function (d, i) {
// select the appropriate text label and alter the opacity
var item = d3.select(".attrText-" + d.id);
var line = d3.select(".attrLine-" + d.id);
//console.log(".attrLine-" + d.id);
//console.log([item, line]);
var opacity = parseFloat(item.attr("opacity"));
if (opacity == 1) {
opacity = 0.05;
line.attr("opacity", 0);
line.style("stroke-width", 0)
}
else {
opacity = 1;
line.attr("opacity", 1);
line.style("stroke-width", 2)
}
item.attr("opacity", opacity);
}
// display the chart title
instance.setTitle = function (title, varClass, subTitle1, subTitle2) {
d3.selectAll(".chartTitle").remove();
var titleText = title;
if (varClass.length > 0) title = title + " (" + varClass + ")"
svg.append("g")
.classed("chartTitle", true)
.append("text")
.attr("x", margin.left + 10)
.attr("y", 10)
.style({ "font-weight": "bold", "font-size": "12px" })
.text(titleText)
.attr("transform", function (d) {
var textLength = this.getComputedTextLength();
var newX = (width - margin.left - margin.right) / 2 - textLength / 2;
return "translate(" + newX + ", 10)";
});
var subTitle = "";
if (subTitle1.length > 0) {
subTitle = subTitle1;
if (subTitle2.length > 0) {
subTitle += " - " + subTitle2;
}
}
else {
if (subTitle2.length > 0) {
subTitle = subTitle2;
}
}
if (subTitle.length > 0) {
svg.append("g")
.classed("chartTitle", true)
.append("text")
.attr("x", margin.left + 10)
.attr("y", 20)
.style({ "font-weight": "normal", "font-size": "11px" })
.text(subTitle)
.attr("transform", function (d) {
var textLength = this.getComputedTextLength();
var newX = (width - margin.left - margin.right) / 2 - textLength / 2;
return "translate(" + newX + ", 20)";
});
}
}
return instance;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment