Last active
August 29, 2015 14:01
-
-
Save logycon/274cfbe3a1e14acbd4b4 to your computer and use it in GitHub Desktop.
Opportunity Map
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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