1 /** 2 * Constructs a new, empty panel with default properties. Panels, with the 3 * exception of the root panel, are not typically constructed directly; instead, 4 * they are added to an existing panel or mark via {@link pv.Mark#add}. 5 * 6 * @class Represents a container mark. Panels allow repeated or nested 7 * structures, commonly used in small multiple displays where a small 8 * visualization is tiled to facilitate comparison across one or more 9 * dimensions. Other types of visualizations may benefit from repeated and 10 * possibly overlapping structure as well, such as stacked area charts. Panels 11 * can also offset the position of marks to provide padding from surrounding 12 * content. 13 * 14 * <p>All Protovis displays have at least one panel; this is the root panel to 15 * which marks are rendered. The box model properties (four margins, width and 16 * height) are used to offset the positions of contained marks. The data 17 * property determines the panel count: a panel is generated once per associated 18 * datum. When nested panels are used, property functions can declare additional 19 * arguments to access the data associated with enclosing panels. 20 * 21 * <p>Panels can be rendered inline, facilitating the creation of sparklines. 22 * This allows designers to reuse browser layout features, such as text flow and 23 * tables; designers can also overlay HTML elements such as rich text and 24 * images. 25 * 26 * <p>All panels have a <tt>children</tt> array (possibly empty) containing the 27 * child marks in the order they were added. Panels also have a <tt>root</tt> 28 * field which points to the root (outermost) panel; the root panel's root field 29 * points to itself. 30 * 31 * <p>See also the <a href="../../api/">Protovis guide</a>. 32 * 33 * @extends pv.Bar 34 */ 35 pv.Panel = function() { 36 pv.Bar.call(this); 37 38 /** 39 * The child marks; zero or more {@link pv.Mark}s in the order they were 40 * added. 41 * 42 * @see #add 43 * @type pv.Mark[] 44 */ 45 this.children = []; 46 this.root = this; 47 48 /** 49 * The internal $dom field is set by the Protovis loader; see lang/init.js. It 50 * refers to the script element that contains the Protovis specification, so 51 * that the panel knows where in the DOM to insert the generated SVG element. 52 * 53 * @private 54 */ 55 this.$dom = pv.Panel.$dom; 56 }; 57 58 pv.Panel.prototype = pv.extend(pv.Bar) 59 .property("canvas") 60 .property("overflow"); 61 62 pv.Panel.prototype.type = "panel"; 63 64 /** 65 * The canvas element; either the string ID of the canvas element in the current 66 * document, or a reference to the canvas element itself. If null, a canvas 67 * element will be created and inserted into the document at the location of the 68 * script element containing the current Protovis specification. This property 69 * only applies to root panels and is ignored on nested panels. 70 * 71 * <p>Note: the "canvas" element here refers to a <tt>div</tt> (or other suitable 72 * HTML container element), <i>not</i> a <tt>canvas</tt> element. The name of 73 * this property is a historical anachronism from the first implementation that 74 * used HTML 5 canvas, rather than SVG. 75 * 76 * @type string 77 * @name pv.Panel.prototype.canvas 78 */ 79 80 /** 81 * Default properties for panels. By default, the margins are zero, the fill 82 * style is transparent. 83 * 84 * @type pv.Panel 85 */ 86 pv.Panel.prototype.defaults = new pv.Panel() 87 .extend(pv.Bar.prototype.defaults) 88 .fillStyle(null) 89 .overflow("visible"); 90 91 /** 92 * Returns an anchor with the specified name. This method is overridden since 93 * the behavior of Panel anchors is slightly different from normal anchors: 94 * adding to an anchor adds to the anchor target's, rather than the anchor 95 * target's parent. To avoid double margins, we override the anchor's proto so 96 * that the margins are zero. 97 * 98 * @param {string} name the anchor name; either a string or a property function. 99 * @returns {pv.Anchor} the new anchor. 100 */ 101 pv.Panel.prototype.anchor = function(name) { 102 103 /* A "view" of this panel whose margins appear to be zero. */ 104 function z() { return 0; } 105 z.prototype = this; 106 z.prototype.left = z.prototype.right = z.prototype.top = z.prototype.bottom = z; 107 108 var anchor = pv.Bar.prototype.anchor.call(new z(), name) 109 .data(function(d) { return [d]; }); 110 anchor.parent = this; 111 return anchor; 112 }; 113 114 /** 115 * Adds a new mark of the specified type to this panel. Unlike the normal 116 * {@link Mark#add} behavior, adding a mark to a panel does not cause the mark 117 * to inherit from the panel. Since the contained marks are offset by the panel 118 * margins already, inheriting properties is generally undesirable; of course, 119 * it is always possible to change this behavior by calling {@link Mark#extend} 120 * explicitly. 121 * 122 * @param {function} type the type of the new mark to add. 123 * @returns {pv.Mark} the new mark. 124 */ 125 pv.Panel.prototype.add = function(type) { 126 var child = new type(); 127 child.parent = this; 128 child.root = this.root; 129 child.childIndex = this.children.length; 130 this.children.push(child); 131 return child; 132 }; 133 134 /** @private TODO */ 135 pv.Panel.prototype.bind = function() { 136 pv.Mark.prototype.bind.call(this); 137 for (var i = 0; i < this.children.length; i++) { 138 this.children[i].bind(); 139 } 140 }; 141 142 /** 143 * @private Evaluates all of the properties for this panel for the specified 144 * instance <tt>s</tt> in the scene graph, including recursively building the 145 * scene graph for child marks. 146 * 147 * @param s a node in the scene graph; the instance of the panel to build. 148 * @see Mark#scene 149 */ 150 pv.Panel.prototype.buildInstance = function(s) { 151 pv.Bar.prototype.buildInstance.call(this, s); 152 if (!s.children) s.children = []; 153 154 /* 155 * Build each child, passing in the parent (this panel) scene graph node. The 156 * child mark's scene is initialized from the corresponding entry in the 157 * existing scene graph, such that properties from the previous build can be 158 * reused; this is largely to facilitate the recycling of SVG elements. 159 */ 160 for (var i = 0; i < this.children.length; i++) { 161 this.children[i].scene = s.children[i]; // possibly undefined 162 this.children[i].build(); 163 } 164 165 /* 166 * Once the child marks have been built, the new scene graph nodes are removed 167 * from the child marks and placed into the scene graph. The nodes cannot 168 * remain on the child nodes because this panel (or a parent panel) may be 169 * instantiated multiple times! 170 */ 171 for (var i = 0; i < this.children.length; i++) { 172 s.children[i] = this.children[i].scene; 173 delete this.children[i].scene; 174 } 175 176 /* Delete any expired child scenes, should child marks have been removed. */ 177 s.children.length = this.children.length; 178 }; 179 180 /** 181 * @private Computes the implied properties for this panel for the specified 182 * instance <tt>s</tt> in the scene graph. Panels have two implied 183 * properties:<ul> 184 * 185 * <li>The <tt>canvas</tt> property references the DOM element, typically a DIV, 186 * that contains the SVG element that is used to display the visualization. This 187 * property may be specified as a string, referring to the unique ID of the 188 * element in the DOM. The string is converted to a reference to the DOM 189 * element. The width and height of the SVG element is inferred from this DOM 190 * element. If no canvas property is specified, a new SVG element is created and 191 * inserted into the document, using the panel dimensions; see 192 * {@link #createCanvas}. 193 * 194 * <li>The <tt>children</tt> array, while not a property per se, contains the 195 * scene graph for each child mark. This array is initialized to be empty, and 196 * is populated above in {@link #buildInstance}. 197 * 198 * </ul>The current implementation creates the SVG element, if necessary, during 199 * the build phase; in the future, it may be preferrable to move this to the 200 * update phase, although then the canvas property would be undefined. In 201 * addition, DOM inspection is necessary to define the implied width and height 202 * properties that may be inferred from the DOM. 203 * 204 * @param s a node in the scene graph; the instance of the panel to build. 205 */ 206 pv.Panel.prototype.buildImplied = function(s) { 207 if (!this.parent) { 208 var c = s.canvas; 209 if (c) { 210 if (typeof c == "string") c = document.getElementById(c); 211 212 /* Clear the container if it's not associated with this panel. */ 213 if (c.$panel != this) { 214 c.$panel = this; 215 c.innerHTML = ""; 216 } 217 218 /* If width and height weren't specified, inspect the container. */ 219 var w, h; 220 if (s.width == null) { 221 w = parseFloat(pv.css(c, "width")); 222 s.width = w - s.left - s.right; 223 } 224 if (s.height == null) { 225 h = parseFloat(pv.css(c, "height")); 226 s.height = h - s.top - s.bottom; 227 } 228 } else if (s.$canvas) { 229 230 /* 231 * If the canvas property is null, and we previously created a canvas for 232 * this scene node, reuse the previous canvas rather than creating a new 233 * one. 234 */ 235 c = s.$canvas; 236 } else { 237 238 /** 239 * Returns the last element in the current document's body. The canvas 240 * element is appended to this last element if another DOM element has not 241 * already been specified via the <tt>$dom</tt> field. 242 */ 243 function lastElement() { 244 var node = document.body; 245 while (node.lastChild && node.lastChild.tagName) { 246 node = node.lastChild; 247 } 248 return (node == document.body) ? node : node.parentNode; 249 } 250 251 /* Insert a new container into the DOM. */ 252 c = s.$canvas = document.createElement("span"); 253 this.$dom // script element for text/javascript+protovis 254 ? this.$dom.parentNode.insertBefore(c, this.$dom) 255 : lastElement().appendChild(c); 256 } 257 s.canvas = c; 258 } 259 pv.Bar.prototype.buildImplied.call(this, s); 260 }; 261