Example #1
0
def write_polygon_projection_svg(f, facepaths, sheetwidth, padding, use_numbers, use_map, center_dot, comment):

    x, y = padding, padding
    w, cur_h = 0., 0.
    pos = []
    for i, face in enumerate(facepaths):
        min_x, min_y, max_x, max_y = face['bbox']
        face_w, face_h = max_x-min_x, max_y-min_y
        if x + face_w > sheetwidth:
            y += cur_h + padding
            x, cur_h = padding, 0.

        pos.append( (x-min_x, y-min_y) )
        cur_h = max(cur_h, face_h)
        x += face_w + padding
        w = max(w, x)

    h = y + cur_h + padding
    w, h = int(math.ceil(w)), int(math.ceil(h))

    f.write(svg.header(w,h))
    f.write('<title><![CDATA['+comment+']]></title>')

    for i, face in enumerate(facepaths):

        x, y = pos[i]
        borders_path = svg.polygon_path(face['borders'])
#        borders_path = svg.polygon_path(face['borders']) + ''.join(svg.polygon_path(s) for s in face['slots'])
        projection_path = svg.polygon_multipath(face['projection'])

        borders = svg.path( borders_path, style=cut )

        if use_map:
            engraving = svg.group( svg.path( borders_path, style=engrave )+
                                   svg.path( projection_path, style=engrave ),
                                   id='engrave_'+str(i) )
        else:
            engraving = ''

        if use_numbers:
            text = svg.text(0,2.5, str(i), style=commentstyle+';'+textstyle )

            for n, a, b in zip(face['neighbours'],
                               face['points'], face['points'][1:]+face['points'][:1]):
                x1, y1 = a
                x2, y2 = b
                dx, dy = (x1+x2)/3., (y1+y2)/3.+1.5
                text += svg.text(dx,dy, str(n), style=commentstyle+';'+smalltextstyle )
        else:
            text = ''

        if center_dot:
            dot = svg.circle(.1, style=cut)
        else:
            dot = ''

        f.write(svg.group( svg.group(borders + engraving) + text + dot, transform='translate('+str(x)+' '+str(y)+')', id='face_'+str(i) ))

    f.write(svg.footer())
Example #2
0
def write_polygon_projection_svg(f, facepaths, sheetwidth, padding, use_numbers, use_map, center_dot, comment):

    x, y = padding, padding
    w, cur_h = 0., 0.
    pos = []
    for i, face in enumerate(facepaths):
        min_x, min_y, max_x, max_y = face['bbox']
        face_w, face_h = max_x-min_x, max_y-min_y
        if x + face_w > sheetwidth:
            y += cur_h + padding
            x, cur_h = padding, 0.

        pos.append( (x-min_x, y-min_y) )
        cur_h = max(cur_h, face_h)
        x += face_w + padding
        w = max(w, x)

    h = y + cur_h + padding
    w, h = int(math.ceil(w)), int(math.ceil(h))

    f.write(svg.header(w,h))
    f.write('<title><![CDATA['+comment+']]></title>')

    for i, face in enumerate(facepaths):

        x, y = pos[i]
        borders_path = svg.polygon_path(face['borders'])
#        borders_path = svg.polygon_path(face['borders']) + ''.join(svg.polygon_path(s) for s in face['slots'])
        projection_path = svg.polygon_multipath(face['projection'])

        borders = svg.path( borders_path, style=cut )

        if use_map:
            engraving = svg.group( svg.path( borders_path, style=engrave )+
                                   svg.path( projection_path, style=engrave ),
                                   id='engrave_'+str(i) )
        else:
            engraving = ''

        if use_numbers:
            text = svg.text(0,2.5, str(i), style=commentstyle+';'+textstyle )

            for n, a, b in zip(face['neighbours'],
                               face['points'], face['points'][1:]+face['points'][:1]):
                x1, y1 = a
                x2, y2 = b
                dx, dy = (x1+x2)/3., (y1+y2)/3.+1.5
                text += svg.text(dx,dy, str(n), style=commentstyle+';'+smalltextstyle )
        else:
            text = ''

        if center_dot:
            dot = svg.circle(.1, style=cut)
        else:
            dot = ''

        f.write(svg.group( svg.group(borders + engraving) + text + dot, transform='translate('+str(x)+' '+str(y)+')', id='face_'+str(i) ))

    f.write(svg.footer())
Example #3
0
 def drawAt(self, canvas, upperLeft):
     x,y = upperLeft
     cx = x + self.Radius
     cy = y + self.Radius
     canvas += svg.circle(cx=cx, cy=cy, r=self.Radius, 
                          fill='white', stroke='black', class_=self.name, id_=self.key())
     
     lowerRight = (x + 2*self.Radius, y + 2 * self.Radius)
     return lowerRight
Example #4
0
    def drawAt(self, canvas, upperLeft):
        x,y = upperLeft
        cx = x + self.Radius
        cy = y + self.Radius
        
        if self._slot.ready():
            fill = 'white'
        elif self._slot._optional:
            fill = 'blue'
        else:
            fill = 'red'

        canvas += svg.circle(cx=cx, cy=cy, r=self.Radius, 
                             fill=fill, stroke='black', class_=self.name, id_=self.key())
        
        lowerRight = (x + 2*self.Radius, y + 2 * self.Radius)
        return lowerRight
Example #5
0
def main():
    parser = argparse.ArgumentParser(description='Generate gears.')
    parser.add_argument("input", help="JSON filename to read")
    args = parser.parse_args()

    data = json.load(open(args.input))
    pieces = data["pieces"]

    out = sys.stdout
    x_multiplier = 1.0
    cz_vertical_offset = 5 * DPI * 0

    svg.header(out, WIDTH * x_multiplier, HEIGHT * 7)

    # Make cuts.
    for piece_index, piece in enumerate(data["pieces"]):
        cx = piece["cx"] * x_multiplier
        cy = piece["cy"] + piece["cz"] * cz_vertical_offset
        color = piece["color"]

        # Always use black for cut, it makes it easier to see in AI.
        color = "black"

        name = "%s_%d" % (piece["type"], piece_index)
        svg.start_group(out, name, cx, cy)

        p = piece["points"]
        svg.start_group(out, name + "_body")
        svg.polyline(out, p, color)
        svg.end_group(out)

        if "hole_radius" in piece:
            svg.start_group(out, name + "_hole")
            svg.circle(out, 0, 0, piece["hole_radius"], color)
            svg.end_group(out)
        if "bind" in piece:
            bind = piece["bind"]
            bind_radius = bind["hole_radius"]
            svg.start_group(out, name + "_bind")
            for center in bind["centers"]:
                svg.circle(out, center[0], center[1], bind_radius, color)
            svg.end_group(out)
        if "holes" in piece:
            # Also cut holes from all the axles.
            svg.start_group(out, name + "_holes")
            for hole in piece["holes"]:
                svg.circle(out, hole["cx"], hole["cy"], hole["r"], color)
            svg.end_group(out)

        svg.end_group(out)

    svg.footer(out)
Example #6
0
    def drawAt(self, canvas, upperLeft):
        x, y = upperLeft
        cx = x + self.Radius
        cy = y + self.Radius

        if self._slot.ready():
            fill = 'white'
        elif self._slot._optional:
            fill = 'blue'
        else:
            fill = 'red'

        canvas += svg.circle(cx=cx,
                             cy=cy,
                             r=self.Radius,
                             fill=fill,
                             stroke='black',
                             class_=self.name,
                             id_=self.key())

        lowerRight = (x + 2 * self.Radius, y + 2 * self.Radius)
        return lowerRight
Example #7
0
def makecircle(magnitude, salience, benefit=1):
    if c.swapsaliencemagnitude:
        tm = salience[:]
        salience = magnitude[:]
        magnitude = tm
    
    #general variables
    indicatorseparatorangles = [-math.pi*2/21*i + math.pi/21 for i in range(0,22)]
    indicatorcenterangles = [-math.pi*2/21*i for i in range(0,21)]
    wedgeangle = math.pi*2/21
    
    
    out = svg.svg(width=1000, height=1000+100*c.saliencelegend, viewbox='0 0 1000 1%d00' % (1*c.saliencelegend==True))
    
    if(benefit):
        colors=c.colorsbenefit
    else:
        colors=c.colorscost
    if c.centerlabel is not None:
        out.add(svg.text(c.centerlabel[1-benefit], 500, 500, fontsize=c.font, p='style="text-anchor: middle; dominant-baseline: middle"'))
    
    #draw wedges
    for i in range(0,21):
        r = ringradii(magnitude[i]+c.includezeromagnitude)
        if(c.saliencebywidth==1):
            wedgewidth = wedgeangle/2/(3+c.includezerosalience)*(salience[i]+c.includezerosalience)
        elif(c.saliencebywidth>1):
            wedgewidth = wedgeangle/2/(saliencebywidth**3)*(c.saliencebywidth**salience[i])
        else:
            wedgewidth = wedgeangle/2
        x1 = (500 + math.cos(indicatorcenterangles[i]+wedgewidth)*c.centerradius)
        y1 = (500 + math.sin(indicatorcenterangles[i]+wedgewidth)*c.centerradius)
        x2 = (500 + math.cos(indicatorcenterangles[i]+wedgewidth)*r)
        y2 = (500 + math.sin(indicatorcenterangles[i]+wedgewidth)*r)
        x3 = (500 + math.cos(indicatorcenterangles[i]-wedgewidth)*r)
        y3 = (500 + math.sin(indicatorcenterangles[i]-wedgewidth)*r)
        x4 = (500 + math.cos(indicatorcenterangles[i]-wedgewidth)*c.centerradius)
        y4 = (500 + math.sin(indicatorcenterangles[i]-wedgewidth)*c.centerradius)
        if(c.saliencebycolor):
            s = ((salience[i]+c.includezerosalience)**c.saliencebycolor)/((3.0+c.includezerosalience)**c.saliencebycolor)
        else:
            s = 1
        red,green,blue = colors[i//7]
        d = 'M %s,%s L %s,%s A %s,%s 0 0,0 %s,%s L %s,%s A %s,%s 0 0,1 %s,%s' % (x1,y1,x2,y2,r,r,x3,y3,x4,y4,c.centerradius,c.centerradius,x1,y1)
        if(salience[i]>0 or c.includezerosalience):
            out.add(svg.path(d, fill='rgb(%s,%s,%s)' % (red,green,blue), p='fill-opacity="%s"' % s))
        
            
            
    #draw dividers and text
    for i in range(0,21):
        
        if(c.saliencebywidth and c.drawsaliencedividers):
            x1 = (500 + math.cos(indicatorcenterangles[i])*c.centerradius)
            y1 = (500 + math.sin(indicatorcenterangles[i])*c.centerradius)
            x2 = (500 + math.cos(indicatorcenterangles[i])*c.edgeradius)
            y2 = (500 + math.sin(indicatorcenterangles[i])*c.edgeradius)
            out.add(svg.line(x1, y1, x2, y2, 'black', .5))
            for j in range(1-c.includezerosalience,3):
                if(c.saliencebywidth>1):
                    wedgewidth = wedgeangle/2/(c.saliencebywidth**3)*(c.saliencebywidth**j)
                else:
                    wedgewidth = wedgeangle/2/(3+c.includezerosalience)*(j+c.includezerosalience)
                for k in [1,-1]:
                    x1 = (500 + math.cos(indicatorcenterangles[i]+wedgewidth*k)*c.centerradius)
                    y1 = (500 + math.sin(indicatorcenterangles[i]+wedgewidth*k)*c.centerradius)
                    x2 = (500 + math.cos(indicatorcenterangles[i]+wedgewidth*k)*c.edgeradius)
                    y2 = (500 + math.sin(indicatorcenterangles[i]+wedgewidth*k)*c.edgeradius)
                    out.add(svg.line(x1, y1, x2, y2, 'black', .5))
        
        #invisible link and tooltip box
        x1 = (500 + math.cos(indicatorcenterangles[i]+wedgeangle/2)*c.centerradius)
        y1 = (500 + math.sin(indicatorcenterangles[i]+wedgeangle/2)*c.centerradius)
        x2 = (500 + math.cos(indicatorcenterangles[i]+wedgeangle/2)*c.textradius+25)
        y2 = (500 + math.sin(indicatorcenterangles[i]+wedgeangle/2)*c.textradius+25)
        x3 = (500 + math.cos(indicatorcenterangles[i]-wedgeangle/2)*c.textradius+25)
        y3 = (500 + math.sin(indicatorcenterangles[i]-wedgeangle/2)*c.textradius+25)
        x4 = (500 + math.cos(indicatorcenterangles[i]-wedgeangle/2)*c.centerradius)
        y4 = (500 + math.sin(indicatorcenterangles[i]-wedgeangle/2)*c.centerradius)
        d = 'M %s,%s L %s,%s L %s,%s L %s,%s z' % (x1,y1,x2,y2,x3,y3,x4,y4)
        newpath = svg.path(d, fill='white', p='fill-opacity="0.0"')
        newpath.add(svg.title(' %s: %s\n Magnitude %s\tSalience %s' % (c.indicatornames[i], c.indicatorfullnames[i], magnitude[i], salience[i])))
        out.add(newpath)

        x = (500 + math.cos(indicatorcenterangles[i])*c.textradius)
        y = (500 + math.sin(indicatorcenterangles[i])*c.textradius)
        out.add(svg.text(c.indicatornames[i], x, y, fontsize=c.font, p='style="text-anchor: middle; dominant-baseline: middle"'))
        
        # draw dividers
        x1 = 500 + math.cos(indicatorseparatorangles[i])*c.centerradius
        y1 = 500 + math.sin(indicatorseparatorangles[i])*c.centerradius
        if(i % 7 == 0):
            x2 = 500 + math.cos(indicatorseparatorangles[i])*(c.textradius+25)
            y2 = 500 + math.sin(indicatorseparatorangles[i])*(c.textradius+25)
            out.add(svg.line(x1, y1, x2, y2, 'black', 10))
        elif (c.xgrid or (c.saliencebywidth and c.drawsaliencedividers)):
                x2 = 500 + math.cos(indicatorseparatorangles[i])*(c.textradius)
                y2 = 500 + math.sin(indicatorseparatorangles[i])*(c.textradius)
                out.add(svg.line(x1, y1, x2, y2, 'black', 2))
    
    #draw rings
    for i in range(0,c.maxvalue+c.includezeromagnitude+1):
        if(i==c.includezeromagnitude):
            w=5
            out.add(svg.circle('500','500',ringradii(i),'black',w,'none'))
        elif (c.ygrid or i==0):
            w=1
            out.add(svg.circle('500','500',ringradii(i),'black',w,'none'))

    return out
Example #8
0
        jagged_longedge(c, d, angle, thickness, overhang, overcut),
        jagged_shortedge(d, a, angle, thickness, overhang, overcut),
    )
    return [ c for e in edges for c in e ]


def slots(radius):
    edges = []
    a, b, c, d = ( (x*radius, y*radius) for x,y in dhxdron_deltoid_2d_coords() )
    
    return (
        slot_short(a, b, native_scale),
        slot_long(b, c, native_scale),
        slot_long(c, d, native_scale),
        slot_short(d, a, native_scale),
    )

style = 'stroke:none;fill:#0000ff;opacity:.3'

print svg.header(radius*2,radius*2)

print '<g transform="translate('+str(radius)+' '+str(radius)+')">'
print svg.circle(radius, style)
print svg.path( svg.polygon_path(shape(radius=radius, thickness=thickness)), style)
for slot in slots(radius=radius):
    print svg.path( svg.polygon_path(slot), style)

print '</g>'
print svg.footer()

Example #9
0
def makecircle(magnitude, salience, benefit=1):
    if c.swapsaliencemagnitude:
        tm = salience[:]
        salience = magnitude[:]
        magnitude = tm

    #general variables
    indicatorseparatorangles = [
        -math.pi * 2 / 21 * i + math.pi / 21 for i in range(0, 22)
    ]
    indicatorcenterangles = [-math.pi * 2 / 21 * i for i in range(0, 21)]
    wedgeangle = math.pi * 2 / 21

    out = svg.svg(width=1000,
                  height=1000 + 100 * c.saliencelegend,
                  viewbox='0 0 1000 1%d00' % (1 * c.saliencelegend == True))

    if (benefit):
        colors = c.colorsbenefit
    else:
        colors = c.colorscost
    if c.centerlabel is not None:
        out.add(
            svg.text(
                c.centerlabel[1 - benefit],
                500,
                500,
                fontsize=c.font,
                p='style="text-anchor: middle; dominant-baseline: middle"'))

    #draw wedges
    for i in range(0, 21):
        r = ringradii(magnitude[i] + c.includezeromagnitude)
        if (c.saliencebywidth == 1):
            wedgewidth = wedgeangle / 2 / (3 + c.includezerosalience) * (
                salience[i] + c.includezerosalience)
        elif (c.saliencebywidth > 1):
            wedgewidth = wedgeangle / 2 / (saliencebywidth**3) * (
                c.saliencebywidth**salience[i])
        else:
            wedgewidth = wedgeangle / 2
        x1 = (500 +
              math.cos(indicatorcenterangles[i] + wedgewidth) * c.centerradius)
        y1 = (500 +
              math.sin(indicatorcenterangles[i] + wedgewidth) * c.centerradius)
        x2 = (500 + math.cos(indicatorcenterangles[i] + wedgewidth) * r)
        y2 = (500 + math.sin(indicatorcenterangles[i] + wedgewidth) * r)
        x3 = (500 + math.cos(indicatorcenterangles[i] - wedgewidth) * r)
        y3 = (500 + math.sin(indicatorcenterangles[i] - wedgewidth) * r)
        x4 = (500 +
              math.cos(indicatorcenterangles[i] - wedgewidth) * c.centerradius)
        y4 = (500 +
              math.sin(indicatorcenterangles[i] - wedgewidth) * c.centerradius)
        if (c.saliencebycolor):
            s = ((salience[i] + c.includezerosalience)**c.saliencebycolor) / (
                (3.0 + c.includezerosalience)**c.saliencebycolor)
        else:
            s = 1
        red, green, blue = colors[i // 7]
        d = 'M %s,%s L %s,%s A %s,%s 0 0,0 %s,%s L %s,%s A %s,%s 0 0,1 %s,%s' % (
            x1, y1, x2, y2, r, r, x3, y3, x4, y4, c.centerradius,
            c.centerradius, x1, y1)
        if (salience[i] > 0 or c.includezerosalience):
            out.add(
                svg.path(d,
                         fill='rgb(%s,%s,%s)' % (red, green, blue),
                         p='fill-opacity="%s"' % s))

    #draw dividers and text
    for i in range(0, 21):

        if (c.saliencebywidth and c.drawsaliencedividers):
            x1 = (500 + math.cos(indicatorcenterangles[i]) * c.centerradius)
            y1 = (500 + math.sin(indicatorcenterangles[i]) * c.centerradius)
            x2 = (500 + math.cos(indicatorcenterangles[i]) * c.edgeradius)
            y2 = (500 + math.sin(indicatorcenterangles[i]) * c.edgeradius)
            out.add(svg.line(x1, y1, x2, y2, 'black', .5))
            for j in range(1 - c.includezerosalience, 3):
                if (c.saliencebywidth > 1):
                    wedgewidth = wedgeangle / 2 / (c.saliencebywidth**
                                                   3) * (c.saliencebywidth**j)
                else:
                    wedgewidth = wedgeangle / 2 / (
                        3 + c.includezerosalience) * (j +
                                                      c.includezerosalience)
                for k in [1, -1]:
                    x1 = (500 +
                          math.cos(indicatorcenterangles[i] + wedgewidth * k) *
                          c.centerradius)
                    y1 = (500 +
                          math.sin(indicatorcenterangles[i] + wedgewidth * k) *
                          c.centerradius)
                    x2 = (500 +
                          math.cos(indicatorcenterangles[i] + wedgewidth * k) *
                          c.edgeradius)
                    y2 = (500 +
                          math.sin(indicatorcenterangles[i] + wedgewidth * k) *
                          c.edgeradius)
                    out.add(svg.line(x1, y1, x2, y2, 'black', .5))

        #invisible link and tooltip box
        x1 = (500 + math.cos(indicatorcenterangles[i] + wedgeangle / 2) *
              c.centerradius)
        y1 = (500 + math.sin(indicatorcenterangles[i] + wedgeangle / 2) *
              c.centerradius)
        x2 = (500 + math.cos(indicatorcenterangles[i] + wedgeangle / 2) *
              c.textradius + 25)
        y2 = (500 + math.sin(indicatorcenterangles[i] + wedgeangle / 2) *
              c.textradius + 25)
        x3 = (500 + math.cos(indicatorcenterangles[i] - wedgeangle / 2) *
              c.textradius + 25)
        y3 = (500 + math.sin(indicatorcenterangles[i] - wedgeangle / 2) *
              c.textradius + 25)
        x4 = (500 + math.cos(indicatorcenterangles[i] - wedgeangle / 2) *
              c.centerradius)
        y4 = (500 + math.sin(indicatorcenterangles[i] - wedgeangle / 2) *
              c.centerradius)
        d = 'M %s,%s L %s,%s L %s,%s L %s,%s z' % (x1, y1, x2, y2, x3, y3, x4,
                                                   y4)
        newpath = svg.path(d, fill='white', p='fill-opacity="0.0"')
        newpath.add(
            svg.title(' %s: %s\n Magnitude %s\tSalience %s' %
                      (c.indicatornames[i], c.indicatorfullnames[i],
                       magnitude[i], salience[i])))
        out.add(newpath)

        x = (500 + math.cos(indicatorcenterangles[i]) * c.textradius)
        y = (500 + math.sin(indicatorcenterangles[i]) * c.textradius)
        out.add(
            svg.text(
                c.indicatornames[i],
                x,
                y,
                fontsize=c.font,
                p='style="text-anchor: middle; dominant-baseline: middle"'))

        # draw dividers
        x1 = 500 + math.cos(indicatorseparatorangles[i]) * c.centerradius
        y1 = 500 + math.sin(indicatorseparatorangles[i]) * c.centerradius
        if (i % 7 == 0):
            x2 = 500 + math.cos(
                indicatorseparatorangles[i]) * (c.textradius + 25)
            y2 = 500 + math.sin(
                indicatorseparatorangles[i]) * (c.textradius + 25)
            out.add(svg.line(x1, y1, x2, y2, 'black', 10))
        elif (c.xgrid or (c.saliencebywidth and c.drawsaliencedividers)):
            x2 = 500 + math.cos(indicatorseparatorangles[i]) * (c.textradius)
            y2 = 500 + math.sin(indicatorseparatorangles[i]) * (c.textradius)
            out.add(svg.line(x1, y1, x2, y2, 'black', 2))

    #draw rings
    for i in range(0, c.maxvalue + c.includezeromagnitude + 1):
        if (i == c.includezeromagnitude):
            w = 5
            out.add(svg.circle('500', '500', ringradii(i), 'black', w, 'none'))
        elif (c.ygrid or i == 0):
            w = 1
            out.add(svg.circle('500', '500', ringradii(i), 'black', w, 'none'))

    return out
Example #10
0
def plot(ratios, 
    range_x     = (0, 2),
    major       = 1.0, 
    minor       = 5,
    title       = None,
    subtitle    = None, 
    colors      = {}):    
    
    display     = 800, 680
    margin_x    = 60, 60
    margin_y    =  20,  80 + 10 * (subtitle is not None) + 20 * (title is not None)
    
    area        = display[0] - sum(margin_x), sum(margin_y) - display[1]
    offset      = margin_x[0], display[1] - margin_y[0]
    
    # draw grid lines
    start, end  = range_x
    # epsilon to deal with rounding errors
    cells       = int((end - start) / major * minor + 1e-5)
    
    grid_minor  = []
    grid_major  = []
    ticks       = []
    labels      = []
    for i in range(cells + 1):
        x      = i * major / (minor * (end - start))
        v      = i * major /  minor
        m      = i % minor == 0
        
        screen = tuple(tuple(map(round, transform(x, area, offset))) for x in ((x, 0), (x, 1)))
        length = 12 if m else 6
        (grid_major if m else grid_minor).append(  
            svg.path(  (transform(screen[0], (1, 1), (0.5,  0)), 
                        transform(screen[1], (1, 1), (0.5, -1))), 
            classes = ('grid', 'grid-major' if m else 'grid-minor')))
        ticks.append( 
            svg.path(  (transform(screen[1], (1, 1), (0.5, -8)), 
                        transform(screen[1], (1, 1), (0.5, -8 - length))), 
            classes = ('tick',)))
        
        if m:
            labels.append(svg.text(str(round(v, 3)), 
                position    = transform(screen[1], (1, 1), (0, -16 - length)), 
                classes     = ('label-numeric', 'label-x') + (('unity',) if v == 1 else ())))
    # title and subtitle
    if type(title) is str:
        screen = tuple(map(round, transform((0.5, 1), area, offset)))
        labels.append(svg.text(title, 
            position    = transform(screen, (1, 1), (0, -70)), 
            classes     = ('title',)))
    if type(subtitle) is str:
        screen = tuple(map(round, transform((0.5, 1), area, offset)))
        labels.append(svg.text(subtitle, 
            position    = transform(screen, (1, 1), (0, -50)), 
            classes     = ('subtitle',)))
    
    # plot data 
    rows    = tuple((name, ratio, (
            margin_x[0] + area[0] * (ratio - range_x[0]) / (range_x[1] - range_x[0]),
            margin_y[1] + (i + 0.5) * 20))
        for i, (name, ratio) in enumerate(sorted(ratios.items(), key = lambda k: k[1])))
    # pixel coordinate of the y axis
    zero    = margin_x[0] + area[0] * (1.0 - range_x[0]) / (range_x[1] - range_x[0])
    
    legend      = tuple(svg.text(name, 
        (zero + 16 * (1 if ratio <= 1 else -1), screen[1]), 
        classes = ('label-legend', 'better' if ratio <= 1 else 'worse'))
        for name, ratio, screen in rows)
    percents    = tuple(svg.text('{:+.2f} %'.format((ratio - 1) * 100), 
        (screen[0] - 16 * (1 if ratio <= 1 else -1), screen[1]), 
        classes = ('label-percent', 'better' if ratio <= 1 else 'worse'))
        for name, ratio, screen in rows)
    
    stems       = tuple(svg.path((
            (zero,                                      screen[1]), 
            (screen[0] + 4 * (1 if ratio <= 1 else -1), screen[1])), 
        classes = ('stem', 'better' if ratio <= 1 else 'worse'))
        for name, ratio, screen in rows 
        if abs(screen[0] - zero) > 4)
    dots        = tuple(svg.circle(screen, radius = 4, 
        classes = ('dot', 'better' if ratio <= 1 else 'worse'))
        for name, ratio, screen in rows)
    
    style = '''
    rect.background 
    {
        fill:   white;
    }
    
    path.grid 
    {
        stroke-width: 1px;
        fill:   none;
    }
    path.grid-major 
    {
        stroke: #eeeeeeff;
    }
    path.grid-minor 
    {
        stroke: #f5f5f5ff;
    }
    
    path.tick 
    {
        stroke-width: 1px;
        stroke: #333333ff;
        fill:   none;
    }
    
    path.stem 
    {
        stroke-width: 1px;
        stroke-dasharray: 3 3;
    }
    circle.dot 
    {
        stroke-width: 2px;
        stroke: #666;
        fill:   none;
    }
    
    text
    {
        fill: #333333ff;
        font-family: 'SF Mono';
    }
    text.label-numeric 
    {
        font-size: 12px;
    }
    text.label-numeric.unity 
    {
        font-weight: 700;
    }
    text.label-x 
    {
        text-anchor: middle; 
        dominant-baseline: text-top;
    }
    
    text.label-legend, text.label-percent
    {
        font-size: 12px;
        dominant-baseline: middle;
    }
    text.label-percent 
    {
        font-weight: 700;
    }
    text.label-legend.better, text.label-percent.worse 
    {
        text-anchor: begin; 
    }
    text.label-legend.worse, text.label-percent.better 
    {
        text-anchor: end; 
    }
    
    text.title, text.subtitle 
    {
        text-anchor: middle; 
    }
    text.title 
    {
        font-size: 20px;
    }
    text.subtitle 
    {
        font-size: 12px;
    }
    ''' + '''
    circle.better, path.stem.better
    {{
        stroke: {color_fill_better}
    }}
    circle.worse, path.stem.worse
    {{
        stroke: {color_fill_worse}
    }}
    text.label-percent.better 
    {{
        fill: {color_better}
    }}
    text.label-percent.worse 
    {{
        fill: {color_worse}
    }}
    '''.format( ** colors )
    
    return svg.svg(display, style, 
        tuple(grid_minor + grid_major + ticks + labels) + legend + percents + stems + dots)