X-Git-Url: https://git.squeep.com/?a=blobdiff_plain;ds=sidebyside;f=src%2Ftemplate%2Fhistogram-svg.js;fp=src%2Ftemplate%2Fhistogram-svg.js;h=13d0c91becb0d4ae94f9dcfe454f8e279e1390c6;hb=737fbd003d5c4dfea81b667ef906f1c106a60612;hp=0000000000000000000000000000000000000000;hpb=17b4ff9c1974842c02071d2cd02144d2e5a99eb5;p=websub-hub diff --git a/src/template/histogram-svg.js b/src/template/histogram-svg.js new file mode 100644 index 0000000..13d0c91 --- /dev/null +++ b/src/template/histogram-svg.js @@ -0,0 +1,131 @@ +'use strict'; + +const th = require('./template-helper'); + +const optionsDefaults = { + barWidth: 20, + barHeight: 100, + scaleBars: true, + barCaptionFn: () => '', + labelZero: undefined, + labelX: undefined, + labelHeight: 8, + fontFamily: 'DejaVu Sans,Verdana,Geneva,sans-serif', + frameColor: 'gray', + tickEvery: undefined, + tickHeight: 4, + tickColor: 'gray', + minItems: 0, + maxItems: undefined, +}; + +function grey(percent) { + const value = Math.round(95 * (1.0 - percent)); + return `rgb(${value}%, ${value}%, ${value}%)`; +} + +function svgHeader(options, width, height) { + return [ + `', + options.title ? `${th.xmlEscape(options.title)}` : '', + options.description ? `${th.xmlEscape(options.description)}` : '', + '\n', + ]; +} + +function svgFrame(options, width) { + if (!options.frameColor) { + return ''; + } + return `\t +\t\t +\t\n`; +} + +function svgTicks(options, width) { + if (!options.tickEvery) { + return ''; + } + const tickSpacing = options.tickEvery * options.barWidth; + const tickOffset = options.barWidth / 2; + const numTicks = Math.ceil(width / tickSpacing); + const ticks = Array.from({ length: numTicks }, (_, i) => i) + .map((index) => `M ${(index * tickSpacing) + tickOffset} ${options.barHeight - 2} v ${options.tickHeight}`) + .join(' '); + + return `\t +\t +\t\n`; +} + +function svgLabels(options, width, height) { + const labels = []; + if (!options.labelHeight) { + return labels; + } + labels.push(`\t\n`); + const y = height - (options.labelHeight / 2) + 2; + if (options.labelZero) { + labels.push(`\t\t${options.labelZero}\n`); + } + if (options.labelX) { + labels.push(`\t\t${options.labelX}\n`); + } + labels.push('\t\n'); + return labels; +} + +function svgFooter() { + return ''; +} + +function svgBar(options, value, index, maxValue) { + const id = `i${index}`; + const x = options.barWidth * index; + const width = options.barWidth; + const height = options.barHeight; + const scale = value / Math.max(1, maxValue); + const scaleHeight = options.scaleBars ? height * scale : height; + const yOffset = height - scaleHeight; + const fill = grey(scale); + const emptyFill = grey(0); + const title = th.xmlEscape(options.barCaptionFn(index, value)); + return [ + ...(options.scaleBars && [ + `\t`, + ...(title && `${title}`), + '\t\n', + ]), + `\t`, + ...(title && `${title}`), + '\t\n', + ].join(''); +} + +module.exports = (items, options = {}) => { + options = Object.assign({}, optionsDefaults, options); + + const maxValue = items.reduce((a, b) => Math.max(a, b), 0); + const hasLabel = !!options.labelX || !!options.labelZero; + const height = options.barHeight + 2 + (hasLabel ? options.labelHeight + 2 : 0); + const width = Math.max(items.length, options.minItems) * options.barWidth; + + return [ + ...svgHeader(options, width, height), + ...items.slice(0, options.maxItems).map((value, index) => svgBar(options, value, index, maxValue)), + svgFrame(options, width), + svgTicks(options, width), + ...svgLabels(options, width, height), + svgFooter(), + ].join(''); +}; \ No newline at end of file