UNPKG

@shjeon0730/horseshoe-chart

Version:

a chart component looks like a horseshoe. basically, it is a high chart, but including multiple lines of text or link on the center. it will also support colored ranges, accessibility functionalities.

239 lines (219 loc) 7.97 kB
const svgUtil = require('@shjeon0730/svg-gen-utils'); const parseColor = (colorItem, idx, totalArr, elseColor) => { if (typeof colorItem === 'object') { if (colorItem.length !== null) { //colorItem is array if (colorItem.length === totalArr.length) { //array of colors for all levels colorItem = colorItem[idx]; if (typeof colorItem !== 'string') { throw Error( 'color should one of these : {(array of color:string) | (array of {boundary:[min:number,max:number], color:string}) | color:string ' ); } } else if (colorItem.length > 0) { for (const item of colorItem) { const { boundary, maxIdx, color } = item; if ( !color || (!( boundary && boundary.length === 2 && typeof boundary[0] === 'number' && typeof boundary[1] === 'number' ) && !(maxIdx && typeof maxIdx === 'number')) ) { throw Error( 'wrong format: {boundary:[min:number,max:number], color:string} or {maxIdx:number, color:string} is needed.' ); } if (boundary) { if (idx >= boundary[0] && idx <= boundary[1]) return color; else continue; } if (maxIdx) { if (idx <= maxIdx) return color; else continue; //default color } } return elseColor; //default color } else { throw Error( 'if you want to set colors for boundary, item should be more than 2' ); } } } if (typeof colorItem === 'string') return colorItem; else throw Error('color item should to be array or string'); }; const getPt = svgUtil.pt; const drawChart = (config) => { const outRadius = config.outRadius || 50; const inRadius = config.inRadius || 40; const roundEdgeSize = config.roundEdgeSize !== undefined ? config.roundEdgeSize : (outRadius - inRadius) / 2.0; const offsetX = config.offsetX === undefined ? outRadius / 2.0 : config.offsetX; const offsetY = config.offsetY === undefined ? outRadius / 2.0 : config.offsetY; const partialZero = config.partialZero === undefined ? 0.0001 : config.partialZero; const selColor = config.selColor || '#12395b'; const backColor = config.backColor || '#d8d8d8'; const ptMulAdd = (pt, mul = 1, addX = 0, addY = undefined) => { addY = addY === undefined || addY === null ? addX : addY; return { x: pt.x * mul + addX, y: pt.y * mul + addY }; }; const filled = config.filled || 0; let intFilled = Math.floor(filled); let partialFilled = filled - intFilled; partialFilled = partialFilled < partialZero ? 0 : partialFilled; //when the partial filled is smaller than partialZero(default:0.0001), consider it is 0. const points = []; const toRad = (ang) => ang / 180.0 * Math.PI; const angCos = (ang) => Math.cos(toRad(ang)); const angSin = (ang) => Math.sin(toRad(ang)); const levelBorderWidth = config.levelBorderWidth === undefined ? 1 : config.levelBorderWidth; const centerGap = config.centerGap || 60; const numOfUnits = config.numOfUnits || 8; const unitAngle = (360 - centerGap + levelBorderWidth) * 1.0 / numOfUnits; const startAngle = 90 + centerGap / 2.0; let partialPoint = null; for (let i = 0; i < numOfUnits; i++) { points.push({ start: getPt( angCos(startAngle + i * unitAngle), angSin(startAngle + i * unitAngle) ), end: getPt( angCos(startAngle + (i + 1) * unitAngle - levelBorderWidth), angSin(startAngle + (i + 1) * unitAngle - levelBorderWidth) ), }); if (partialFilled !== 0 && i === intFilled) { partialPoint = { start: getPt( angCos(startAngle + i * unitAngle), angSin(startAngle + i * unitAngle) ), end: getPt( angCos( startAngle + i * unitAngle + unitAngle * partialFilled - levelBorderWidth ), angSin( startAngle + i * unitAngle + unitAngle * partialFilled - levelBorderWidth ) ), }; } } // this graph has outbound, inbound, and startPt, endPt. // startPt and endPt is calculated based on the angle. // startAngle starts from 90 deg + gap/2. // 90 deg is bottom direction. each direction's axis is like thiis: // left: (-1,0), right: (1,0), top:(0, -1), bottom(0, 1). // when you multiply radius with the points which is calculated above, it will be the point in x-y plain. // ptMulAdd function is used to create a point with the distance from base point. const mapFunc = (pt, idx, arr) => { let color = idx < intFilled ? selColor : backColor; let strokeColor = idx < intFilled ? 'black' : 'gray'; const elseColor = idx < intFilled ? '#12395b' : 'gray'; color = parseColor(color, idx, arr, elseColor); const key = 'level-' + idx; const moreProps = { key, id: key }; if (idx === 0) { const startX = pt.start.x * inRadius + offsetX; const startY = pt.start.y * inRadius + offsetY; const pathUtil = svgUtil.path.pathUtils(getPt(startX, startY)); const builder = pathUtil.element; const edgeEnd = ptMulAdd(pt.start, outRadius, offsetX, offsetY); const elements = [ roundEdgeSize === 0 ? builder.line(edgeEnd) : builder.arc(edgeEnd, roundEdgeSize, 0, 1), builder.arc( ptMulAdd(pt.end, outRadius, offsetX, offsetY), outRadius, 0, 1 ), builder.line(ptMulAdd(pt.end, inRadius, offsetX, offsetY)), builder.arc( ptMulAdd(pt.start, inRadius, offsetX, offsetY), inRadius, 0, 0 ), ]; return pathUtil.render(elements, strokeColor, color, '0.1', moreProps); } else if (idx === arr.length - 1) { const startX = pt.start.x * outRadius + offsetX; const startY = pt.start.y * outRadius + offsetY; const pathUtil = svgUtil.path.pathUtils(getPt(startX, startY)); const builder = pathUtil.element; const edgeEnd = ptMulAdd(pt.end, inRadius, offsetX, offsetY); const elements = [ builder.arc( ptMulAdd(pt.end, outRadius, offsetX, offsetY), outRadius, 0, 1 ), roundEdgeSize === 0 ? builder.line(edgeEnd) : builder.arc(edgeEnd, roundEdgeSize, 0, 1), builder.arc( ptMulAdd(pt.start, inRadius, offsetX, offsetY), inRadius, 0, 0 ), builder.line(ptMulAdd(pt.start, outRadius, offsetX, offsetY)), ]; return pathUtil.render(elements, strokeColor, color, '0.1', moreProps); } else { const startX = pt.start.x * outRadius + offsetX; const startY = pt.start.y * outRadius + offsetY; const pathUtil = svgUtil.path.pathUtils(getPt(startX, startY)); const builder = pathUtil.element; const elements = [ builder.arc( ptMulAdd(pt.end, outRadius, offsetX, offsetY), outRadius, 0, 1 ), builder.line(ptMulAdd(pt.end, inRadius, offsetX, offsetY)), builder.arc( ptMulAdd(pt.start, inRadius, offsetX, offsetY), inRadius, 0, 0 ), builder.line(ptMulAdd(pt.start, outRadius, offsetX, offsetY)), ]; return pathUtil.render(elements, strokeColor, color, '0.1', moreProps); } }; const arr = points.map(mapFunc); if (partialPoint) { const partialElement = mapFunc(partialPoint, intFilled - 1, points); partialElement.attributes.key = partialElement.attributes.key + '_partial'; arr.push(partialElement); } return arr; }; module.exports = { getPt, drawChart, };