Skip to content

Instantly share code, notes, and snippets.

@CodeXmonk
Last active February 12, 2016 01:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save CodeXmonk/6112167 to your computer and use it in GitHub Desktop.
Save CodeXmonk/6112167 to your computer and use it in GitHub Desktop.
Simple modification based on mbostock's Bullet Charts.

There is an other, newer variant with Miso Project's d3.chart.js and with a little bit of more serious explanation. That chart can be found here on bl.ocks.org.

Just an idea...

( feedback is welcomed: @CodeXmonk )

Simple modification based on mbostock's Bullet Charts.

We Just Need To Make Use Of What We Have...

Background

I'm not a programmer. I only was a pilot who became a psychologist so now as a psychologist I work for/with pilots and do some research/statistics in the field of flight psychology and around. So I am not a programmer just a Code Monkey.

What is this?

I know that everyone here deals with data but for instance I don't use graphs so I don't know much about them (as I don't know much about javascript or English neither, excuse me for that). So allow me to go into details a little bit.

In psychology we use a specially standarized value the so-called: T Score. A little trickery is allowed here and in this way the mean of the sample will be 50 and standard deviation: 10. It is good for us because we like more numbers like:43 than -0,67. First one is T score and the second one is Z score of the same value.

The other reason why T score is better sometimes is that we can came to realize straightaway that it (43) is not too far away from the middle (50), in the range [40-60] where 68,27% of sample elements can be found.

(Very) simple example

  • An old hungarian humor - I try to translate it...

    • How many?
    • What is 30?
    • Why, what is how many?
  • Imagine that I want to introduce two persons to you. I tell you that one of them is 168,78 cm tall and weighs 61,31 kilos, while the other's height is 168,78 cm and the weight is 61,31 kg. What do you know now? Not too much (except your opinion that CodeXmonk must be as sophisticated person as this stupid example!-) Ok, I give you other information as well. One of them is a man and the other one is a woman - probably they are hungarian persons.

I think now you know more. In this situation, to know means (from statistical point of view) that you are aware of the sample and population statistics (mean, standard deviation, etc.).

So, if you look at them now, what will you see? Their equality? Yes and no. YES, because they are equal to each other in these aspects (height, weight). And NO: because what will be obtrusive in no time is that the man although he seems to be average as a hungarian person, at the same time he is not as tall and is thinner than what could be anticipated if we think of an averige man. It is the same with the woman only the shoe is on the other foot: she is taller and heavier than an averige woman.

Details of implementation

Bluish background stands for population statistics - mean: 50, sd: 10, and areas/ranges accordingly, meaning:

  • T < 30 : wrong; small; tiny; etc...
  • 30 <= T && T < 40 : below average; not so good;
  • 40 <= T && T <= 60 : averige ( let's say 45 < T < 55: info is not too meaningful or "everything is normal" )
  • 60 < T && T <= 70 : above average; good;
  • 70 < T : veri high; exceptional

These values are hard coded in json/data: ...,"ranges":[30,40,50,60,70],...

Greyish background stands for sample statistics. Mean and standard deviation must be computed. These values can be put (as computed here) in: ...,"measures":[38.93,47.15,63.59,71.81],...

  • measures[0] : mean - 2*sd
  • measures[1] : mean - 1*sd
  • measures[2] : mean + 1*sd
  • measures[3] : mean + 2*sd

And ...,"markers":[55.37,Tscore]...

  • markers[0] : it is the computed mean of the sample
  • markers[1] : the computed T Score of the given raw value

Sample values, measures[0-3] and markers[0] are computed results converted to T score based on sample statistics.

"terjedelem"

"terjedelem" is the hungarian word for the statistical term: range. Much of the excitements can be found somewhere around the middle (50) in the range of 20-80. That is why most of the time we don't need the range of 0-100.

The smaller the "terjedelem", the more precise resolution we get.

I suppose that the english word: "range" could have been used here but range, ranges, rangez would be too much in terms of readability.

Although it is 20-80 now, as a general rule I would use: ...,"terjedelem":[10,90],...

(function() {
// Simple modification based on mbostock's Bullet Charts.
d3.bulleT = function() {
var orient = "left",
reverse = false,
vertical = false,
terjedelem = bulleTTerjedelem,
ranges = bulleTRanges,
markers = bulleTMarkers,
measures = bulleTMeasures,
width = 380,
height = 30,
tickFormat = null;
// For each small multiple…
function bulleT(g) {
g.each(function(d, i) {
var terjedelemz = terjedelem.call(this, d, i),
rangez = ranges.call(this, d, i).slice().sort(d3.descending),
markerz = markers.call(this, d, i),
measurez = measures.call(this, d, i).slice().sort(d3.descending),
g = d3.select(this);
var wrap = g.select("g.wrap");
if (wrap.empty()) wrap = g.append("g").attr("class", "wrap");
// Compute the x-scale.
var x0 = d3.scale.linear()
.domain([terjedelemz[0], terjedelemz[1]])
.range(reverse ? [width, terjedelemz[0]] : [terjedelemz[0], width]);
// Stash the new scale.
this.__chart__ = x0;
// Derive width-scales from the x-scales.
var w = bulleTWidth(x0,terjedelemz[0]);
// Update the range rects.
rangez.unshift(terjedelemz[1]);
var range = wrap.selectAll("rect.range")
.data(rangez);
range.enter().append("rect")
.filter( function(d, i){ if(i != 3){ return d} })
.attr("class", function(d, i) { return "range s" + i; })
.attr("width", w)
.attr("y", 0)
.attr("height",height)
.attr("x", reverse ? x0 : terjedelemz[0]);
range.enter().append("line")
.filter( function(d, i){ if(i == 3){ return d} })
.attr("class", "marker")
.attr("x1", x0)
.attr("x2", x0)
.attr("y1", 0)
.attr("y2", height);
// Append the measure rects.
measurez.unshift(terjedelemz[1]);
var measure = wrap.selectAll("rect.measure")
.data(measurez);
measure.enter().append("rect")
.attr("class", function(d, i) { return "measure s" + i; })
.attr("width", w)
.attr("height", height / 2)
.attr("x", reverse ? x0 : terjedelemz[0])
.attr("y", height / 4);
// Append rect and line marker.
var marker = wrap.selectAll("rect.marker")
.data(markerz);
marker.enter().append("rect")
.filter( function(d, i){ if(i == 1){ return d} })
.attr("class", "marker s1")
.attr("width", 6)
.attr("y", -(height/10))
.attr("height",function(d) {return height+(height/5);})
.attr("x", x0)
.attr("transform", "translate(-3,0)");
marker.enter().append("line")
.filter( function(d, i){ if(i == 0){ return d} })
.attr("class", "marker s0")
.attr("x1", x0)
.attr("x2", x0)
.attr("y1", height / 4)
.attr("y2", height-(height / 4) );
// Compute the tick format.
var format = tickFormat || x0.tickFormat(8);
// Update the tick groups.
var tick = g.selectAll("tick")
.data(x0.ticks(8), function(d) {
return this.textContent || format(d);
});
// Initialize the ticks with the old scale, x0.
var tickEnter = tick.enter().append("g")
.attr("class", "tick")
.attr("transform", bulleTTranslate(x0))
.style("opacity", 1);
tickEnter.append("line")
.attr("y1", height)
.attr("y2", height * 7 / 6);
tickEnter.append("text")
.attr("text-anchor", "middle")
.attr("transform", function(d){
if (vertical) {
return "rotate(90)";
}
})
.attr("dy", function(d){
if(vertical){return width/60; }else{ return height+15 }
})
.attr("dx", function(d){
if(vertical){return height+15 ;}
})
.text(format);
});
}
// left, right, top, bottom
bulleT.orient = function(x) {
if (!arguments.length) return orient;
orient = x;
reverse = orient == "right" || orient == "bottom";
return bulleT;
};
// terjedelem
bulleT.terjedelem = function(x) {
if (!arguments.length) return terjedelem;
terjedelem = x;
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.vertical = function(x) {
if (!arguments.length) return vertical;
vertical = 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;
};
return bulleT;
};
function bulleTTerjedelem(d) {
return d.terjedelem;
}
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(" + x(d) + ",0)";
};
}
function bulleTWidth(x,y) {
var x0 = x(0);
return function(d) {
return Math.abs(x(d-y) - x0);
};
}
})();
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
padding-top: 40px;
position: relative;
/*width: 100%;*/
}
table{
width:60%;
margin-left:auto;
margin-right:auto;
}
td{width:50%;}
.bulleT { font: 10px sans-serif; margin-left:auto;margin-right:auto;}
.bulleT .marker { stroke: #4D4D4D; stroke-width: 2px;}
.bulleT .marker.s0 { fill-opacity:0; stroke: #999999; stroke-width: 2px; }
.bulleT .marker.s1 { fill-opacity:0; stroke: #000; stroke-width: 2px; }
.bulleT .tick line { stroke: #666; stroke-width: .5px; }
.bulleT .range.s0 { fill: #005C7A; }
.bulleT .range.s1 { fill: #29A3CC; }
.bulleT .range.s2 { fill: #c6dbef; }
.bulleT .range.s3 { fill: #29A3CC; }
.bulleT .range.s4 { fill: #005C7A; }
.bulleT .measure.s0 { fill: #4D4D4D; }
.bulleT .measure.s1 { fill: #999999; }
.bulleT .measure.s2 { fill: #eeeeee; }
.bulleT .measure.s3 { fill: #999999; }
.bulleT .measure.s4 { fill: #4D4D4D; }
.bulleT .title { font-size: 12px; font-weight: bold; }
.bulleT .subtitle.s04 { fill: #000000; font-size: 16px; font-weight: bold;}
.bulleT .subtitle.s13 { fill: #999999; font-size: 12px; font-weight: bold;}
.bulleT .subtitle.s2 { fill: #999999; font-size: 10px;}
</style>
<table>
<tr>
<td>
<div id="BulleT_vertical"></div>
</td>
<td>
<div id="BulleT_horizontal"></div>
</td>
</tr>
</table>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="bulleT.js"></script>
<script>
// just for now and simplicity
var Tscore_Man_Height = 50;
var Tscore_Woman_Height = 50;
var Tscore_Man_Weight = 50;
var Tscore_Woman_Weight = 50;
// terjedelem is the hungarian translation of the statistical term of range
var data = [
{"title":"Man","dimension":"(height)","subtitle":Tscore_Man_Height+" [T]","terjedelem":[20,80],"ranges":[30,40,50,60,70],"measures":[38.93,47.15,63.59,71.81],"markers":[55.37,Tscore_Man_Height]},
{"title":"Woman","dimension":"(height)","subtitle":Tscore_Woman_Height+" [T]","terjedelem":[20,80],"ranges":[30,40,50,60,70],"measures":[27.02,35.82,53.44,62.24],"markers":[44.63,Tscore_Woman_Height]},
{"title":"Man","dimension":"(weight)","subtitle":Tscore_Woman_Weight+" [T]","terjedelem":[20,80],"ranges":[30,40,50,60,70],"measures":[39.70,48.31,65.54,74.15],"markers":[56.93,Tscore_Man_Weight]},
{"title":"Woman","dimension":"(weight)","subtitle":Tscore_Woman_Weight+" [T]","terjedelem":[20,80],"ranges":[30,40,50,60,70],"measures":[29.24,36.21,50.14,57.11],"markers":[43.18,Tscore_Woman_Weight]}
]
var Width = 300, Height = 45;
var margin = {top: 5, right: 5, bottom: 20, left: 60},
width = Width - margin.left - margin.right,
height = Height - margin.top - margin.bottom;
var chart = d3.bulleT()
.width(width)
.height(height);
function bulleT(whichData,whereToPut,direction) {
var a=window.Width, b=window.Height;
if( direction == "vertical"){
Height=a;Width=b+10;
vertical = true;
}else{
Height=a;Width=b;
vertical = false;
}
var svg = d3.select(whereToPut).selectAll("svg")
.data(whichData)
.enter().append("svg")
.attr("class", "bulleT")
.attr("width", Width)
.attr("height", Height)
.append("g")
.attr("transform", function(){
if( direction == "vertical"){
return "rotate(-90)translate("+ -(Height-margin.left) +",10)";
}else{
return "translate("+ margin.left +","+ margin.top +")";
}
})
.call(chart.vertical(vertical));
var title = svg.append("g")
.style("text-anchor", function(){
if( direction == "vertical"){
return "middle";
}else{
return "end";
}
})
.attr("transform", function(){
if( direction == "vertical"){
return "rotate(90)translate("+ Width/4 +",10)";
}else{
return "translate(0," + height / 3 + ")";
}
});
title.append("text")
.attr("class", "title")
.text(function(d) { return d.title; });
title.append("text")
.attr("dy", "1.2em")
.text(function(d) { return d.dimension; })
title.append("text")
.attr("class",function(d) {
switch (true)
{
case ( (d.markers[1] < 30) || (70 < d.markers[1]) ):
return "subtitle s04";
break;
break;
case ( (30 <= d.markers[1]) && (d.markers[1] < 40) ):
return "subtitle s13";
break;
case ( (40 <= d.markers[1]) && (d.markers[1] <= 60) ):
return "subtitle s2";
break;
case ( (60 < d.markers[1]) && (d.markers[1] <= 70) ):
return "subtitle s13";
break;
}
}
)
.attr("dy", "2.4em")
.text(function(d) { return d.subtitle; });
};
bulleT(data,"#BulleT_vertical","vertical"); // "horizontal" or "vertical"
bulleT(data,"#BulleT_horizontal","horizontal");
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment