<data><y>9</y><y>15</y><y>11</y><y>6</y><y>5</y><y>10</y><y>8</y> <y>8</y><y>3</y><y>12</y><y>14</y><y>9</y><y>16</y><y>14</y></data>
And I want to look at the data as a histogram.
Uses SVG
100×100 space
There are n values, so each rectangle is 100/n wide.
The range of the data is max - min
The vertical space is therefore divided over 100/range, which we'll call vscale
The height of the rectangle for each value v is v × vscale
.
The SVG will look something like this:
<svg ...> <xf:repeat bind="values"> <rect width="{...}" height="{...}" x="{...}" y="{...}" /> </xf:repeat> </svg>
Note: SVG coordinate system is 'upside down'. (0,0) is top left, and goes downwards and to the right.
We load the data:
<instance id="data" src="data.xml"/>
Identify the values:
<bind ref="instance('data')/y" id="values"/>
The first thing we need is how many data values there are:
<bind ref="n" calculate="count(bind('values'))"/>
The width of each rectangle is:
<bind ref="width" calculate="100 div ../n" type="double"/>
Now we can fill in the horizontal values:
<svg ...> <xf:repeat bind="values"> <rect width="{width}" height="{...}" x="{(position()-1) * width}" y="{...}" /> </xf:repeat> </svg>
Get the minimum and maximum values:
<bind ref="min" calculate="min(bind('values'))"/> <bind ref="max" calculate="max(bind('values'))"/>
Calculate the range minimum and maximum, and the range:
<bind ref="rmin" calculate="min(0, ../min)"/> <bind ref="rmax" calculate="max(0, ../max)"/> <bind ref="range" calculate="../rmax - ../rmin"/>
And so the vertical scale is
<bind ref="vscale" calculate="100 div ../range" type="double"/>
Except if the range is zero (i.e. all values are zero), so we actually need
calculate="if(../range = 0, 1, 100 div ../range)"
Ideally we would just say
height="{. * vscale}"
Incredibly, although SVG allows lines to have negative length, rectangles are not allowed to have negative height.
So we have to do some extra work.
height="{abs(.) * vscale}"
So now we have a row of rectangles, of the correct height, of the correct width, at the correct horizontal position:
Now to fix the vertical position.
Currently all rectangles start at SVG's vertical zero, and the other end of the longest (positive) bar is where zero really ought to be.
That value is rmax
, so all we have to do is push all the bars
down by the difference between its value and rmax
. All negative
values just have to start at rmax
:
y="{vscale * if(. < 0, rmax, rmax - .) }"
Giving the final result:
<svg ...> <xf:repeat bind="values"> <rect width="{width}" height="{vscale * abs(.)}" x="{(position()-1) * width}" y="{vscale * if(. < 0, rmax, rmax - .) }" /> </xf:repeat> </svg>