// Chart design for T scores based on original bullet chart by Mike Bostock:
// http://bl.ocks.org/mbostock/4061961
// with d3.chart.js (0.1.2)
// http://misoproject.com/d3-chart/
d3.chart("BulleT", {
initialize: function() {
var chart = this;
this.xScale = d3.scale.linear();
this.base.classed("BulleT", true);
this.titleGroup = this.base.append("g");
this.title = this.titleGroup.append("text")
.attr("class", "title");
this.dimension = this.titleGroup.append("text")
.attr("class", "subtitle s2");
this.subtitle = this.titleGroup.append("text")
.attr("class", "subtitle s2");
// Default configuration
this._margin = { top: 0, right: 0, bottom: 0, left: 0 };
this.orientation("");
this.duration(0);
this.markers(function(d) {
return d.markers;
});
this.measures(function(d) {
return d.measures;
});
this.width(100);
this.height(100);
this.tickFormat(this.xScale.tickFormat(d3.format(".0d")));
this.reverse(false);
this.orient("left");
this.terjedelem(function(d) {
return d.terjedelem;
});
this.ranges(function(d) {
return d.ranges;
});
this.rangesLine(function(d) {
return d.rangesLine;
});
this.layer("ranges", this.base.append("g").classed("ranges", true), {
dataBind: function(data) {
var data_ranges = new Array();
// @CodeXmonk: a bit of hack too - ToDo later.
// This layer operates on "ranges" data, ranges[2] not needed
data_ranges[0] = data.ranges[0];
data_ranges[1] = data.ranges[1];
data_ranges[2] = data.ranges[3];
data_ranges[3] = data.ranges[4];
terjedelem = data.terjedelem;
data_ranges.unshift(terjedelem[1]);
return this.selectAll("rect.range").data(data_ranges);
},
insert: function() {
return this.append("rect");
},
events: {
enter: function(d, i) {
var orientation = chart.orientation();
var terjedelem = chart.terjedelem;
this.attr("class", function(d, i) { return "range s" + i; })
.attr("width", chart.xScale)
.attr("height", function(){
if( orientation == "vertical" ){
return chart.width();
}else{
return chart.height();
}
})
.attr("x", this.chart()._reverse ? chart.xScale : terjedelem[0]);
},
"merge:transition": function() {
var terjedelem = chart.terjedelem;
this.duration(chart.duration())
.attr("width", chart.xScale)
.attr("x", chart._reverse ? chart.xScale : terjedelem[0]);
},
exit: function() {
this.remove();
}
}
});
/******************************************************************************/
this.layer("rangesLine", this.base.append("g").classed("rangesLine", true), {
dataBind: function(data) {
// @CodeXmonk: This layer operates on "ranges" data
data_rangesLine = data.rangesLine;
return this.selectAll("line.range").data(data_rangesLine);
},
insert: function() {
return this.append("line");
},
events: {
enter: function(d, i) {
var orientation = chart.orientation();
this.attr("class", function(d, i) { return "range line"; })
.attr("x1", chart.xScale)
.attr("x2", chart.xScale)
.attr("y1", 0)
.attr("y2", function(){
if( orientation == "vertical" ){
return chart.width();
}else{
return chart.height();
}
});
},
"merge:transition": function() {
var orientation = chart.orientation();
this.attr("class", function(d, i) { return "range line"; })
.attr("x1", chart.xScale)
.attr("x2", chart.xScale)
.attr("y1", 0)
.attr("y2", function(){
if( orientation == "vertical" ){
return chart.width();
}else{
return chart.height();
}
});
},
exit: function() {
this.remove();
}
}
});
/******************************************************************************/
this.layer("measures", this.base.append("g").classed("measures", true), {
dataBind: function(data) {
// @CodeXmonk: This layer operates on "measures" data
data_measures = data.measures;
var terjedelem = data.terjedelem;
data_measures.unshift(terjedelem[1]);
return this.selectAll("rect.measure").data(data_measures);
},
insert: function() {
return this.append("rect");
},
events: {
enter: function() {
var orientation = chart.orientation();
var hy;
if( orientation == "vertical" ){
hy = chart.width() / 2;
}else{
hy = chart.height() / 2;
}
var terjedelem = chart.terjedelem();
this.attr("class", function(d, i) { return "measure s" + i; })
.attr("width", chart.xScale)
.attr("height", hy)
.attr("x", terjedelem[0])
.attr("y", hy/2);
},
"merge:transition": function() {
var terjedelem = chart.terjedelem;
this.duration(chart.duration())
.attr("width", chart.xScale)
.attr("x", terjedelem[0])
}
}
});
/******************************************************************************/
this.layer("markerSample", this.base.append("g").classed("markerSample", true), {
dataBind: function(data) {
// @CodeXmonk: This layer operates on "markerSample" datum
data = data.markers;
return this.selectAll("line.marker").data(data.slice(0,1));
},
insert: function() {
return this.append("line");
},
events: {
enter: function() {
var orientation = chart.orientation();
var whichOne;
if( orientation == "vertical" ){
whichOne = chart.width();
}else{
whichOne = chart.height();
}
this.attr("class", "marker Sample")
.attr("x1", chart.xScale)
.attr("x2", chart.xScale)
.attr("y1", whichOne / 4)
.attr("y2", whichOne - (whichOne/4) );
},
"merge:transition": function() {
var orientation = chart.orientation();
var whichOne;
if( orientation == "vertical" ){
whichOne = chart.width();
}else{
whichOne = chart.height();
}
this.duration(chart.duration())
.attr("x1", chart.xScale)
.attr("x2", chart.xScale)
.attr("y1", whichOne / 4)
.attr("y2", whichOne - (whichOne/4) );
}
}
});
/******************************************************************************/
this.layer("markerSubject", this.base.append("g").classed("markerSubject", true), {
dataBind: function(data) {
// @CodeXmonk: This layer operates on "markerSubject" datum
data = data.markers;
return this.selectAll("rect.marker").data(data.slice(1,2));
},
insert: function() {
return this.append("rect");
},
events: {
enter: function() {
var orientation = chart.orientation();
var whichOne;
if( orientation == "vertical" ){
whichOne = chart.width();
}else{
whichOne = chart.height();
}
this.attr("class", "marker Subject")
.attr("width", 6)
.attr("y", -(whichOne/10))
.attr("height",function(d) {return whichOne+(whichOne/5);})
.attr("x", chart.xScale)
.attr("transform", "translate(-3,0)");
},
"merge:transition": function() {
var orientation = chart.orientation();
var whichOne;
if( orientation == "vertical" ){
whichOne = chart.width();
}else{
whichOne = chart.height();
}
this.duration(chart.duration())
.attr("width", 6)
.attr("y", -(whichOne/10))
.attr("height",function(d) {return whichOne+(whichOne/5);})
.attr("x", chart.xScale)
.attr("transform", "translate(-3,0)");
}
}
});
/******************************************************************************/
this.layer("ticks", this.base.append("g").classed("ticks", true), {
dataBind: function() {
var format = this.chart().tickFormat();
return this.selectAll("g.tick").data(this.chart().xScale.ticks(8), function(d) {
return this.textContent || format(d);
});
},
insert: function() {
var orientation = chart.orientation();
var whichOne;
if( orientation == "vertical" ){
whichOne = chart.width();
}else{
whichOne = chart.height();
}
var tick = this.append("g").attr("class", "tick");
var height = whichOne;
var format = chart.tickFormat();
tick.append("line")
.attr("y1", whichOne)
.attr("y2", whichOne * 7 / 6);
tick.append("text")
.attr("text-anchor", "middle")
.attr("dy", "1em")
.attr("y", whichOne * 7 / 6)
.attr("transform", function(){
if( orientation == "vertical" ){
return "translate("+(whichOne+whichOne/2)+","+(whichOne+whichOne/2)+")rotate(90)";
} /* @CodeXmonk: if vertical BUT its not exact yet - ToDo */
})
.text(format);
return tick;
},
events: {
enter: function() {
this.attr("transform", function(d) {
return "translate(" + chart.xScale(d) + ",0)";
})
.style("opacity",1);
},
"merge:transition": function() {
var orientation = chart.orientation();
var whichOne;
if( orientation == "vertical" ){
whichOne = chart.width();
}else{
whichOne = chart.height();
}
this.duration(chart.duration())
.attr("transform", function(d) {
return "translate(" + chart.xScale(d) + ",0)";
})
.style("opacity", 1);
this.select("line")
.attr("y1", whichOne)
.attr("y2", whichOne * 7 / 6);
this.select("text")
.attr("y", whichOne * 7 / 6);
},
"exit:transition": function() {
this.duration(chart.duration())
.attr("transform", function(d) {
return "translate(" + chart.xScale(d) + ",0)";
})
.style("opacity", 1e-6)
.remove();
}
}
});
d3.timer.flush();
},
transform: function(data) {
var orientation = this.orientation();
var height = this.height();
if( orientation == "vertical" ){
this.base.attr("transform","translate(15," + ( height + 10 ) + ")rotate(-90)");
}
// misoproject: Copy data before sorting
var newData = {
title: data.title,
dimension: data.dimension,
randomizer: data.randomizer,
terjedelem: data.terjedelem.slice(),
ranges: data.ranges.slice().sort(d3.descending),
rangesLine: data.rangesLine.slice(),
measures: data.measures.slice().sort(d3.descending),
markers: data.markers.slice()
};
this.xScale.domain([newData.terjedelem[0], newData.terjedelem[1]]);
this.titleGroup
.style("text-anchor", function(){
if( orientation == "vertical" ){
return "middle";
}else{
return "end";
}
});
if( orientation == "vertical" ){
this.titleGroup.attr("transform", function(){ return "translate(-15,10)rotate(90)"});
}
this.dimension
.attr("dy", function(){
if( orientation == "vertical" ){
return "12px";
}else{
return "1.2em";
}
});
this.subtitle
.attr("dy", function(){
if( orientation == "vertical" ){
return "26px";
}else{
return "2.4em";
}
});
this.title.text(data.title);
this.dimension.text(data.dimension+"[T]");
this.subtitle.text(data.markers[1]);
this.subtitle.attr("class",function(d) {
switch (true){
case ( (data.markers[1] < 30) || (70 < data.markers[1]) ):
return "subtitle s04";
break;
case ( (30 <= data.markers[1]) && (data.markers[1] < 40) ):
return "subtitle s13";
break;
case ( (40 <= data.markers[1]) && (data.markers[1] <= 60) ):
return "subtitle s2";
break;
case ( (60 < data.markers[1]) && (data.markers[1] <= 70) ):
return "subtitle s13";
break;
}
});
return newData;
},
// misoproject: reverse or not
reverse: function(x) {
if (!arguments.length) return this._reverse;
this._reverse = x;
return this;
},
// misoproject: left, right, top, bottom
orient: function(x) {
if (!arguments.length) return this._orient;
this._orient = x;
this._reverse = this._orient == "right" || this._orient == "bottom";
return this;
},
// @CodeXmonk: terjedelem set (min,max) of the scale
terjedelem: function(x) {
if (!arguments.length) return this._terjedelem;
this._terjedelem = x;
return this;
},
// misoproject: ranges (bad, satisfactory, good)
ranges: function(x) {
if (!arguments.length) return this._ranges;
this._ranges = x;
return this;
},
// @CodeXmonk: for population mean T=50
rangesLine: function(x) {
if (!arguments.length) return this._ranges;
this._ranges = x;
return this;
},
// misoproject: markers (previous, goal)
markers: function(x) {
if (!arguments.length) return this._markers;
this._markers = x;
return this;
},
// misoproject: measures (actual, forecast)
measures: function(x) {
if (!arguments.length) return this._measures;
this._measures = x;
return this;
},
width: function(x) {
var margin, width_tmp;
if (!arguments.length) {
return this._width;
}
margin = this.margin();
width_tmp = x[0];
width_tmp = width_tmp - (margin.left + margin.right);
this._width = width_tmp;
if (x.length == 1){ /* @CodeXmonk: Scale needs to be put here when it's horizontal */
this.xScale.range(this._reverse ? [width_tmp, 0] : [0, width_tmp]);
}
this.base.attr("width", width_tmp);
return this;
},
height: function(x) {
var margin, height_tmp;
if (!arguments.length) {
return this._height;
}
margin = this.margin();
height_tmp = x[0];
height_tmp = height_tmp - (margin.top + margin.bottom);
this._height = height_tmp;
if (x.length != 1){ /* @CodeXmonk: Scale needs to be put here when it's vertical */
this.xScale.range(this._reverse ? [height_tmp, 0] : [0, height_tmp]);
}
this.base.attr("height", height_tmp);
this.titleGroup.attr("transform", "translate(-16," + height_tmp / 3 + ")");
return this;
},
margin: function(margin) {
if (!margin) {
return this._margin;
}
var margin_tmp = margin;
["top", "right", "bottom", "left"].forEach(function(dimension) {
if (dimension in margin_tmp) {
this._margin[dimension] = margin_tmp[dimension];
}
}, this);
this.base.attr("transform", "translate(" + this._margin.left + "," + this._margin.top + ")");
return this;
},
tickFormat: function(x) {
if (!arguments.length) return this._tickFormat;
this._tickFormat = x;
return this;
},
orientation: function(x) {
if (!arguments.length) return this._orientation;
this._orientation = x;
return this;
},
duration: function(x) {
if (!arguments.length) return this._duration;
this._duration = x;
return this;
}
});
|
(function() {
"use strict";
function HumbleData(){
this.original = [
{ "title":"Humble",
"dimension":"data",
"terjedelem":[20,80],
"ranges":[30,40,50,60,70],
"rangesLine":[50],
"measures":[30,40,60,70],
"markers":[50,50]
}
]
}
function Data(){
this.original = [
{ "title":"Man",
"dimension":"height",
"terjedelem":[20,80],
"ranges":[30,40,50,60,70],
"rangesLine":[50],
"measures":[38.93,47.15,63.59,71.81],
"markers":[55.37,50]
},
{ "title":"Woman",
"dimension":"height",
"terjedelem":[20,80],
"ranges":[30,40,50,60,70],
"rangesLine":[50],
"measures":[27.02,35.82,53.44,62.24],
"markers":[44.63,50]
},
{ "title":"Man",
"dimension":"weight",
"terjedelem":[20,80],
"ranges":[30,40,50,60,70],
"rangesLine":[50],
"measures":[39.70,48.31,65.54,74.15],
"markers":[56.93,50]
},
{ "title":"Woman",
"dimension":"weight",
"terjedelem":[20,80],
"ranges":[30,40,50,60,70],
"rangesLine":[50],
"measures":[29.24,36.21,50.14,57.11],
"markers":[43.18,50]
}
]
}
var getData = new Data(); /* data with sample statistics */
var data = getData.original; /* be resetable */
var getHumbleData = new HumbleData(); /* data without sample statistics */
var dataHumble = getHumbleData.original; /* be resetable */
// this one was the original d3.json version for start - when bulleTs-data.json is in the same directory
// d3.json("bulleTs-data.json", function(error, data) {
// and this is mine for getting data from the above function Data() but it needs to be called again, as it is done below: bulleT();
function bulleT() {
var myChartVertical = d3.select("#BulleT_vertical").chart("BulleTs", {
seriesCount: data.length
});
myChartVertical.margin({ top: 40, right: 20, bottom: 120, left: 5 })
.orientation("vertical")
.width([45,"vertical"]) /* bit of a hack. if array.length > 1 it means */
.height([350,"vertical"])/* that it is for vertical chart*/
.duration(1000);
myChartVertical.draw(data);
var myChartHorizontal = d3.select("#BulleT_horizontal").chart("BulleTs", {
seriesCount: data.length
});
myChartHorizontal.margin({ top: 5, right: 40, bottom: 20, left: 120 })
.orientation("horizontal")
.width([350])/* as writen before. now array.length == 1, so it is for horizontal */
.height([45])
.duration(1000);
myChartHorizontal.draw(data);
/* as we don't know anything about sample statistics only subjet's score will change*/
/* that's what I call humble data */
var myChartVerticalHumble = d3.select("#BulleT_vertical_Humble").chart("BulleTs", {
seriesCount: dataHumble.length
});
myChartVerticalHumble.margin({ top: 40, right: 20, bottom: 120, left: 5 })
.orientation("vertical")
.width([45,"vertical"])
.height([350,"vertical"])
.duration(1000);
myChartVerticalHumble.draw(dataHumble);
d3.selectAll("button#Randomize").on("click", function() {
data.forEach(randomize);
myChartVertical.draw(data);
myChartHorizontal.draw(data);
dataHumble.forEach(randomizeHumble);
myChartVerticalHumble.draw(dataHumble);
});
d3.selectAll("button#Reset").on("click", function() {
getData = new Data();
data = getData.original;
myChartVertical.draw(data);
myChartHorizontal.draw(data);
getHumbleData = new HumbleData();
dataHumble = getHumbleData.original;
myChartVerticalHumble.draw(dataHumble);
});
}
function randomize(d) {
var k = 10;
if (!d.randomizer) d.randomizer = randomizer(d);
d.markers[0] = d.randomizer(d.markers[0],40,60);
d.markers[1] = d.randomizer(d.markers[1],21,78);
k = d.randomizer(k,4,9);
d.measures[0] = d.markers[0] - 2*k;
d.measures[1] = d.markers[0] - k;
d.measures[2] = d.markers[0] + k;
d.measures[3] = d.markers[0] + 2*k;
return d;
}
function randomizeHumble(d) {
var k = 10;
if (!d.randomizer) d.randomizer = randomizer(d);
d.markers[1] = d.randomizer(d.markers[1],21,78);
k = d.randomizer(k,4,9);
return d;
}
// Returns a random integer between min and max
// Using Math.round() will give you a non-uniform distribution!
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomizer(d,min,max) {
return function(d,min,max) {
return getRandomInt(min,max);
};
}
bulleT();
})();
|