Skip to content

Instantly share code, notes, and snippets.

@saraquigley
Last active December 23, 2015 11:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save saraquigley/6628958 to your computer and use it in GitHub Desktop.
Save saraquigley/6628958 to your computer and use it in GitHub Desktop.
Strategic Dilemmas
// Chart design based on the recommendations of Stephen Few. Implementation
// based on the work of Clint Ivy, Jamie Love, and Jason Davies.
// http://projects.instantcognition.com/protovis/bulletchart/
// Forked by Sara Quigley to accommodate negative values and
// static axis that don't move when the values change
function bulletChart() {
var orient = "left", // TODO top & bottom
reverse = false,
duration = 100,
ranges = bulletRanges,
markers = bulletMarkers,
measures = bulletMeasures,
width = 400,
height = 40,
tickFormat = null
isUpdate = false;
// For each small multiple…
function bullet(g) {
g.each(function(d, i) {
var rangez = ranges.call(this, d, i).slice().sort(d3.descending),
markerz = markers.call(this, d, i).slice(),
measurez = measures.call(this, d, i).slice().sort(d3.descending),
g = d3.select(this);
reverse = (d3.min([0, rangez[0], markerz[0], measurez[0]]) < 0 ? true : false);
// Compute the new x-scale.
var x1 = d3.scale.linear()
//.domain(d3.extent([0, rangez[0], markerz[0], measurez[0]])).nice() // SQ added this 7/18 for variable domain supporting negative values
.domain([d.min, d.max])
// .domain([0, Math.max(rangez[0], markerz[0], measurez[0])]) // SQ commented this 7/18
.range([0, width]);
// Retrieve the old x-scale, if this is an update.
var x0 = this.__chart__ || d3.scale.linear()
.domain([d.min, d.max])
//.domain(d3.extent([0, rangez[0], markerz[0], measurez[0]])).nice()
.range(x1.range());
if (this.childElementCount == 1) { isUpdate = true;}
// Stash the new scale.
this.__chart__ = x1;
// Derive width-scales from the x-scales.
var w0 = bulletWidth(x0),
w1 = bulletWidth(x1);
// Update the range rects.
var range = g.selectAll("rect.range")
.data(rangez);
range.enter().append("svg:rect")
.attr("class", function(d, i) { return "range s" + i; })
.attr("width", w0)
.attr("height", height)
.attr("x", d3.min([x0(d.ranges), x0(0)]))
.transition()
.duration(duration)
.attr("width", w1)
.attr("x", d3.min([x1(d.ranges), x1(0)]) );
range.transition()
.duration(duration)
.attr("x", d3.min([x1(d.ranges), x1(0)]))
.attr("width", w1)
.attr("height", height);
// Update the measure rects.
var measure = g.selectAll("rect.measure")
.data(measurez);
measure.enter().append("svg:rect")
.attr("class", function(d, i) { return "measure s" + i; })
.attr("width", w0)
.attr("height", height / 6)
.attr("x", d3.min([x0(d.measures), x0(0)]))
.attr("y", height * .45)
.transition()
.duration(duration)
.attr("width", w1)
.attr("x", d3.min([x1(d.measures), x1(0)]));
// if it's an update, glow; otherwise, don't
if (isUpdate) {
measure.transition()
.duration(1500)
.style("fill", "rgb(254, 217, 118)")
.transition()
.duration(1500)
.style("fill", "#969696")
.attr("width", w1)
.attr("height", height / 6)
.attr("x", d3.min([x1(d.measures), x1(0)]))
.attr("y", height * .45);
}
else {
measure.transition()
.delay(2000)
.duration(100)
.attr("width", w1)
.attr("height", height / 6)
.attr("x", d3.min([x1(d.measures), x1(0)]))
.attr("y", height * .45);
}
// Update the marker lines.
var marker = g.selectAll("line.marker")
.data(markerz);
marker.enter().append("svg:line")
.attr("class", function(d, i) { return "marker s" + i; })
.attr("x1", x0)
.attr("x2", x0)
.attr("y1", height / 8)
.attr("y2", height * 7 / 8)
.transition()
.duration(duration)
.attr("x1", x1)
.attr("x2", x1);
marker.transition()
.delay(2000)
.duration(2000)
.attr("x1", x1)
.attr("x2", x1)
.attr("y1", height / 8)
.attr("y2", height * 7 / 8);
// Compute the tick format.
var format = tickFormat || x1.tickFormat(8);
// Update the tick groups.
var tick = g.selectAll("g.tick")
.data(x1.ticks(8), function(d) {
return this.textContent || format(d);
});
// Initialize the ticks with the old scale, x0.
var tickEnter = tick.enter().append("svg:g")
.attr("class", "tick")
.attr("transform", bulletTranslate(x0))
.style("opacity", 1e-6);
tickEnter.append("svg:line")
.attr("y1", height)
.attr("y2", height * 7 / 6);
tickEnter.append("svg:text")
.attr("text-anchor", "middle")
.attr("dy", "1em")
.attr("y", height * 7 / 6)
.text(format);
// Transition the entering ticks to the new scale, x1.
tickEnter.transition()
.delay(1500)
.duration(1000)
.attr("transform", bulletTranslate(x1))
.style("opacity", 1);
// Transition the updating ticks to the new scale, x1.
var tickUpdate = tick.transition()
.delay(1500)
.duration(1000)
.attr("transform", bulletTranslate(x1))
.style("opacity", 1);
tickUpdate.select("line")
.attr("y1", height)
.attr("y2", height * 7 / 6);
tickUpdate.select("text")
.attr("y", height * 7 / 6);
// Transition the exiting ticks to the new scale, x1.
tick.exit().transition()
.delay(1000)
.duration(duration)
.attr("transform", bulletTranslate(x1))
.style("opacity", 1e-6)
.remove();
});
d3.timer.flush();
}
// left, right, top, bottom
bullet.orient = function(x) {
if (!arguments.length) return orient;
orient = x;
reverse = orient == "right" || orient == "bottom";
return bullet;
};
// ranges (bad, satisfactory, good)
bullet.ranges = function(x) {
if (!arguments.length) return ranges;
ranges = x;
return bullet;
};
// markers (previous, goal)
bullet.markers = function(x) {
if (!arguments.length) return markers;
markers = x;
return bullet;
};
// measures (actual, forecast)
bullet.measures = function(x) {
if (!arguments.length) return measures;
measures = x;
return bullet;
};
bullet.width = function(x) {
if (!arguments.length) return width;
width = x;
return bullet;
};
bullet.height = function(x) {
if (!arguments.length) return height;
height = x;
return bullet;
};
bullet.tickFormat = function(x) {
if (!arguments.length) return tickFormat;
tickFormat = x;
return bullet;
};
bullet.duration = function(x) {
if (!arguments.length) return duration;
duration = x;
return bullet;
};
return bullet;
};
function bulletRanges(d) {
return d.ranges;
}
function bulletMarkers(d) {
return d.markers;
}
function bulletMeasures(d) {
return d.measures;
}
function bulletTranslate(x) {
return function(d) {
return "translate(" + Math.abs(x(d)) + ",0)";
// return "translate(" + x(d) + ",0)";
};
}
function bulletWidth(x) {
var x0 = x(0);
return function(d) {
return Math.abs(x(d) - x0);
};
}
.d3-slider {
position: relative;
font-family: Verdana,Arial,sans-serif;
font-size: 1.1em;
z-index: 2;
background-image: -webkit-gradient();
background-color: #FFF;
border: 1px solid #D3D3D3;
border-radius: 3em/3em;
opacity: 0.85;
height:8px;
}
.d3-slider-horizontal {
height:8px;
}
.d3-slider-vertical {
width: .8em;
height: 100px;
}
.d3-slider-handle {
position: absolute;
z-index: 3;
-webkit-appearance: none;
background-color: #f2ab1e;
background-repeat: repeat-x;
background-image: -khtml-gradient(linear, left top, left bottom, from(#f0c911), to(#f2ab1e));
background-image: -moz-linear-gradient(top, #f0c911, #f2ab1e);
background-image: -ms-linear-gradient(top, #f0c911, #f2ab1e);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #f0c911), color-stop(100%, #f2ab1e));
background-image: -webkit-linear-gradient(top, #f0c911, #f2ab1e);
background-image: -o-linear-gradient(top, #f0c911, #f2ab1e);
background-image: linear-gradient(top, #f0c911, #f2ab1e);
opacity: 1;
width: 16px;
height: 24px;
border: 1px solid #f2ab1e;
border-radius: 8px;
}
.d3-slider-handle:hover {
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #f2ab1e), color-stop(1, #f0c911) );
background:-moz-linear-gradient( center top, #f2ab1e 5%, #f0c911 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f2ab1e', endColorstr='#f0c911');
background-color: #f2ab1e;
}
.d3-slider-horizontal .d3-slider-handle {
top: -.6em;
margin-left: -.6em;
}
.d3-slider-axis {
position: relative;
z-index: 1;
}
.d3-slider-axis-bottom {
top: .8em;
}
.d3-slider-axis-right {
left: .8em;
}
.d3-slider-axis path {
stroke-width: 0;
fill: none;
}
.d3-slider-axis line {
fill: none;
stroke: #eee;
stroke-width: 0;
shape-rendering: crispEdges;
}
.d3-slider-axis text {
font-size: 11px;
fill: none;
}
.d3-slider-vertical .d3-slider-handle {
left: -.25em;
margin-left: 0;
margin-bottom: -.6em;
}
/*
Based on Bjorn Sandvik's work (see below) but slightly modified
by Sara Quigley to accommodate slower transitions
D3.js Slider
Inspired by jQuery UI Slider
Copyright (c) 2013, Bjorn Sandvik - http://blog.thematicmapping.org
BSD license: http://opensource.org/licenses/BSD-3-Clause
*/
d3.slider = function module() {
"use strict";
// Public variables width default settings
var min = 0,
max = 100,
step = 1,
animate = true,
orientation = "horizontal",
axis = false,
margin = 50,
value,
scale;
// Private variables
var axisScale,
dispatch = d3.dispatch("slide"),
formatPercent = d3.format(".2%"),
tickFormat = d3.format(".0"),
sliderLength;
function slider(selection) {
selection.each(function() {
// Create scale if not defined by user
if (!scale) {
scale = d3.scale.linear().domain([min, max]);
}
// Start value
value = value || scale.domain()[0];
// DIV container
var div = d3.select(this).classed("d3-slider d3-slider-" + orientation, true);
var drag = d3.behavior.drag();
// Slider handle
var handle = div.append("a")
.classed("d3-slider-handle", true)
.attr("xlink:href", "#")
.on("click", stopPropagation)
.call(drag);
// Horizontal slider
if (orientation === "horizontal") {
div.on("click", onClickHorizontal);
drag.on("drag", onDragHorizontal);
handle.style("left", formatPercent(scale(value)));
sliderLength = parseInt(div.style("width"), 10);
} else { // Vertical
div.on("click", onClickVertical);
drag.on("drag", onDragVertical);
handle.style("bottom", formatPercent(scale(value)));
sliderLength = parseInt(div.style("height"), 10);
}
if (axis) {
createAxis(div);
}
function createAxis(dom) {
// Create axis if not defined by user
if (typeof axis === "boolean") {
axis = d3.svg.axis()
//.ticks(Math.round(sliderLength / 100))
.ticks(4) // added by Sara Quigley
.tickFormat(tickFormat)
.orient((orientation === "horizontal") ? "bottom" : "right");
}
// Copy slider scale to move from percentages to pixels
axisScale = scale.copy().range([0, sliderLength]);
axis.scale(axisScale);
// Create SVG axis container
var svg = dom.append("svg")
.classed("d3-slider-axis d3-slider-axis-" + axis.orient(), true)
.on("click", stopPropagation);
var g = svg.append("g");
// Horizontal axis
if (orientation === "horizontal") {
svg.style("left", -margin);
svg.attr({
width: sliderLength + margin * 2,
height: margin
});
if (axis.orient() === "top") {
svg.style("top", -margin);
g.attr("transform", "translate(" + margin + "," + margin + ")")
} else { // bottom
g.attr("transform", "translate(" + margin + ",0)")
}
} else { // Vertical
svg.style("top", -margin);
svg.attr({
width: margin,
height: sliderLength + margin * 2
});
if (axis.orient() === "left") {
svg.style("left", -margin);
g.attr("transform", "translate(" + margin + "," + margin + ")")
} else { // right
g.attr("transform", "translate(" + 0 + "," + margin + ")")
}
}
g.call(axis);
}
// Move slider handle on click/drag
function moveHandle(pos) {
var newValue = stepValue(scale.invert(pos / sliderLength));
if (value !== newValue) {
var oldPos = formatPercent(scale(stepValue(value))),
newPos = formatPercent(scale(stepValue(newValue))),
position = (orientation === "horizontal") ? "left" : "bottom";
dispatch.slide(d3.event.sourceEvent || d3.event, value = newValue);
if (animate) {
handle.transition()
.styleTween(position, function() { return d3.interpolate(oldPos, newPos); })
.duration((typeof animate === "number") ? animate : 750); // changed to 750 by Sara Quigley
} else {
handle.style(position, newPos);
}
}
}
// Calculate nearest step value
function stepValue(val) {
var valModStep = (val - scale.domain()[0]) % step,
alignValue = val - valModStep;
if (Math.abs(valModStep) * 2 >= step) {
alignValue += (valModStep > 0) ? step : -step;
}
return alignValue;
}
function onClickHorizontal() {
moveHandle(d3.event.offsetX || d3.event.layerX);
}
function onClickVertical() {
moveHandle(sliderLength - d3.event.offsetY || d3.event.layerY);
}
function onDragHorizontal() {
moveHandle(Math.max(0, Math.min(sliderLength, d3.event.x)));
}
function onDragVertical() {
moveHandle(sliderLength - Math.max(0, Math.min(sliderLength, d3.event.y)));
}
function stopPropagation() {
d3.event.stopPropagation();
}
});
}
// Getter/setter functions
slider.min = function(_) {
if (!arguments.length) return min;
min = _;
return slider;
}
slider.max = function(_) {
if (!arguments.length) return max;
max = _;
return slider;
}
slider.step = function(_) {
if (!arguments.length) return step;
step = _;
return slider;
}
slider.animate = function(_) {
if (!arguments.length) return animate;
animate = _;
return slider;
}
slider.orientation = function(_) {
if (!arguments.length) return orientation;
orientation = _;
return slider;
}
slider.axis = function(_) {
if (!arguments.length) return axis;
axis = _;
return slider;
}
slider.margin = function(_) {
if (!arguments.length) return margin;
margin = _;
return slider;
}
slider.value = function(_) {
if (!arguments.length) return value;
value = _;
return slider;
}
slider.scale = function(_) {
if (!arguments.length) return scale;
scale = _;
return slider;
}
d3.rebind(slider, dispatch, "on");
return slider;
}
// levers -------- actions
var w = 610,
h = 60,
m = [10, 50, 20, 310]; // top right bottom left
// implications
var wO = 650,
hO = 60,
mO = [10, 30, 20, 340]; // top right bottom left
#header {
position: absolute;
display: block;
top: 20px;
left: 120px;
font: 300 32px "Helvetica Neue";
color: #525252;
}
#sub-header {
top: 20px;
font: 300 32px "Gill Sans";
color: #525252;
}
#dilemma-A-text {
position: absolute;
display: block;
text-align: end;
top: 100px;
left: 10px;
font: 300 32px "Helvetica Neue";
color: rgb(116, 169, 207);
width: 620px;
}
#versus {
position: absolute;
display: block;
top: 126px;
left: 646px;
width: 20px;
font: 200 24px "Helvetica Neue";
color: #969696;
}
#dilemma-B-text {
position: absolute;
display: block;
top: 100px;
left: 690px;
width: 590px;
font: 300 32px "Helvetica Neue";
color: rgb(116, 169, 207);
}
#dilemma {
position: absolute;
top: 291px;
left: 540px;
width: 240px;
}
#actions-header {
position: absolute;
top: 390px;
left: -20px;
width: 600px;
text-align: end;
font: 200 24px "Helvetica Neue";
color: #969696;
}
#implications-header {
position: absolute;
top: 390px;
left: 740px;
width: 590px;
text-align: start;
font: 200 24px "Helvetica Neue";
color: #969696;
}
.emphasized-header {
font: 500 24px "Helvetica Neue";
}
input[type=range] {
-webkit-appearance: none;
background-image: -webkit-gradient();
background-color: #FFF;
border: 1px solid #D3D3D3;
border-radius: 3em/3em;
opacity: 0.85;
width: 240px;
height:8px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
background-color: #f2ab1e;
background-repeat: repeat-x;
background-image: -khtml-gradient(linear, left top, left bottom, from(#f0c911), to(#f2ab1e));
background-image: -moz-linear-gradient(top, #f0c911, #f2ab1e);
background-image: -ms-linear-gradient(top, #f0c911, #f2ab1e);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #f0c911), color-stop(100%, #f2ab1e));
background-image: -webkit-linear-gradient(top, #f0c911, #f2ab1e);
background-image: -o-linear-gradient(top, #f0c911, #f2ab1e);
background-image: linear-gradient(top, #f0c911, #f2ab1e);
opacity: 1;
width: 16px;
height: 24px;
border: 1px solid #f2ab1e;
border-radius: 8px;
}
input[type="range"]::-webkit-slider-thumb:hover {
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #f2ab1e), color-stop(1, #f0c911) );
background:-moz-linear-gradient( center top, #f2ab1e 5%, #f0c911 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f2ab1e', endColorstr='#f0c911');
background-color:#f2ab1e;
}
.legend-swatch {
fill: rgb(231, 41, 138);
fill-opacity: .9;
stroke: rgb(231, 41, 138);
stroke-opacity: 1;
}
.legend-label {
font: 400 12px "Helvetica";
color: #525252;
}
.bullet-swatch-yel {
fill: rgb(255, 237, 111);
fill-opacity: .9;
stroke: rgb(255, 237, 111);
stroke-opacity: 1;
}
.bullet-swatch-red {
fill-opacity: .65;
fill: rgb(227, 26, 28);
stroke: rgb(227, 26, 28);
stroke-opacity: 1;
}
.legend-container {
position: absolute;
display: block;
left: 1080px;
top: 40px;
}
text.diamond-label {
position: absolute;
display: block;
fill: #525252;
font: 500 28px "Gill Sans";
}
.dilemmaB-label {
position: absolute;
left: 150px;
width: 600px;
display: inline-block;
text-align: left;
top: -50px;
color: #525252;
font: 400 28px "Gill Sans";
}
.dilemmaA-label {
position: absolute;
left: -510px;
width: 600px;
display: inline-block;
text-align: right;
top: -50px;
color: #525252;
font: 400 28px "Gill Sans";
}
rect.dilemma-defaultValue {
fill: #fff; /* 0064cd */
fill-opacity: .75;
z-index: -1;
}
#chart {
position: absolute;
width: 600px;
left: 40px;
top: 464px;
}
#implications {
position: absolute;
left: 620px;
top: 470px;
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.lever { font: 11px sans-serif;}
.lever .marker.s0 { stroke: rgb(116, 169, 207); stroke-width: 5px; }
.lever .marker.s1 { stroke: #f6b23d; stroke-width: 7px; }
.lever .axis line, .lever .axis path { stroke: #666; stroke-width: .5px; fill: none; }
.lever .range.s0 { fill-opacity: 1; fill: #bbb;}
.lever .range.s1 { fill-opacity: .75; fill: #ddd; }
.lever .range.s2 { fill-opacity: 1; fill: #eee;}
.lever .measure.s0 { fill: #969696; fill-opacity: .85;}
.lever .measure.s1 { fill: #525252; fill-opacity: 0;}
.lever .title { font-size: 20px; font-weight: 300; fill: #525252; font-family: Gill Sans;}
.lever .subtitle { fill: #999; font-size: 12px; }
.implication { font: 11px sans-serif;}
.implication .marker.s0 { stroke: rgb(116, 169, 207); stroke-width: 5px; }
.implication .marker.s1 { stroke: #f6b23d; stroke-width: 7px; }
.implication .axis line, .bullet .axis path { stroke: #666; stroke-width: .5px; }
.implication .range.s0 { fill-opacity: 0; fill: rgb(227, 26, 28);}
.implication .range.s1 { fill-opacity: 0; fill: rgb(255, 237, 111); }
.implication .range.s2 { fill-opacity: 0; fill: rgb(240, 240, 240);}
.implication .measure.s0 { fill: #969696; fill-opacity: .85;}
.implication .measure.s1 { fill: #525252; fill-opacity: 0;}
.implication .title { font-size: 20px; font-weight: 300; fill: #525252; font-family: Gill Sans;}
.implication .subtitle { fill: #999; font-size: 12px; }
.implication .label { font-size: 20px; font-weight: 400; fill: #525252; font-family: "Helvetica Neue";}
.bar.positive {
fill: rgb(116, 169, 207)
fill-opacity: 1;
}
.bar.negative {
fill: rgb(227, 26, 28);}
fill-opacity: .75;
}
.implication .axis text {
font: 10px sans-serif;
}
.implication .axis path,
.implication .axis line {
fill: none;
stroke: rgb(221, 52, 151);
stroke-width: 5px;
shape-rendering: crispEdges;
}
path.diamond {
position: absolute;
top: 80px;
stroke-width: 0px;
stroke: rgb(224, 224, 224);
fill: url(#radial-gradient);
z-index: 0;
}
svg.pathContainer {
position: absolute;
top: 60px;
z-index: 0;
}
svg.sliderPath {
position: absolute;
top: 244px;
left: 510px;
z-index: -100;
}
.glow {
fill: rgb(254, 217, 118);
z-index: -100;
}
aside {
background: #FFF8B8;
background-image: -webkit-linear-gradient(top, #FFF8B8, #FEED77);
box-shadow: 0 8px 16px rgba(0,0,0,.5);
color: #000;
display: block;
font: 400 16px "Helvetica Neue";
padding: 6px;
position: absolute;
left: 60px;
top: 560px;
-o-transform: rotate(-1deg);
-ms-transform: rotate(-1deg);
-moz-transform: rotate(-1deg);
-webkit-transform: rotate(-1deg);
-o-transition: opacity 100ms linear;
-ms-transition: opacity 100ms linear;
-moz-transition: opacity 100ms linear;
-webkit-transition: opacity 100ms linear;
}
.emph {
font: 400 14px "Helvetica Neue";
}
aside p:last-child {
margin-bottom: 0;
}
aside:hover {
opacity: .5;
}
[ { "dilemmaPosition": 0,
"bullets": [
{
"title": "Customer Experience",
"subtitle": "% positive overall experience",
"id":"stuExperience",
"ranges": [
60,
75,
100
],
"measures": [
95
],
"markers": [
85,
95
],
"min": 0,
"max": 100
},
{
"title": "Product Quality",
"subtitle": "Index, base value = 100",
"id":"topFaculty",
"ranges": [
0,
50,
100
],
"measures": [
80
],
"markers": [
90,
80
],
"min": 0,
"max": 100
},
{
"title": "Product Ratings",
"subtitle": "Index, base value = 100",
"id":"topRankings",
"ranges": [
0,
50,
100
],
"measures": [
30
],
"markers": [
50,
30
],
"min": 0,
"max": 100
},
{
"title": "Revenue",
"subtitle": "$, in millions",
"id": "revenue",
"ranges": [
-100,
0,
60
],
"measures": [
-97
],
"markers": [
-90,
-97
],
"min": -150,
"max": 50
}
] },
{ "dilemmaPosition": 1,
"bullets": [
{
"title": "Customer Experience",
"subtitle": "% positive overall experience",
"id":"stuExperience",
"ranges": [
60,
75,
100
],
"measures": [
90
],
"markers": [
85,
90
],
"min": 0,
"max": 100
},
{
"title": "Product Quality",
"subtitle": "Index, base value = 100",
"id":"topFaculty",
"ranges": [
0,
50,
100
],
"measures": [
85
],
"markers": [
90,
85
],
"min": 0,
"max": 100
},
{
"title": "Product Ratings",
"subtitle": "Index, base value = 100",
"id":"topRankings",
"ranges": [
0,
50,
100
],
"measures": [
40
],
"markers": [
50,
40
],
"min": 0,
"max": 100
},
{
"title": "Revenue",
"subtitle": "$, in millions",
"id": "budgetBalance",
"ranges": [
-100,
0,
60
],
"measures": [
-95
],
"markers": [
-90,
-95
],
"min": -150,
"max": 50
}
] },
{ "dilemmaPosition": 2,
"bullets": [
{
"title": "Customer Experience",
"subtitle": "% positive overall experience",
"id":"stuExperience",
"ranges": [
60,
75,
100
],
"measures": [
85
],
"markers": [
85,
85
],
"min": 0,
"max": 100
},
{
"title": "Product Quality",
"subtitle": "Index, base value = 100",
"id":"topFaculty",
"ranges": [
0,
50,
100
],
"measures": [
90
],
"markers": [
90,
90
],
"min": 0,
"max": 100
},
{
"title": "Product Ratings",
"subtitle": "Index, base value = 100",
"id":"topRankings",
"ranges": [
0,
50,
100
],
"measures": [
50
],
"markers": [
50,
50
],
"min": 0,
"max": 100
},
{
"title": "Revenue",
"subtitle": "$, in millions",
"id": "budgetBalance",
"ranges": [
-100,
0,
60
],
"measures": [
-90
],
"markers": [
-90,
-90
],
"min": -150,
"max": 50
}
] },
{ "dilemmaPosition": 3,
"bullets": [
{
"title": "Customer Experience",
"subtitle": "% positive overall experience",
"id":"stuExperience",
"ranges": [
60,
75,
100
],
"measures": [
70
],
"markers": [
85,
70
],
"min": 0,
"max": 100
},
{
"title": "Product Quality",
"subtitle": "Index, base value = 100",
"id":"topFaculty",
"ranges": [
0,
50,
100
],
"measures": [
94
],
"markers": [
90,
94
],
"min": 0,
"max": 100
},
{
"title": "Product Ratings",
"subtitle": "Index, base value = 100",
"id":"topRankings",
"ranges": [
0,
50,
100
],
"measures": [
60
],
"markers": [
50,
60
],
"min": 0,
"max": 100
},
{
"title": "Revenue",
"subtitle": "$, in millions",
"id": "budgetBalance",
"ranges": [
-100,
0,
60
],
"measures": [
-85
],
"markers": [
-90,
-85
],
"min": -150,
"max": 50
}
] }
]
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="superformula.js"></script>
<script type="text/javascript" src="bulletQ.js"></script>
<script type="text/javascript" src="dilemma_config.js"></script>
<script type="text/javascript" src="d3sliderQ.js"></script>
<title>Strategic Dilemmas</title>
<link type="text/css" rel="stylesheet" href="dilemmas.css"/>
<link type="text/css" rel="stylesheet" href="d3slider.css"/>
</head>
<body>
<div id="header">A Strategic Dilemma
<span id="sub-header"></span></div>
<div id="dilemma-A-text">Doing A with the intent of<br>achieving X goal</div>
<div id="versus"><b>vs.</b></div>
<div id="dilemma-B-text">Doing B with the intent of<br>achieving Y goal</div>
<div id="actions-header">... <span class="emphasized-header">Actions</span> that reflect the choice</div>
<div id="implications-header">And the <span class="emphasized-header">implications</span> that ensue ...</div>
<div id="dilemma"></div>
<div id="chart"></div>
<div id="implications"></div>
<script type="text/javascript">
// slider background
var format = d3.format(".4n"),
scale = d3.scale.linear().domain([-10, 20, 1000]).range([0, 800, 1000]);
var svg = d3.select("body").append("svg")
.attr("width", 300)
.attr("height", 100)
.attr("class", "sliderPath");
var shape = d3.superformula()
.type("quigleySlider")
.size(45000)
.segments(4000);
var path = svg.append("path")
.attr("class", "diamond")
.attr("transform", "translate(150,50)")
.attr("d", shape);;
svg.append("radialGradient")
.attr("id", "radial-gradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 250)
.selectAll("stop")
.data([
{offset: "10%", color: "rgb(116, 169, 207)"}, // same as the action measures
{offset: "95%", color: "rgb(240, 240, 240)"}
])
.enter().append("stop")
.attr("offset", function(d) { return d.offset; })
.attr("stop-color", function(d) { return d.color; });
// slider
var geoDilemma = [
{
"id": "genericSlider",
"defaultValue": 2,
"dilemmaA": "Choice A",
"dilemmaB": "Choice B"
}
];
var dilemma = d3.select("#dilemma").selectAll("div")
.data(geoDilemma)
.enter().append("div")
.attr("id", function(d) { return d.id; })
.style("top", function(d, i) { return d[i] * 80; })
.call(d3.slider()
.value(geoDilemma[0].defaultValue)
.axis(true)
.min(0)
.max(3)
.step(1)
.on("slide", function(evt, value) { update(value); }));
//dilemma A on the left of the horizontal slider
dilemma.append("span")
.attr("class", "dilemmaA-label")
.text(function(d) { return d.dilemmaA; });
//dilemma B on the right of the horizontal slider
dilemma.append("span")
.attr("class", "dilemmaB-label")
.text(function(d) { return d.dilemmaB; });
// shadow for position 2
svg.append("svg:rect")
.attr("class", "dilemma-defaultValue")
.attr("width", 16)
.attr("height", 24)
.attr("rx", 8)
.attr("ry", 8)
.attr("x", 181)
.attr("y", 39);
// set the initial dilemma positions
// levers -------- actions
var actionChart = bulletChart()
.width(w - m[1] - m[3])
.height(h - m[0] - m[2]);
d3.json("levers.json", function(data) {
var vis = d3.select("#chart").selectAll("svg")
.data(data[geoDilemma[0].defaultValue].bullets)
.enter().append("svg")
.attr("class", "lever")
.attr("id", function(d) {return d.id ;})
.attr("width", w)
.attr("height", h)
.append("g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")")
.on("change", function() { dispatch.statechange(this.value); })
.call(actionChart);
var title = vis.append("g")
.attr("text-anchor", "end")
.attr("transform", "translate(-6," + (h - m[0] - m[2]) / 2 + ")");
title.append("text")
.attr("class", "title")
.text(function(d) { return d.title; });
title.append("text")
.attr("class", "subtitle")
.attr("dy", "1.25em")
.text(function(d) { return d.subtitle; });
actionChart.duration(1000);
});
// implications
var outcomeChart = bulletChart()
.width(wO - mO[1] - mO[3])
.height(hO - mO[0] - mO[2]);
d3.json("implications.json", function(data) {
var vis = d3.select("#implications").selectAll("svg")
.data(data[geoDilemma[0].defaultValue].bullets)
.enter().append("svg")
.attr("class", "implication")
.attr("id", function(d) {return d.id ;})
.attr("width", wO)
.attr("height", hO)
.append("g")
.attr("class", "bulletGroup")
.attr("transform", "translate(" + mO[3] + "," + mO[0] + ")")
.call(outcomeChart);
var title = vis.append("g")
.attr("text-anchor", "end")
.attr("transform", "translate(-6," + (hO - mO[0] - mO[2]) / 2 + ")");
title.append("text")
.attr("class", "title")
.text(function(d) { return d.title; });
title.append("text")
.attr("class", "subtitle")
.attr("dy", "1.25em")
.text(function(d) { return d.subtitle; });
outcomeChart.duration(1000);
});
function update(dilemmaPosition) {
// update the lever / action taken
d3.json("levers.json", function(data) {
var actions = d3.selectAll(".lever")
.data(data[dilemmaPosition].bullets)
.transition()
.ease("cubic-in-out")
.delay(500)
.duration(4000)
.call(actionChart);
});
// update implications
d3.json("implications.json", function(data) {
var outcomes = d3.selectAll(".bulletGroup")
.data(data[dilemmaPosition].bullets)
.transition()
.ease("cubic-in-out")
.delay(3000)
.duration(3500)
.attr("transform", "translate(" + mO[3] + "," + mO[0] + ")")
.call(outcomeChart);
});
}
</script>
</body>
</html>
[
{ "dilemmaPosition": 0,
"bullets": [
{
"title": "% Growth in International Sales",
"subtitle": "% Percentage",
"id": "levPercentMission",
"ranges": [
100,
100,
100
],
"measures": [
50
],
"markers": [
30,
50
],
"min": 0,
"max": 100
}
]
},
{ "dilemmaPosition": 1,
"bullets": [
{
"title": "% Growth in International Sales",
"subtitle": "% Percentage",
"id": "levPercentMission",
"ranges": [
100,
100,
100
],
"measures": [
40
],
"markers": [
30,
40
],
"min": 0,
"max": 100
}
]
},
{ "dilemmaPosition": 2,
"bullets": [
{
"title": "% Growth in International Sales",
"subtitle": "% Percentage",
"id": "levPercentMission",
"ranges": [
100,
100,
100
],
"measures": [
30
],
"markers": [
30,
30
],
"min": 0,
"max": 100
}
]
},
{ "dilemmaPosition": 3,
"bullets": [
{
"title": "% Growth in International Sales",
"subtitle": "% Percentage",
"id": "levPercentMission",
"ranges": [
100,
100,
100
],
"measures": [
20
],
"markers": [
30,
20
],
"min": 0,
"max": 100
}
]
}
]
/*
Christophe Viau implemented a new shape type as a D3 plugin based on superformulas.
see https://gist.github.com/mbostock/1021103 for more info
Sara Quigley just added and names a few new types...
*/
(function() {
var _symbol = d3.svg.symbol(),
_line = d3.svg.line();
d3.superformula = function() {
var type = _symbol.type(),
size = _symbol.size(),
segments = size,
params = {};
function superformula(d, i) {
var n, p = _superformulaTypes[type.call(this, d, i)];
for (n in params) p[n] = params[n].call(this, d, i);
return _superformulaPath(p, segments.call(this, d, i), Math.sqrt(size.call(this, d, i)));
}
superformula.type = function(x) {
if (!arguments.length) return type;
type = d3.functor(x);
return superformula;
};
superformula.param = function(name, value) {
if (arguments.length < 2) return params[name];
params[name] = d3.functor(value);
return superformula;
};
// size of superformula in square pixels
superformula.size = function(x) {
if (!arguments.length) return size;
size = d3.functor(x);
return superformula;
};
// number of discrete line segments
superformula.segments = function(x) {
if (!arguments.length) return segments;
segments = d3.functor(x);
return superformula;
};
return superformula;
};
function _superformulaPath(params, n, diameter) {
var i = -1,
dt = 2 * Math.PI / n,
t,
r = 0,
x,
y,
points = [];
while (++i < n) {
t = params.m * (i * dt - Math.PI) / 4;
t = Math.pow(Math.abs(Math.pow(Math.abs(Math.cos(t) / params.a), params.n2)
+ Math.pow(Math.abs(Math.sin(t) / params.b), params.n3)), -1 / params.n1);
if (t > r) r = t;
points.push(t);
}
r = diameter * Math.SQRT1_2 / r;
i = -1; while (++i < n) {
x = (t = points[i] * r) * Math.cos(i * dt);
y = t * Math.sin(i * dt);
points[i] = [Math.abs(x) < 1e-6 ? 0 : x, Math.abs(y) < 1e-6 ? 0 : y];
}
return _line(points) + "Z";
}
var _superformulaTypes = {
asterisk: {m: 12, n1: .3, n2: 0, n3: 10, a: 1, b: 1},
bean: {m: 2, n1: 1, n2: 4, n3: 8, a: 1, b: 1},
butterfly: {m: 3, n1: 1, n2: 6, n3: 2, a: .6, b: 1},
circle: {m: 4, n1: 2, n2: 2, n3: 2, a: 1, b: 1},
clover: {m: 6, n1: .3, n2: 0, n3: 10, a: 1, b: 1},
cloverFour: {m: 8, n1: 10, n2: -1, n3: -8, a: 1, b: 1},
cross: {m: 8, n1: 1.3, n2: .01, n3: 8, a: 1, b: 1},
diamond: {m: 4, n1: .85, n2: 1, n3: 1, a: 1, b: 1},
quigleyDiamond: {m: 4, n1: 1.513, n2: 1, n3: 1, a: 1, b: 1},
quigleyCross: {m: 4.025, n1: .5750, n2: -0.6625, n3: -0.4375, a: -.7750, b: -2.5},
quigleySlider: {m: 4.000, n1: 15.16, n2: 17.15, n3: 11.86, a: 5.337, b: 1},
dilemmas6: {m: 6, n1: .85, n2: 1, n3: 1, a: 1, b: 1},
drop: {m: 1, n1: .5, n2: .5, n3: .5, a: 1, b: 1},
ellipse: {m: 4, n1: 2, n2: 2, n3: 2, a: 9, b: 6},
gear: {m: 19, n1: 100, n2: 50, n3: 50, a: 1, b: 1},
heart: {m: 1, n1: .8, n2: 1, n3: -8, a: 1, b: .18},
heptagon: {m: 7, n1: 1000, n2: 400, n3: 400, a: 1, b: 1},
hexagon: {m: 6, n1: 1000, n2: 400, n3: 400, a: 1, b: 1},
malteseCross: {m: 8, n1: .9, n2: .1, n3: 100, a: 1, b: 1},
pentagon: {m: 5, n1: 1000, n2: 600, n3: 600, a: 1, b: 1},
rectangle: {m: 4, n1: 100, n2: 100, n3: 100, a: 2, b: 1},
roundedStar: {m: 5, n1: 2, n2: 7, n3: 7, a: 1, b: 1},
square: {m: 4, n1: 100, n2: 100, n3: 100, a: 1, b: 1},
star: {m: 5, n1: 30, n2: 100, n3: 100, a: 1, b: 1},
triangle: {m: 3, n1: 100, n2: 200, n3: 200, a: 1, b: 1}
};
d3.superformulaTypes = d3.keys(_superformulaTypes);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment