1 // ranges (bad, satisfactory, good)
  2 // measures (actual, forecast)
  3 // markers (previous, goal)
  4 
  5 /*
  6  * Chart design based on the recommendations of Stephen Few. Implementation
  7  * based on the work of Clint Ivy, Jamie Love, and Jason Davies.
  8  * http://projects.instantcognition.com/protovis/bulletchart/
  9  */
 10 
 11 /**
 12  * Constructs a new, empty bullet layout. Layouts are not typically constructed
 13  * directly; instead, they are added to an existing panel via
 14  * {@link pv.Mark#add}.
 15  *
 16  * @class
 17  * @extends pv.Layout
 18  */
 19 pv.Layout.Bullet = function() {
 20   pv.Layout.call(this);
 21   var that = this,
 22       buildImplied = that.buildImplied,
 23       scale = that.x = pv.Scale.linear(),
 24       orient,
 25       horizontal,
 26       rangeColor,
 27       measureColor,
 28       x;
 29 
 30   /** @private Cache layout state to optimize properties. */
 31   this.buildImplied = function(s) {
 32     buildImplied.call(this, x = s);
 33     orient = s.orient;
 34     horizontal = /^left|right$/.test(orient);
 35     rangeColor = pv.ramp("#bbb", "#eee")
 36         .domain(0, Math.max(1, x.ranges.length - 1));
 37     measureColor = pv.ramp("steelblue", "lightsteelblue")
 38         .domain(0, Math.max(1, x.measures.length - 1));
 39   };
 40 
 41   /**
 42    * The range prototype.
 43    *
 44    * @type pv.Mark
 45    * @name pv.Layout.Bullet.prototype.range
 46    */
 47   (this.range = new pv.Mark())
 48       .data(function() { return x.ranges; })
 49       .reverse(true)
 50       .left(function() { return orient == "left" ? 0 : null; })
 51       .top(function() { return orient == "top" ? 0 : null; })
 52       .right(function() { return orient == "right" ? 0 : null; })
 53       .bottom(function() { return orient == "bottom" ? 0 : null; })
 54       .width(function(d) { return horizontal ? scale(d) : null; })
 55       .height(function(d) { return horizontal ? null : scale(d); })
 56       .fillStyle(function() { return rangeColor(this.index); })
 57       .antialias(false)
 58       .parent = that;
 59 
 60   /**
 61    * The measure prototype.
 62    *
 63    * @type pv.Mark
 64    * @name pv.Layout.Bullet.prototype.measure
 65    */
 66   (this.measure = new pv.Mark())
 67       .extend(this.range)
 68       .data(function() { return x.measures; })
 69       .left(function() { return orient == "left" ? 0 : horizontal ? null : this.parent.width() / 3.25; })
 70       .top(function() { return orient == "top" ? 0 : horizontal ? this.parent.height() / 3.25 : null; })
 71       .right(function() { return orient == "right" ? 0 : horizontal ? null : this.parent.width() / 3.25; })
 72       .bottom(function() { return orient == "bottom" ? 0 : horizontal ? this.parent.height() / 3.25 : null; })
 73       .fillStyle(function() { return measureColor(this.index); })
 74       .parent = that;
 75 
 76   /**
 77    * The marker prototype.
 78    *
 79    * @type pv.Mark
 80    * @name pv.Layout.Bullet.prototype.marker
 81    */
 82   (this.marker = new pv.Mark())
 83       .data(function() { return x.markers; })
 84       .left(function(d) { return orient == "left" ? scale(d) : horizontal ? null : this.parent.width() / 2; })
 85       .top(function(d) { return orient == "top" ? scale(d) : horizontal ? this.parent.height() / 2 : null; })
 86       .right(function(d) { return orient == "right" ? scale(d) : null; })
 87       .bottom(function(d) { return orient == "bottom" ? scale(d) : null; })
 88       .strokeStyle("black")
 89       .shape("bar")
 90       .angle(function() { return horizontal ? 0 : Math.PI / 2; })
 91       .parent = that;
 92 
 93   (this.tick = new pv.Mark())
 94       .data(function() { return scale.ticks(7); })
 95       .left(function(d) { return orient == "left" ? scale(d) : null; })
 96       .top(function(d) { return orient == "top" ? scale(d) : null; })
 97       .right(function(d) { return orient == "right" ? scale(d) : horizontal ? null : -6; })
 98       .bottom(function(d) { return orient == "bottom" ? scale(d) : horizontal ? -8 : null; })
 99       .height(function() { return horizontal ? 6 : null; })
100       .width(function() { return horizontal ? null : 6; })
101       .parent = that;
102 };
103 
104 pv.Layout.Bullet.prototype = pv.extend(pv.Layout)
105     .property("orient", String) // left, right, top, bottom
106     .property("ranges")
107     .property("markers")
108     .property("measures")
109     .property("maximum", Number);
110 
111 /**
112  * Default properties for bullet layouts.
113  *
114  * @type pv.Layout.Bullet
115  */
116 pv.Layout.Bullet.prototype.defaults = new pv.Layout.Bullet()
117     .extend(pv.Layout.prototype.defaults)
118     .orient("left")
119     .ranges([])
120     .markers([])
121     .measures([]);
122 
123 /**
124  * The orientation.
125  *
126  * @type string
127  * @name pv.Layout.Bullet.prototype.orient
128  */
129 
130 /**
131  * The array of range values.
132  *
133  * @type array
134  * @name pv.Layout.Bullet.prototype.ranges
135  */
136 
137 /**
138  * The array of marker values.
139  *
140  * @type array
141  * @name pv.Layout.Bullet.prototype.markers
142  */
143 
144 /**
145  * The array of measure values.
146  *
147  * @type array
148  * @name pv.Layout.Bullet.prototype.measures
149  */
150 
151 /**
152  * Optional; the maximum range value.
153  *
154  * @type number
155  * @name pv.Layout.Bullet.prototype.maximum
156  */
157 
158 /** @private */
159 pv.Layout.Bullet.prototype.buildImplied = function(s) {
160   pv.Layout.prototype.buildImplied.call(this, s);
161   var size = this.parent[/^left|right$/.test(s.orient) ? "width" : "height"]();
162   s.maximum = s.maximum || pv.max([].concat(s.ranges, s.markers, s.measures));
163   this.x.domain(0, s.maximum).range(0, size);
164 };
165