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'