3 const th
= require('./template-helper');
5 const optionsDefaults
= {
9 barCaptionFn: () => '',
13 fontFamily: 'DejaVu Sans,Verdana,Geneva,sans-serif',
22 function grey(percent
) {
23 const value
= Math
.round(95 * (1.0 - percent
));
24 return `rgb(${value}%, ${value}%, ${value}%)`;
27 function svgHeader(options
, width
, height
) {
32 xmlns="http://www.w3.org/2000/svg"
33 xmlns:xlink="http://www.w3.org/1999/xlink"
34 xmlns:ev="http://www.w3.org/2001/xml-events"
35 width="${width}px" height="${height}px"
36 viewBox="0 0 ${width} ${height}"
37 preserveAspectRatio="xMinYMin meet"`,
38 options
.accessibleText
? ` role="img" aria-label="${th.xmlEscape(options.accessibleText)}"` : '',
40 options
.title
? `<title>${th.xmlEscape(options.title)}</title>` : '',
41 options
.description
? `<desc>${th.xmlEscape(options.description)}</desc>` : '',
46 function svgFrame(options
, width
) {
47 if (!options
.frameColor
) {
50 return `\t<g id="frame">
51 \t\t<rect x="0.5px" y="0.5px" width="${width - 1}px" height="${options.barHeight - 1}px" fill="none" stroke="${options.frameColor}" />
55 function svgTicks(options
, width
) {
56 if (!options
.tickEvery
) {
59 const tickSpacing
= options
.tickEvery
* options
.barWidth
;
60 const tickOffset
= options
.barWidth
/ 2;
61 const numTicks
= Math
.ceil(width
/ tickSpacing
);
62 const ticks
= Array
.from({ length: numTicks
}, (_
, i
) => i
)
63 .map((index
) => `M ${(index * tickSpacing) + tickOffset} ${options.barHeight - 2} v ${options.tickHeight}`)
66 return `\t<g id="ticks">
67 \t<path d="${ticks}" stroke="${options.tickColor}" fill="none" stroke-width="0.5px" />
71 function svgLabels(options
, width
, height
) {
73 if (!options
.labelHeight
) {
76 labels
.push(`\t<g font-size="${options.labelHeight}px" font-family="${options.fontFamily}" font-variant="small-caps">\n`);
77 const y
= height
- (options
.labelHeight
/ 2) + 2;
78 if (options
.labelZero
) {
79 labels
.push(`\t\t<text font-size="${options.labelHeight}px" text-anchor="start" x="0" y="${y}">${options.labelZero}</text>\n`);
82 labels
.push(`\t\t<text text-anchor="middle" x="${width / 2}" y="${y}">${options.labelX}</text>\n`);
84 labels
.push('\t</g>\n');
88 function svgFooter() {
92 function svgBar(options
, value
, index
, maxValue
) {
93 const id
= `i${index}`;
94 const x
= options
.barWidth
* index
;
95 const width
= options
.barWidth
;
96 const height
= options
.barHeight
;
97 const scale
= value
/ Math
.max(1, maxValue
);
98 const scaleHeight
= options
.scaleBars
? height
* scale : height
;
99 const yOffset
= height
- scaleHeight
;
100 const fill
= grey(scale
);
101 const emptyFill
= grey(0);
102 const title
= th
.xmlEscape(options
.barCaptionFn(index
, value
));
104 ...(options
.scaleBars
&& [
105 `\t<rect id="${id}" x="${x}" y="0" width="${width}" height="${height}" fill="${emptyFill}">`,
106 ...(title
&& `<title>${title}</title>`),
109 `\t<rect id="${id}" x="${x}" y="${yOffset}" width="${width}" height="${scaleHeight}" fill="${fill}">`,
110 ...(title
&& `<title>${title}</title>`),
115 module
.exports
= (items
, options
= {}) => {
116 options
= Object
.assign({}, optionsDefaults
, options
);
118 const maxValue
= items
.reduce((a
, b
) => Math
.max(a
, b
), 0);
119 const hasLabel
= !!options
.labelX
|| !!options
.labelZero
;
120 const height
= options
.barHeight
+ 2 + (hasLabel
? options
.labelHeight
+ 2 : 0);
121 const width
= Math
.max(items
.length
, options
.minItems
) * options
.barWidth
;
124 ...svgHeader(options
, width
, height
),
125 ...items
.slice(0, options
.maxItems
).map((value
, index
) => svgBar(options
, value
, index
, maxValue
)),
126 svgFrame(options
, width
),
127 svgTicks(options
, width
),
128 ...svgLabels(options
, width
, height
),