1 /**
  2  * Constructs a new wedge with default properties. Wedges are not typically
  3  * constructed directly, but by adding to a panel or an existing mark via
  4  * {@link pv.Mark#add}.
  5  *
  6  * @class Represents a wedge, or pie slice. Specified in terms of start and end
  7  * angle, inner and outer radius, wedges can be used to construct donut charts
  8  * and polar bar charts as well. If the {@link #angle} property is used, the end
  9  * angle is implied by adding this value to start angle. By default, the start
 10  * angle is the previously-generated wedge's end angle. This design allows
 11  * explicit control over the wedge placement if desired, while offering
 12  * convenient defaults for the construction of radial graphs.
 13  *
 14  * <p>The center point of the circle is positioned using the standard box model.
 15  * The wedge can be stroked and filled, similar to {link Bar}.
 16  *
 17  * <p>See also the <a href="../../api/Wedge.html">Wedge guide</a>.
 18  *
 19  * @extends pv.Mark
 20  */
 21 pv.Wedge = function() {
 22   pv.Mark.call(this);
 23 };
 24 
 25 pv.Wedge.prototype = pv.extend(pv.Mark)
 26     .property("startAngle")
 27     .property("endAngle")
 28     .property("angle")
 29     .property("innerRadius")
 30     .property("outerRadius")
 31     .property("lineWidth")
 32     .property("strokeStyle")
 33     .property("fillStyle");
 34 
 35 pv.Wedge.prototype.type = "wedge";
 36 
 37 /**
 38  * The start angle of the wedge, in radians. The start angle is measured
 39  * clockwise from the 3 o'clock position. The default value of this property is
 40  * the end angle of the previous instance (the {@link Mark#sibling}), or -PI / 2
 41  * for the first wedge; for pie and donut charts, typically only the
 42  * {@link #angle} property needs to be specified.
 43  *
 44  * @type number
 45  * @name pv.Wedge.prototype.startAngle
 46  */
 47 
 48 /**
 49  * The end angle of the wedge, in radians. If not specified, the end angle is
 50  * implied as the start angle plus the {@link #angle}.
 51  *
 52  * @type number
 53  * @name pv.Wedge.prototype.endAngle
 54  */
 55 
 56 /**
 57  * The angular span of the wedge, in radians. This property is used if end angle
 58  * is not specified.
 59  *
 60  * @type number
 61  * @name pv.Wedge.prototype.angle
 62  */
 63 
 64 /**
 65  * The inner radius of the wedge, in pixels. The default value of this property
 66  * is zero; a positive value will produce a donut slice rather than a pie slice.
 67  * The inner radius can vary per-wedge.
 68  *
 69  * @type number
 70  * @name pv.Wedge.prototype.innerRadius
 71  */
 72 
 73 /**
 74  * The outer radius of the wedge, in pixels. This property is required. For
 75  * pies, only this radius is required; for donuts, the inner radius must be
 76  * specified as well. The outer radius can vary per-wedge.
 77  *
 78  * @type number
 79  * @name pv.Wedge.prototype.outerRadius
 80  */
 81 
 82 /**
 83  * The width of stroked lines, in pixels; used in conjunction with
 84  * <tt>strokeStyle</tt> to stroke the wedge's border.
 85  *
 86  * @type number
 87  * @name pv.Wedge.prototype.lineWidth
 88  */
 89 
 90 /**
 91  * The style of stroked lines; used in conjunction with <tt>lineWidth</tt> to
 92  * stroke the wedge's border. The default value of this property is null,
 93  * meaning wedges are not stroked by default.
 94  *
 95  * @type string
 96  * @name pv.Wedge.prototype.strokeStyle
 97  * @see pv.color
 98  */
 99 
100 /**
101  * The wedge fill style; if non-null, the interior of the wedge is filled with
102  * the specified color. The default value of this property is a categorical
103  * color.
104  *
105  * @type string
106  * @name pv.Wedge.prototype.fillStyle
107  * @see pv.color
108  */
109 
110 /**
111  * Default properties for wedges. By default, there is no stroke and the fill
112  * style is a categorical color.
113  *
114  * @type pv.Wedge
115  */
116 pv.Wedge.prototype.defaults = new pv.Wedge()
117     .extend(pv.Mark.prototype.defaults)
118     .startAngle(function() {
119         var s = this.sibling();
120         return s ? s.endAngle : -Math.PI / 2;
121       })
122     .innerRadius(0)
123     .lineWidth(1.5)
124     .strokeStyle(null)
125     .fillStyle(defaultFillStyle.by(pv.index));
126 
127 /**
128  * Returns the mid-radius of the wedge, which is defined as half-way between the
129  * inner and outer radii.
130  *
131  * @see #innerRadius
132  * @see #outerRadius
133  * @returns {number} the mid-radius, in pixels.
134  */
135 pv.Wedge.prototype.midRadius = function() {
136   return (this.innerRadius() + this.outerRadius()) / 2;
137 };
138 
139 /**
140  * Returns the mid-angle of the wedge, which is defined as half-way between the
141  * start and end angles.
142  *
143  * @see #startAngle
144  * @see #endAngle
145  * @returns {number} the mid-angle, in radians.
146  */
147 pv.Wedge.prototype.midAngle = function() {
148   return (this.startAngle() + this.endAngle()) / 2;
149 };
150 
151 /**
152  * Constructs a new wedge anchor with default properties. Wedges support five
153  * different anchors:<ul>
154  *
155  * <li>outer
156  * <li>inner
157  * <li>center
158  * <li>start
159  * <li>end
160  *
161  * </ul>In addition to positioning properties (left, right, top bottom), the
162  * anchors support text rendering properties (text-align, text-baseline,
163  * textAngle). Text is rendered to appear inside the wedge.
164  *
165  * @param {string} name the anchor name; either a string or a property function.
166  * @returns {pv.Anchor}
167  */
168 pv.Wedge.prototype.anchor = function(name) {
169   var w = this;
170   return pv.Mark.prototype.anchor.call(this, name)
171     .left(function() {
172         switch (this.name()) {
173           case "outer": return w.left() + w.outerRadius() * Math.cos(w.midAngle());
174           case "inner": return w.left() + w.innerRadius() * Math.cos(w.midAngle());
175           case "start": return w.left() + w.midRadius() * Math.cos(w.startAngle());
176           case "center": return w.left() + w.midRadius() * Math.cos(w.midAngle());
177           case "end": return w.left() + w.midRadius() * Math.cos(w.endAngle());
178         }
179       })
180     .right(function() {
181         switch (this.name()) {
182           case "outer": return w.right() + w.outerRadius() * Math.cos(w.midAngle());
183           case "inner": return w.right() + w.innerRadius() * Math.cos(w.midAngle());
184           case "start": return w.right() + w.midRadius() * Math.cos(w.startAngle());
185           case "center": return w.right() + w.midRadius() * Math.cos(w.midAngle());
186           case "end": return w.right() + w.midRadius() * Math.cos(w.endAngle());
187         }
188       })
189     .top(function() {
190         switch (this.name()) {
191           case "outer": return w.top() + w.outerRadius() * Math.sin(w.midAngle());
192           case "inner": return w.top() + w.innerRadius() * Math.sin(w.midAngle());
193           case "start": return w.top() + w.midRadius() * Math.sin(w.startAngle());
194           case "center": return w.top() + w.midRadius() * Math.sin(w.midAngle());
195           case "end": return w.top() + w.midRadius() * Math.sin(w.endAngle());
196         }
197       })
198     .bottom(function() {
199         switch (this.name()) {
200           case "outer": return w.bottom() + w.outerRadius() * Math.sin(w.midAngle());
201           case "inner": return w.bottom() + w.innerRadius() * Math.sin(w.midAngle());
202           case "start": return w.bottom() + w.midRadius() * Math.sin(w.startAngle());
203           case "center": return w.bottom() + w.midRadius() * Math.sin(w.midAngle());
204           case "end": return w.bottom() + w.midRadius() * Math.sin(w.endAngle());
205         }
206       })
207     .textAlign(function() {
208         switch (this.name()) {
209           case "outer": return pv.Wedge.upright(w.midAngle()) ? "right" : "left";
210           case "inner": return pv.Wedge.upright(w.midAngle()) ? "left" : "right";
211         }
212         return "center";
213       })
214     .textBaseline(function() {
215         switch (this.name()) {
216           case "start": return pv.Wedge.upright(w.startAngle()) ? "top" : "bottom";
217           case "end": return pv.Wedge.upright(w.endAngle()) ? "bottom" : "top";
218         }
219         return "middle";
220       })
221     .textAngle(function() {
222         var a = 0;
223         switch (this.name()) {
224           case "center":
225           case "inner":
226           case "outer": a = w.midAngle(); break;
227           case "start": a = w.startAngle(); break;
228           case "end": a = w.endAngle(); break;
229         }
230         return pv.Wedge.upright(a) ? a : (a + Math.PI);
231       });
232 };
233 
234 /**
235  * Returns true if the specified angle is considered "upright", as in, text
236  * rendered at that angle would appear upright. If the angle is not upright,
237  * text is rotated 180 degrees to be upright, and the text alignment properties
238  * are correspondingly changed.
239  *
240  * @param {number} angle an angle, in radius.
241  * @returns {boolean} true if the specified angle is upright.
242  */
243 pv.Wedge.upright = function(angle) {
244   angle = angle % (2 * Math.PI);
245   angle = (angle < 0) ? (2 * Math.PI + angle) : angle;
246   return (angle < Math.PI / 2) || (angle > 3 * Math.PI / 2);
247 };
248 
249 /**
250  * @private Overrides the default behavior of {@link pv.Mark.buildImplied} such
251  * that the end angle is computed from the start angle and angle (angular span)
252  * if not specified.
253  *
254  * @param s a node in the scene graph; the instance of the wedge to build.
255  */
256 pv.Wedge.prototype.buildImplied = function(s) {
257   pv.Mark.prototype.buildImplied.call(this, s);
258 
259   /*
260    * TODO If the angle or endAngle is updated by an event handler, the implied
261    * properties won't recompute correctly, so this will lead to potentially
262    * buggy redraw. How to re-evaluate implied properties on update?
263    */
264   if (s.endAngle == null) s.endAngle = s.startAngle + s.angle;
265   if (s.angle == null) s.angle = s.endAngle - s.startAngle;
266 };
267