@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
JavaScript
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,
};