|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731 |
- import { getBarHeightAndYAttr, truncateString, shortenLargeNumber, getSplineCurvePointsStr } from './draw-utils';
- import { getStringWidth, isValidNumber } from './helpers';
- import { DOT_OVERLAY_SIZE_INCR, PERCENTAGE_BAR_DEFAULT_DEPTH } from './constants';
- import { lightenDarkenColor } from './colors';
-
- export const AXIS_TICK_LENGTH = 6;
- const LABEL_MARGIN = 4;
- const LABEL_MAX_CHARS = 15;
- export const FONT_SIZE = 10;
- const BASE_LINE_COLOR = '#dadada';
- const FONT_FILL = '#555b51';
-
- function $(expr, con) {
- return typeof expr === "string"? (con || document).querySelector(expr) : expr || null;
- }
-
- export function createSVG(tag, o) {
- var element = document.createElementNS("http://www.w3.org/2000/svg", tag);
-
- for (var i in o) {
- var val = o[i];
-
- if (i === "inside") {
- $(val).appendChild(element);
- }
- else if (i === "around") {
- var ref = $(val);
- ref.parentNode.insertBefore(element, ref);
- element.appendChild(ref);
-
- } else if (i === "styles") {
- if(typeof val === "object") {
- Object.keys(val).map(prop => {
- element.style[prop] = val[prop];
- });
- }
- } else {
- if(i === "className") { i = "class"; }
- if(i === "innerHTML") {
- element['textContent'] = val;
- } else {
- element.setAttribute(i, val);
- }
- }
- }
-
- return element;
- }
-
- function renderVerticalGradient(svgDefElem, gradientId) {
- return createSVG('linearGradient', {
- inside: svgDefElem,
- id: gradientId,
- x1: 0,
- x2: 0,
- y1: 0,
- y2: 1
- });
- }
-
- function setGradientStop(gradElem, offset, color, opacity) {
- return createSVG('stop', {
- 'inside': gradElem,
- 'style': `stop-color: ${color}`,
- 'offset': offset,
- 'stop-opacity': opacity
- });
- }
-
- export function makeSVGContainer(parent, className, width, height) {
- return createSVG('svg', {
- className: className,
- inside: parent,
- width: width,
- height: height
- });
- }
-
- export function makeSVGDefs(svgContainer) {
- return createSVG('defs', {
- inside: svgContainer,
- });
- }
-
- export function makeSVGGroup(className, transform='', parent=undefined) {
- let args = {
- className: className,
- transform: transform
- };
- if(parent) args.inside = parent;
- return createSVG('g', args);
- }
-
- export function wrapInSVGGroup(elements, className='') {
- let g = createSVG('g', {
- className: className
- });
- elements.forEach(e => g.appendChild(e));
- return g;
- }
-
- export function makePath(pathStr, className='', stroke='none', fill='none', strokeWidth=2) {
- return createSVG('path', {
- className: className,
- d: pathStr,
- styles: {
- stroke: stroke,
- fill: fill,
- 'stroke-width': strokeWidth
- }
- });
- }
-
- export function makeArcPathStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
- let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
- let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y];
- return `M${center.x} ${center.y}
- L${arcStartX} ${arcStartY}
- A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
- ${arcEndX} ${arcEndY} z`;
- }
-
- export function makeCircleStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
- let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
- let [arcEndX, midArc, arcEndY] = [center.x + endPosition.x, center.y * 2, center.y + endPosition.y];
- return `M${center.x} ${center.y}
- L${arcStartX} ${arcStartY}
- A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
- ${arcEndX} ${midArc} z
- L${arcStartX} ${midArc}
- A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
- ${arcEndX} ${arcEndY} z`;
- }
-
- export function makeArcStrokePathStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
- let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
- let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y];
-
- return `M${arcStartX} ${arcStartY}
- A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
- ${arcEndX} ${arcEndY}`;
- }
-
- export function makeStrokeCircleStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
- let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
- let [arcEndX, midArc, arcEndY] = [center.x + endPosition.x, radius * 2 + arcStartY, center.y + startPosition.y];
-
- return `M${arcStartX} ${arcStartY}
- A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
- ${arcEndX} ${midArc}
- M${arcStartX} ${midArc}
- A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
- ${arcEndX} ${arcEndY}`;
- }
-
- export function makeGradient(svgDefElem, color, lighter = false) {
- let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default');
- let gradientDef = renderVerticalGradient(svgDefElem, gradientId);
- let opacities = [1, 0.6, 0.2];
- if(lighter) {
- opacities = [0.4, 0.2, 0];
- }
-
- setGradientStop(gradientDef, "0%", color, opacities[0]);
- setGradientStop(gradientDef, "50%", color, opacities[1]);
- setGradientStop(gradientDef, "100%", color, opacities[2]);
-
- return gradientId;
- }
-
- export function percentageBar(x, y, width, height,
- depth=PERCENTAGE_BAR_DEFAULT_DEPTH, fill='none') {
-
- let args = {
- className: 'percentage-bar',
- x: x,
- y: y,
- width: width,
- height: height,
- fill: fill,
- styles: {
- 'stroke': lightenDarkenColor(fill, -25),
- // Diabolically good: https://stackoverflow.com/a/9000859
- // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray
- 'stroke-dasharray': `0, ${height + width}, ${width}, ${height}`,
- 'stroke-width': depth
- },
- };
-
- return createSVG("rect", args);
- }
-
- export function heatSquare(className, x, y, size, radius, fill='none', data={}) {
- let args = {
- className: className,
- x: x,
- y: y,
- width: size,
- height: size,
- rx: radius,
- fill: fill
- };
-
- Object.keys(data).map(key => {
- args[key] = data[key];
- });
-
- return createSVG("rect", args);
- }
-
- export function legendBar(x, y, size, fill='none', label, truncate=false) {
- label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;
-
- let args = {
- className: 'legend-bar',
- x: 0,
- y: 0,
- width: size,
- height: '2px',
- fill: fill
- };
- let text = createSVG('text', {
- className: 'legend-dataset-text',
- x: 0,
- y: 0,
- dy: (FONT_SIZE * 2) + 'px',
- 'font-size': (FONT_SIZE * 1.2) + 'px',
- 'text-anchor': 'start',
- fill: FONT_FILL,
- innerHTML: label
- });
-
- let group = createSVG('g', {
- transform: `translate(${x}, ${y})`
- });
- group.appendChild(createSVG("rect", args));
- group.appendChild(text);
-
- return group;
- }
-
- export function legendDot(x, y, size, fill='none', label, truncate=false) {
- label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;
-
- let args = {
- className: 'legend-dot',
- cx: 0,
- cy: 0,
- r: size,
- fill: fill
- };
- let text = createSVG('text', {
- className: 'legend-dataset-text',
- x: 0,
- y: 0,
- dx: (FONT_SIZE) + 'px',
- dy: (FONT_SIZE/3) + 'px',
- 'font-size': (FONT_SIZE * 1.2) + 'px',
- 'text-anchor': 'start',
- fill: FONT_FILL,
- innerHTML: label
- });
-
- let group = createSVG('g', {
- transform: `translate(${x}, ${y})`
- });
- group.appendChild(createSVG("circle", args));
- group.appendChild(text);
-
- return group;
- }
-
- export function makeText(className, x, y, content, options = {}) {
- let fontSize = options.fontSize || FONT_SIZE;
- let dy = options.dy !== undefined ? options.dy : (fontSize / 2);
- let fill = options.fill || FONT_FILL;
- let textAnchor = options.textAnchor || 'start';
- return createSVG('text', {
- className: className,
- x: x,
- y: y,
- dy: dy + 'px',
- 'font-size': fontSize + 'px',
- fill: fill,
- 'text-anchor': textAnchor,
- innerHTML: content
- });
- }
-
- function makeVertLine(x, label, y1, y2, options={}) {
- if(!options.stroke) options.stroke = BASE_LINE_COLOR;
- let l = createSVG('line', {
- className: 'line-vertical ' + options.className,
- x1: 0,
- x2: 0,
- y1: y1,
- y2: y2,
- styles: {
- stroke: options.stroke
- }
- });
-
- let text = createSVG('text', {
- x: 0,
- y: y1 > y2 ? y1 + LABEL_MARGIN : y1 - LABEL_MARGIN - FONT_SIZE,
- dy: FONT_SIZE + 'px',
- 'font-size': FONT_SIZE + 'px',
- 'text-anchor': 'middle',
- innerHTML: label + ""
- });
-
- let line = createSVG('g', {
- transform: `translate(${ x }, 0)`
- });
-
- line.appendChild(l);
- line.appendChild(text);
-
- return line;
- }
-
- function makeHoriLine(y, label, x1, x2, options={}) {
- if(!options.stroke) options.stroke = BASE_LINE_COLOR;
- if(!options.lineType) options.lineType = '';
- if (options.shortenNumbers) label = shortenLargeNumber(label);
-
- let className = 'line-horizontal ' + options.className +
- (options.lineType === "dashed" ? "dashed": "");
-
- let l = createSVG('line', {
- className: className,
- x1: x1,
- x2: x2,
- y1: 0,
- y2: 0,
- styles: {
- stroke: options.stroke
- }
- });
-
- let text = createSVG('text', {
- x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN,
- y: 0,
- dy: (FONT_SIZE / 2 - 2) + 'px',
- 'font-size': FONT_SIZE + 'px',
- 'text-anchor': x1 < x2 ? 'end' : 'start',
- innerHTML: label+""
- });
-
- let line = createSVG('g', {
- transform: `translate(0, ${y})`,
- 'stroke-opacity': 1
- });
-
- if(text === 0 || text === '0') {
- line.style.stroke = "rgba(27, 31, 35, 0.6)";
- }
-
- line.appendChild(l);
- line.appendChild(text);
-
- return line;
- }
-
- export function yLine(y, label, width, options={}) {
- if (!isValidNumber(y)) y = 0;
-
- if(!options.pos) options.pos = 'left';
- if(!options.offset) options.offset = 0;
- if(!options.mode) options.mode = 'span';
- if(!options.stroke) options.stroke = BASE_LINE_COLOR;
- if(!options.className) options.className = '';
-
- let x1 = -1 * AXIS_TICK_LENGTH;
- let x2 = options.mode === 'span' ? width + AXIS_TICK_LENGTH : 0;
-
- if(options.mode === 'tick' && options.pos === 'right') {
- x1 = width + AXIS_TICK_LENGTH;
- x2 = width;
- }
-
- // let offset = options.pos === 'left' ? -1 * options.offset : options.offset;
-
- x1 += options.offset;
- x2 += options.offset;
-
- return makeHoriLine(y, label, x1, x2, {
- stroke: options.stroke,
- className: options.className,
- lineType: options.lineType,
- shortenNumbers: options.shortenNumbers
- });
- }
-
- export function xLine(x, label, height, options={}) {
- if (!isValidNumber(x)) x = 0;
-
- if(!options.pos) options.pos = 'bottom';
- if(!options.offset) options.offset = 0;
- if(!options.mode) options.mode = 'span';
- if(!options.stroke) options.stroke = BASE_LINE_COLOR;
- if(!options.className) options.className = '';
-
- // Draw X axis line in span/tick mode with optional label
- // y2(span)
- // |
- // |
- // x line |
- // |
- // |
- // ---------------------+-- y2(tick)
- // |
- // y1
-
- let y1 = height + AXIS_TICK_LENGTH;
- let y2 = options.mode === 'span' ? -1 * AXIS_TICK_LENGTH : height;
-
- if(options.mode === 'tick' && options.pos === 'top') {
- // top axis ticks
- y1 = -1 * AXIS_TICK_LENGTH;
- y2 = 0;
- }
-
- return makeVertLine(x, label, y1, y2, {
- stroke: options.stroke,
- className: options.className,
- lineType: options.lineType
- });
- }
-
- export function yMarker(y, label, width, options={}) {
- if(!options.labelPos) options.labelPos = 'right';
- let x = options.labelPos === 'left' ? LABEL_MARGIN
- : width - getStringWidth(label, 5) - LABEL_MARGIN;
-
- let labelSvg = createSVG('text', {
- className: 'chart-label',
- x: x,
- y: 0,
- dy: (FONT_SIZE / -2) + 'px',
- 'font-size': FONT_SIZE + 'px',
- 'text-anchor': 'start',
- innerHTML: label+""
- });
-
- let line = makeHoriLine(y, '', 0, width, {
- stroke: options.stroke || BASE_LINE_COLOR,
- className: options.className || '',
- lineType: options.lineType
- });
-
- line.appendChild(labelSvg);
-
- return line;
- }
-
- export function yRegion(y1, y2, width, label, options={}) {
- // return a group
- let height = y1 - y2;
-
- let rect = createSVG('rect', {
- className: `bar mini`, // remove class
- styles: {
- fill: `rgba(228, 234, 239, 0.49)`,
- stroke: BASE_LINE_COLOR,
- 'stroke-dasharray': `${width}, ${height}`
- },
- // 'data-point-index': index,
- x: 0,
- y: 0,
- width: width,
- height: height
- });
-
- if(!options.labelPos) options.labelPos = 'right';
- let x = options.labelPos === 'left' ? LABEL_MARGIN
- : width - getStringWidth(label+"", 4.5) - LABEL_MARGIN;
-
- let labelSvg = createSVG('text', {
- className: 'chart-label',
- x: x,
- y: 0,
- dy: (FONT_SIZE / -2) + 'px',
- 'font-size': FONT_SIZE + 'px',
- 'text-anchor': 'start',
- innerHTML: label+""
- });
-
- let region = createSVG('g', {
- transform: `translate(0, ${y2})`
- });
-
- region.appendChild(rect);
- region.appendChild(labelSvg);
-
- return region;
- }
-
- export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) {
- let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
- y -= offset;
-
- if(height === 0) {
- height = meta.minHeight;
- y -= meta.minHeight;
- }
-
- // Preprocess numbers to avoid svg building errors
- if (!isValidNumber(x)) x = 0;
- if (!isValidNumber(y)) y = 0;
- if (!isValidNumber(height, true)) height = 0;
- if (!isValidNumber(width, true)) width = 0;
-
- let rect = createSVG('rect', {
- className: `bar mini`,
- style: `fill: ${color}`,
- 'data-point-index': index,
- x: x,
- y: y,
- width: width,
- height: height
- });
-
- label += "";
-
- if(!label && !label.length) {
- return rect;
- } else {
- rect.setAttribute('y', 0);
- rect.setAttribute('x', 0);
- let text = createSVG('text', {
- className: 'data-point-value',
- x: width/2,
- y: 0,
- dy: (FONT_SIZE / 2 * -1) + 'px',
- 'font-size': FONT_SIZE + 'px',
- 'text-anchor': 'middle',
- innerHTML: label
- });
-
- let group = createSVG('g', {
- 'data-point-index': index,
- transform: `translate(${x}, ${y})`
- });
- group.appendChild(rect);
- group.appendChild(text);
-
- return group;
- }
- }
-
- export function datasetDot(x, y, radius, color, label='', index=0) {
- let dot = createSVG('circle', {
- style: `fill: ${color}`,
- 'data-point-index': index,
- cx: x,
- cy: y,
- r: radius
- });
-
- label += "";
-
- if(!label && !label.length) {
- return dot;
- } else {
- dot.setAttribute('cy', 0);
- dot.setAttribute('cx', 0);
-
- let text = createSVG('text', {
- className: 'data-point-value',
- x: 0,
- y: 0,
- dy: (FONT_SIZE / 2 * -1 - radius) + 'px',
- 'font-size': FONT_SIZE + 'px',
- 'text-anchor': 'middle',
- innerHTML: label
- });
-
- let group = createSVG('g', {
- 'data-point-index': index,
- transform: `translate(${x}, ${y})`
- });
- group.appendChild(dot);
- group.appendChild(text);
-
- return group;
- }
- }
-
- export function getPaths(xList, yList, color, options={}, meta={}) {
- let pointsList = yList.map((y, i) => (xList[i] + ',' + y));
- let pointsStr = pointsList.join("L");
-
- // Spline
- if (options.spline)
- pointsStr = getSplineCurvePointsStr(xList, yList);
-
- let path = makePath("M"+pointsStr, 'line-graph-path', color);
-
- // HeatLine
- if(options.heatline) {
- let gradient_id = makeGradient(meta.svgDefs, color);
- path.style.stroke = `url(#${gradient_id})`;
- }
-
- let paths = {
- path: path
- };
-
- // Region
- if(options.regionFill) {
- let gradient_id_region = makeGradient(meta.svgDefs, color, true);
-
- let pathStr = "M" + `${xList[0]},${meta.zeroLine}L` + pointsStr + `L${xList.slice(-1)[0]},${meta.zeroLine}`;
- paths.region = makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`);
- }
-
- return paths;
- }
-
- export let makeOverlay = {
- 'bar': (unit) => {
- let transformValue;
- if(unit.nodeName !== 'rect') {
- transformValue = unit.getAttribute('transform');
- unit = unit.childNodes[0];
- }
- let overlay = unit.cloneNode();
- overlay.style.fill = '#000000';
- overlay.style.opacity = '0.4';
-
- if(transformValue) {
- overlay.setAttribute('transform', transformValue);
- }
- return overlay;
- },
-
- 'dot': (unit) => {
- let transformValue;
- if(unit.nodeName !== 'circle') {
- transformValue = unit.getAttribute('transform');
- unit = unit.childNodes[0];
- }
- let overlay = unit.cloneNode();
- let radius = unit.getAttribute('r');
- let fill = unit.getAttribute('fill');
- overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
- overlay.setAttribute('fill', fill);
- overlay.style.opacity = '0.6';
-
- if(transformValue) {
- overlay.setAttribute('transform', transformValue);
- }
- return overlay;
- },
-
- 'heat_square': (unit) => {
- let transformValue;
- if(unit.nodeName !== 'circle') {
- transformValue = unit.getAttribute('transform');
- unit = unit.childNodes[0];
- }
- let overlay = unit.cloneNode();
- let radius = unit.getAttribute('r');
- let fill = unit.getAttribute('fill');
- overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
- overlay.setAttribute('fill', fill);
- overlay.style.opacity = '0.6';
-
- if(transformValue) {
- overlay.setAttribute('transform', transformValue);
- }
- return overlay;
- }
- };
-
- export let updateOverlay = {
- 'bar': (unit, overlay) => {
- let transformValue;
- if(unit.nodeName !== 'rect') {
- transformValue = unit.getAttribute('transform');
- unit = unit.childNodes[0];
- }
- let attributes = ['x', 'y', 'width', 'height'];
- Object.values(unit.attributes)
- .filter(attr => attributes.includes(attr.name) && attr.specified)
- .map(attr => {
- overlay.setAttribute(attr.name, attr.nodeValue);
- });
-
- if(transformValue) {
- overlay.setAttribute('transform', transformValue);
- }
- },
-
- 'dot': (unit, overlay) => {
- let transformValue;
- if(unit.nodeName !== 'circle') {
- transformValue = unit.getAttribute('transform');
- unit = unit.childNodes[0];
- }
- let attributes = ['cx', 'cy'];
- Object.values(unit.attributes)
- .filter(attr => attributes.includes(attr.name) && attr.specified)
- .map(attr => {
- overlay.setAttribute(attr.name, attr.nodeValue);
- });
-
- if(transformValue) {
- overlay.setAttribute('transform', transformValue);
- }
- },
-
- 'heat_square': (unit, overlay) => {
- let transformValue;
- if(unit.nodeName !== 'circle') {
- transformValue = unit.getAttribute('transform');
- unit = unit.childNodes[0];
- }
- let attributes = ['cx', 'cy'];
- Object.values(unit.attributes)
- .filter(attr => attributes.includes(attr.name) && attr.specified)
- .map(attr => {
- overlay.setAttribute(attr.name, attr.nodeValue);
- });
-
- if(transformValue) {
- overlay.setAttribute('transform', transformValue);
- }
- },
- };
|