lib/rubyvis/mark.rb in rubyvis-0.1.5 vs lib/rubyvis/mark.rb in rubyvis-0.1.6

- old
+ new

@@ -1,13 +1,133 @@ module Rubyvis - # Represents a data-driven graphical mark. The <tt>Mark</tt> class is - # the base class for all graphical marks in Protovis; it does not provide any - # specific rendering functionality, but together with {@link Panel} establishes - # the core framework. + # Constructs a new mark with default properties. Marks, with the exception of + # the root panel, are not typically constructed directly; instead, they are + # added to a panel or an existing mark via Mark#add + # + # Represents a data-driven graphical mark. The +Mark+ class is + # the base class for all graphical marks in Protovis; it does not provide any + # specific rendering functionality, but together with Panel establishes + # the core framework. + # + # Concrete mark types include familiar visual elements such as bars, lines + # and labels. Although a bar mark may be used to construct a bar chart, marks + # know nothing about charts; it is only through their specification and + # composition that charts are produced. These building blocks permit many + # combinatorial possibilities. + # + # Marks are associated with <b>data</b>: a mark is generated once per + # associated datum, mapping the datum to visual <b>properties</b> such as + # position and color. Thus, a single mark specification represents a set of + # visual elements that share the same data and visual encoding. The type of + # mark defines the names of properties and their meaning. A property may be + # static, ignoring the associated datum and returning a constant; or, it may be + # dynamic, derived from the associated datum or index. Such dynamic encodings + # can be specified succinctly using anonymous functions. Special properties + # called event handlers can be registered to add interactivity. + # + # Protovis uses <b>inheritance</b> to simplify the specification of related + # marks: a new mark can be derived from an existing mark, inheriting its + # properties. The new mark can then override properties to specify new + # behavior, potentially in terms of the old behavior. In this way, the old mark + # serves as the <b>prototype</b> for the new mark. Most mark types share the + # same basic properties for consistency and to facilitate inheritance. + # + # The prioritization of redundant properties is as follows:<ol> + # + # <li>If the <tt>width</tt> property is not specified (i.e., null), its value + # is the width of the parent panel, minus this mark's left and right margins; + # the left and right margins are zero if not specified. + # + # <li>Otherwise, if the <tt>right</tt> margin is not specified, its value is + # the width of the parent panel, minus this mark's width and left margin; the + # left margin is zero if not specified. + # + # <li>Otherwise, if the <tt>left</tt> property is not specified, its value is + # the width of the parent panel, minus this mark's width and the right margin. + # + # </ol>This prioritization is then duplicated for the <tt>height</tt>, + # <tt>bottom</tt> and <tt>top</tt> properties, respectively. + # + # While most properties are <i>variable</i>, some mark types, such as lines + # and areas, generate a single visual element rather than a distinct visual + # element per datum. With these marks, some properties may be <b>fixed</b>. + # Fixed properties can vary per mark, but not <i>per datum</i>! These + # properties are evaluated solely for the first (0-index) datum, and typically + # are specified as a constant. However, it is valid to use a function if the + # property varies between panels or is dynamically generated. + # class Mark + # Hash storing properties names for current Mark type. @properties={} + + # The enclosing parent panel. The parent panel is generally undefined only for the root panel; however, it is possible to create "offscreen" marks that are used only for inheritance purposes. + # @attr [Panel] + attr_accessor :parent + + # The root parent panel. This may be undefined for "offscreen" marks that are + # created for inheritance purposes only. + # + # @attr [Panel] + attr_accessor :root + + # The child index. -1 if the enclosing parent panel is null; otherwise, the + # zero-based index of this mark into the parent panel's <tt>children</tt> + # array. + # @attr [Number] + attr_accessor :child_index + + + # The scene graph. The scene graph is an array of objects; each object + # (or "node") corresponds to an instance of this mark and an element in the + # data array. The scene graph can be traversed to lookup previously-evaluated + # properties. + attr_accessor :scene + + # The mark prototype, possibly undefined, from which to inherit property + # functions. The mark prototype is not necessarily of the same type as this + # mark. Any properties defined on this mark will override properties inherited + # either from the prototype or from the type-specific defaults. + # @attr [Mark] + attr_accessor :proto + + # The mark anchor target, possibly undefined. + # @attr [Mark] + attr_accessor :target + + # The current scale factor, based on any enclosing transforms. The current + # scale can be used to create scale-independent graphics. For example, to + # define a dot that has a radius of 10 irrespective of any zooming, say: + # + # <pre>dot.shape_radius(lambda { 10 / self.scale})</pre> + # + # Note that the stroke width and font size are defined irrespective of scale + # (i.e., in screen space) already. Also note that when a transform is applied + # to a panel, the scale affects only the child marks, not the panel itself. + # @attr [Float] + # @see Panel#transform + attr_accessor :scale + + # Array with stores properties values + attr_reader :_properties + + # OpenStruct which store values for scenes + attr_accessor :binds + + # Defines a setter-getter for the specified property. + # + # If a cast function has been assigned to the specified property name, the + # property function is wrapped by the cast function, or, if a constant is + # specified, the constant is immediately cast. Note, however, that if the + # property value is null, the cast function is not invoked. + # + # Parameters: + # * @param [String] name the property name. + # * @param [Boolean] +def+ whether is a property or a def. + # * @param [Proc] +cast+ the cast function for this property. + # * @param [Class] +klass+ the klass on which property will be added + def self.property_method(name, _def, func=nil, klass=nil) return if klass.method_defined? name klass.send(:define_method, name) do |*arguments| v,dummy = arguments if _def and self.scene @@ -41,10 +161,11 @@ if camel!=name klass.send(:alias_method, camel, name) end end + # Creates a dinamic property using property_method(). def self.attr_accessor_dsl(*attr) attr.each do |sym| if sym.is_a? Array name,func=sym else @@ -58,87 +179,208 @@ self.send(name,v) } end end + # Delete index + # @private def delete_index @index=nil @index_defined=false end + + # The mark index. The value of this field depends on which instance (i.e., + # which element of the data array) is currently being evaluated. During the + # build phase, the index is incremented over each datum; when handling events, + # the index is set to the instance that triggered the event. + # @return [Integer] def index @index end - + # Returns true if index attribute is set and not deleted def index_defined? @index_defined end + # Set index attribute. def index=(v) @index_defined=true @index=v v end - + # Sets the value of the property <i>name</i> to <i>v</i> def property_value(name,v) prop=Property.new({:name=>name, :id=>Rubyvis.id, :value=>v}) @_properties.delete_if{|v1| v1.name==name} @_properties.push(prop) #@_properties_values[name]=v - return prop + prop end + + # Alias for setting the left, right, top and bottom properties simultaneously. + # + # @see Mark#left + # @see Mark#right + # @see Mark#top + # @see Mark#bottom + # @return [Mark] self def margin(n) self.left(n).right(n).top(n).bottom(n) end + + + # @private Returns the current instance of this mark in the scene graph. + # This is typically equivalent to <tt>this.scene[this.index]</tt>, however if the scene or index is unset, the default instance of the mark is returned. If no default is set, the default is the last instance. + # Similarly, if the scene or index of the parent panel is unset, the default instance of this mark in the last instance of the enclosing panel is returned, and so on. + # + # @return a node in the scene graph. def instance(default_index=nil) scene=self.scene scene||=self.parent.instance(-1).children[self.child_index] index = index_defined? ? self.index : default_index # "defined?: #{index_defined?} : type: #{type}, self.index: #{self.index}, index:#{index}" scene[index<0 ? scene.size-1: index] end - - - attr_accessor :parent, :root, :child_index, :scene, :proto, :target, :scale - attr_reader :_properties + ## + # :section: Marks properties + ## + ## + # :attr: data + # The data property; an array of objects. The size of the array determines the number of marks that will be instantiated; each element in the array will be passed to property functions to compute the property values. Typically, the data property is specified as a constant array, such as + # m.data([1, 2, 3, 4, 5]) + # However, it is perfectly acceptable to define the data property as a + # proc. This function might compute the data dynamically, allowing + # different data to be used per enclosing panel. For instance, in the stacked + # area graph example (see Mark#scene), the data function on the area mark + # dereferences each series. - attr_accessor_dsl :data,:visible, :left, :right, :top, :bottom, :cursor, :title, :reverse, :antialias, :events, :id + ## + # :attr: visible + # The visible property; a boolean determining whether or not the mark instance + # is visible. If a mark instance is not visible, its other properties will not + # be evaluated. Similarly, for panels no child marks will be rendered. + + ## + # :attr: left + # The left margin; the distance, in pixels, between the left edge of the + # enclosing panel and the left edge of this mark. Note that in some cases this + # property may be redundant with the right property, or with the conjunction of + # right and width. + + ## + # :attr: right + # The right margin; the distance, in pixels, between the right edge of the + # enclosing panel and the right edge of this mark. Note that in some cases this + # property may be redundant with the left property, or with the conjunction of + # left and width. + + ## + # :attr: top + # The top margin; the distance, in pixels, between the top edge of the + # enclosing panel and the top edge of this mark. Note that in some cases this + # property may be redundant with the bottom property, or with the conjunction + # of bottom and height. + + ## + # :attr: bottom + # The bottom margin; the distance, in pixels, between the bottom edge of the + # enclosing panel and the bottom edge of this mark. Note that in some cases + # this property may be redundant with the top property, or with the conjunction + # of top and height. + + ## + # :attr: cursor + # The cursor property; corresponds to the CSS cursor property. This is + # typically used in conjunction with event handlers to indicate interactivity. + # See {CSS 2 cursor}[http://www.w3.org/TR/CSS2/ui.html#propdef-cursor] + + ## + # :attr: title + # The title property; corresponds to the HTML/SVG title property, allowing the + # general of simple plain text tooltips. + + + ## + # :attr: events + # The events property; corresponds to the SVG pointer-events property, + # specifying how the mark should participate in mouse events. The default value + # is "painted". Supported values are: + # + # <p>"painted": The given mark may receive events when the mouse is over a + # "painted" area. The painted areas are the interior (i.e., fill) of the mark + # if a 'fillStyle' is specified, and the perimeter (i.e., stroke) of the mark + # if a 'strokeStyle' is specified. + # + # <p>"all": The given mark may receive events when the mouse is over either the + # interior (i.e., fill) or the perimeter (i.e., stroke) of the mark, regardless + # of the specified fillStyle and strokeStyle. + # + # <p>"none": The given mark may not receive events. + + ## + # :attr: reverse + # The reverse property; a boolean determining whether marks are ordered from + # front-to-back or back-to-front. SVG does not support explicit z-ordering; + # shapes are rendered in the order they appear. Thus, by default, marks are + # rendered in data order. Setting the reverse property to false reverses the + # order in which they are rendered; however, the properties are still evaluated + # (i.e., built) in forward order. + + ## + # :attr: id + # The instance identifier, for correspondence across animated transitions. If + # no identifier is specified, correspondence is determined using the mark + # index. Identifiers are not global, but local to a given mark. + + # + + attr_accessor_dsl :data, :visible, :left, :right, :top, :bottom, :cursor, :title, :reverse, :antialias, :events, :id @scene=nil @stack=[] @index=nil + # @private Records which properties are defined on this mark type. def self.properties @properties end + # Common index for all marks def Mark.index @index end + + # Set common index for all marks def Mark.index=(v) @index=v end - - - + # Get common scene for all marks def self.scene @scene end + # Set common scene for all marks def self.scene=(v) @scene=v end + + + # Return properties for current mark def properties (self.class).properties end + # Get common stack for all marks def Mark.stack @stack end + # Set common stack for all marks def Mark.stack=(v) @stack=v end + # Create a new Mark def initialize(opts=Hash.new) @_properties=[] opts.each {|k,v| self.send("#{k}=",v) if self.respond_to? k } @@ -146,24 +388,43 @@ @child_index=-1 @index=-1 @index_defined = true @scale=1 @scene=nil - end + + + # The mark type; a lower name. The type name controls rendering + # behavior, and unless the rendering engine is extended, must be one of the + # built-in concrete mark types: area, bar, dot, image, label, line, panel, + # rule, or wedge. def type "mark" end + + # Default properties for all mark types. By default, the data array is the + # parent data as a single-element array; if the data property is not + # specified, this causes each mark to be instantiated as a singleton + # with the parents datum. The visible property is true by default, + # and the reverse property is false. def self.defaults Mark.new({:data=>lambda {|d| [d]}, :visible=>true, :antialias=>true, :events=>'painted'}) end + + # Sets the prototype of this mark to the specified mark. Any properties not + # defined on this mark may be inherited from the specified prototype mark, + # or its prototype, and so on. The prototype mark need not be the same + # type of mark as this mark. (Note that for inheritance to be useful, + # properties with the same name on different mark types should + # have equivalent meaning.) def extend(proto) @proto=proto @target=proto.target self end - + # Find the instances of this mark that match source. + # @see Anchor def instances(source) mark = self _index = [] scene=nil @@ -186,47 +447,91 @@ if (index_defined?) s = scene[self.index].dup s.right = s.top = s.left = s.bottom = 0; return [s]; end - return scene; + scene end + + + #Returns the previous instance of this mark in the scene graph, or + # null if this is the first instance. + # + # @return a node in the scene graph, or null. def sibling (self.index==0) ? nil: self.scene[self.index-1] end + + + # Returns the current instance in the scene graph of this mark, + # in the previous instance of the enclosing parent panel. + # May return null if this instance could not be found. + # + # @return a node in the scene graph, or null. def cousin par=self.parent s= par ? par.sibling : nil (s and s.children) ? s.children[self.child_index][self.index] : nil end def add(type) parent.add(type).extend(self) end + # Returns an anchor with the specified name. All marks support the five + # standard anchor names: + # * top + # * left + # * center + # * bottom + # * right + # + # In addition to positioning properties (left, right, top bottom), the + # anchors support text rendering properties (text-align, text-baseline). + # Text is rendered to appear inside the mark by default. + # + # To facilitate stacking, anchors are defined in terms of their opposite + # edge. For example, the top anchor defines the bottom property, + # such that the mark extends upwards; the bottom anchor instead defines + # the top property, such that the mark extends downwards. See also Layout::Stack + # + # While anchor names are typically constants, the anchor name is a true + # property, which means you can specify a function to compute the anchor name + # dynamically. See the Anchor#name property for details. + # + # @param [String] name the anchor name; either a string or a property function. + # @return [Anchor] the new anchor. def anchor(name='center') mark_anchor(name) end - def mark_anchor(name="center") - anchor=Rubyvis::Anchor.new(self).name(name).data(lambda { + # Implementation of mark anchor + def mark_anchor(name="center") # :nodoc: + anchor=Rubyvis::Anchor. + new(self). + name(name). + data(lambda { pp self.scene.target if $DEBUG - a=self.scene.target.map {|s| puts "s:#{s.data}" if $DEBUG; s.data} - p a if $DEBUG - a - }).visible(lambda { - self.scene.target[self.index].visible - }).id(lambda {self.scene.target[self.index].id}).left(lambda { + a=self.scene.target.map {|s| puts "s:#{s.data}" if $DEBUG; s.data} + p a if $DEBUG + a + }). + visible(lambda { + self.scene.target[self.index].visible + }). + id(lambda {self.scene.target[self.index].id}). + left(lambda { + s = self.scene.target[self.index] + w = s.width + w||=0 + if ['bottom','top','center'].include?(self.name) + s.left + w / 2.0 + elsif self.name=='left' + nil + else + s.left + w + end + }). + top(lambda { s = self.scene.target[self.index] - w = s.width - w||=0 - if ['bottom','top','center'].include?(self.name) - s.left + w / 2.0 - elsif self.name=='left' - nil - else - s.left + w - end - }).top(lambda { - s = self.scene.target[self.index] h = s.height h||= 0 if ['left','right','center'].include? self.name s.top+h/2.0 elsif self.name=='top' @@ -259,16 +564,23 @@ }) return anchor end - - def build_implied(s) + # Computes the implied properties for this mark for the specified + # instance <tt>s</tt> in the scene graph. Implied properties are those with + # dependencies on multiple other properties; for example, the width property + # may be implied if the left and right properties are set. This method can be + # overridden by concrete mark types to define new implied properties, if + # necessary. + # + # @param s a node in the scene graph; the instance of the mark to build. + def build_implied(s) # :nodoc: mark_build_implied(s) end - - def mark_build_implied(s) + + def mark_build_implied(s) # :nodoc: l=s.left r=s.right t=s.top b=s.bottom prop=self.properties @@ -328,10 +640,21 @@ s.height=h if prop[:height] s.text_style=Rubyvis::Color.transparent if prop[:text_style] and !s.text_style s.fill_style=Rubyvis::Color.transparent if prop[:fill_style] and !s.fill_style s.stroke_style=Rubyvis::Color.transparent if prop[:stroke_style] and !s.stroke_style end + # Renders this mark, including recursively rendering all child marks if this is + # a panel. This method finds all instances of this mark and renders them. This + # method descends recursively to the level of the mark to be rendered, finding + # all visible instances of the mark. After the marks are rendered, the scene + # and index attributes are removed from the mark to restore them to a clean + # state. + # + # <p>If an enclosing panel has an index property set (as is the case inside in + # an event handler), then only instances of this mark inside the given instance + # of the panel will be rendered; otherwise, all visible instances of the mark + # will be rendered. def render parent=self.parent @stack=Mark.stack if parent and !self.root.scene root.render() @@ -349,11 +672,11 @@ self.context( parent ? parent.scene : nil, parent ? parent.index : -1, lambda {render_render(self.root, 0,1)}) end - def render_render(mark,depth,scale) + def render_render(mark,depth,scale) # :nodoc: mark.scale=scale if (depth < @indexes.size) @stack.unshift(nil) if (mark.index_defined?) render_instance(mark, depth, scale); @@ -365,18 +688,18 @@ mark.delete_index end stack.shift else mark.build - pv.Scene.scale = scale; - pv.Scene.update_all(mark.scene); + Rubyvis.Scene.scale = scale; + Rubyvis.Scene.update_all(mark.scene); end mark.scale=nil end - def render_instance(mark,depth,scale) + def render_instance(mark,depth,scale) # :nodoc: s=mark.scene[mark.index] if s.visible child_index=@indexes[depth] child=mark.children[child_index] child_index.times {|i| @@ -395,11 +718,10 @@ mark.children[i].scene=nil } end end - private :render_render, :render_instance def bind_bind(mark) begin mark._properties.each {|v| #p v.name k=v.name @@ -417,16 +739,18 @@ end end } end while(mark = mark.proto) end - - attr_accessor :binds + private :bind_bind, :render_render, :render_instance + # @private In the bind phase, inherited property definitions are cached so they + # do not need to be queried during build. def bind mark_bind end - def mark_bind() + + def mark_bind() # :nodoc: @seen={} @types={1=>[],2=>[],3=>[]} @_data=nil @_required=[] bind_bind(self) @@ -515,10 +839,42 @@ context_clear(scene,index) context_apply(oscene,oindex) end end + # Evaluates properties and computes implied properties. Properties are + # stored in the Mark.scene array for each instance of this mark. + # + # As marks are built recursively, the Mark.index property is updated to + # match the current index into the data array for each mark. Note that the + # index property is only set for the mark currently being built and its + # enclosing parent panels. The index property for other marks is unset, but is + # inherited from the global +Mark+ class prototype. This allows mark + # properties to refer to properties on other marks <i>in the same panel</i> + # conveniently; however, in general it is better to reference mark instances + # specifically through the scene graph rather than depending on the magical + # behavior of Mark#index. + # + # The root scene array has a special property, <tt>data</tt>, which stores + # the current data stack. The first element in this stack is the current datum, + # followed by the datum of the enclosing parent panel, and so on. The data + # stack should not be accessed directly; instead, property functions are passed + # the current data stack as arguments. + # + # <p>The evaluation of the <tt>data</tt> and <tt>visible</tt> properties is + # special. The <tt>data</tt> property is evaluated first; unlike the other + # properties, the data stack is from the parent panel, rather than the current + # mark, since the data is not defined until the data property is evaluated. + # The <tt>visible</tt> property is subsequently evaluated for each instance; + # only if true will the {@link #buildInstance} method be called, evaluating + # other properties and recursively building the scene graph. + # + # <p>If this mark is being re-built, any old instances of this mark that no + # longer exist (because the new data array contains fewer elements) will be + # cleared using {@link #clearInstance}. + # + # @param parent the instance of the parent panel from the scene graph. def build scene=self.scene stack=Mark.stack if(!scene) @@ -561,21 +917,30 @@ Mark.index=-1 delete_index stack.shift() self end - - def build_instance(s) + # @private + def build_instance(s) # :nodoc: mark_build_instance(s) end - def mark_build_instance(s1) + # @private + def mark_build_instance(s1) # :nodoc: build_properties(s1, self.binds.required) if s1.visible build_properties(s1, self.binds.optional) build_implied(s1) end end + + + + # Evaluates the specified array of properties for the specified + # instance <tt>s</tt> in the scene graph. + # + # @param s a node in the scene graph; the instance of the mark to build. + # @param properties an array of properties. def build_properties(ss, props) #p props props.each do |prop| v=prop.value @@ -583,14 +948,14 @@ if prop._type==3 v=v.js_apply(self, Mark.stack) end ss.send((prop.name.to_s+"=").to_sym, v) end - # p ss end + # @todo implement def event(type,handler) #@_handlers[type]=handler - return self + self end end end require 'rubyvis/mark/anchor'