Skip to content

Commit

Permalink
Improved donut arcs & dataLabels
Browse files Browse the repository at this point in the history
  • Loading branch information
graphieros committed Jan 26, 2024
1 parent 7e09fee commit fe5159b
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 85 deletions.
9 changes: 5 additions & 4 deletions playground/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ arrow({
})

function xyCb(item: any) {
console.log(item)
// console.log(item)
}

const xyDataset = [
Expand Down Expand Up @@ -117,7 +117,7 @@ function getArc(arc: any) {
}

function getLegend(legend: any) {
console.log(legend)
// console.log(legend)
}

let donut = chartDonut({
Expand Down Expand Up @@ -169,7 +169,8 @@ let donut = chartDonut({
showDataLabels: true,
donutRadiusRatio: 1,
dataLabelsRoundingValue: 1,
dataLabelsRoundingPercentage: 2
dataLabelsRoundingPercentage: 2,
dataLabelsAsDivs: false
},
callbacks: {
onClickArc: getArc,
Expand All @@ -179,7 +180,7 @@ let donut = chartDonut({
}
})

console.log(findArcMidpoint(donut.arcs[0].pathElement))
// console.log(findArcMidpoint(donut.arcs[0].pathElement))

const nuke = document.getElementById('nuke');

Expand Down
2 changes: 1 addition & 1 deletion savyg/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "savyg",
"private": false,
"version": "1.2.0",
"version": "1.2.1",
"description": "A savvy library to create svg elements and charts with ease",
"author": "Alec Lloyd Probert",
"repository": {
Expand Down
146 changes: 98 additions & 48 deletions savyg/src/utils_chart_donut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@ export type ChartDonutDatasetItem = StrokeOptions & {
export type ChartDonutOptions = {
backgroundColor?: string
className?: string
dataLabelsAsDivs?: boolean
dataLabelsColor?: string
dataLabelsFontSize?: number
dataLabelsRoundingPercentage?: number
dataLabelsRoundingValue?: number
dataLabelsOffset?: number
dataLabelsLineOffset?: number
hideLabelUnderPercentage?: number
donutBorderWidth?: number
donutRadiusRatio?: number
donutThickness?: number
fontFamily?: string
hideLabelUnderPercentage?: number
id?: string
interactive?: boolean
legendColor?: string
Expand Down Expand Up @@ -82,12 +84,14 @@ export function chartDonut({

const userOptions: ChartDonutOptions = {
backgroundColor: options?.backgroundColor ?? '#FFFFFF',
dataLabelsAsDivs: options?.dataLabelsAsDivs ?? false,
dataLabelsColor: options?.dataLabelsColor ?? "#000000",
dataLabelsFontSize: options?.dataLabelsFontSize ?? 12,
dataLabelsRoundingPercentage: options?.dataLabelsRoundingPercentage ?? 0,
dataLabelsRoundingValue: options?.dataLabelsRoundingValue ?? 0,
dataLabelsOffset: options?.dataLabelsOffset ?? 70,
dataLabelsLineOffset: options?.dataLabelsLineOffset ?? 45,
dataLabelsOffset: options?.dataLabelsOffset ?? 40,
dataLabelsLineOffset: options?.dataLabelsLineOffset ?? 20,
donutBorderWidth: options?.donutBorderWidth ?? 1,
donutThickness: options?.donutThickness ?? 48,
donutRadiusRatio: options?.donutRadiusRatio ?? 1,
fontFamily: options?.fontFamily ?? 'inherit',
Expand Down Expand Up @@ -157,8 +161,9 @@ export function chartDonut({
series: formattedDataset,
cx: width / 2,
cy: height / 2,
rx: width / (5.5 / userOptions.donutRadiusRatio!),
ry: width / (5.5 / userOptions.donutRadiusRatio!)
rx: width / (5 / userOptions.donutRadiusRatio!),
ry: width / (5 / userOptions.donutRadiusRatio!),
size: userOptions.donutThickness
})

const tooltipId = createUid();
Expand Down Expand Up @@ -197,9 +202,8 @@ export function chartDonut({
tt!.innerHTML = html;
}

function killTooltip(index: number) {
function killTooltip() {
const tt = document.getElementById(tooltipId);

for (let i = 0; i < formattedDataset.length; i += 1) {
selectionElements.forEach(o => {
const e = document.getElementById(o.el.replace('##', i + ''))
Expand All @@ -209,10 +213,7 @@ export function chartDonut({
}
})
}

tt!.setAttribute("style", "display:none");
const trap = document.getElementById(`${globalUid}_${index}`);
trap?.setAttribute('fill', 'transparent');
}

function setTooltipCoordinates(event: any) {
Expand Down Expand Up @@ -259,12 +260,18 @@ export function chartDonut({
arcs = arcs.map((a: any, i: number) => {
return {
...a,
path: path({
borderPath: path({
options: {
d: a.path,
stroke: a.color,
"stroke-width": userOptions.donutThickness,
fill: "none",
}
}),

path: path({
options: {
d: a.arcSlice,
stroke: userOptions.backgroundColor,
"stroke-width": a.value < 1 ? 0.1 : userOptions.donutBorderWidth,
fill: a.color,
id: `${globalUid}_${i}`,
"shape-rendering": userOptions["shape-rendering"]
}
Expand All @@ -283,8 +290,10 @@ export function chartDonut({

let count = formattedDataset.map(d => d.proportion * 100).filter(v => v < userOptions.hideLabelUnderPercentage!).length;

arcs.forEach((arc: { path: string, color: string, name: string, proportion: number, value: number, center: { endX: number, endY: number } }, i: number) => {
const arcMidPoint = findArcMidpoint(arc.path as unknown as SVGPathElement)
arcs.forEach((arc: {
borderPath: string, path: string, color: string, name: string, proportion: number, value: number, center: { endX: number, endY: number }
}, i: number) => {
const arcMidPoint = findArcMidpoint(arc.borderPath as unknown as SVGPathElement)

const lineEndpoint = offsetFromCenterPoint({
initX: arcMidPoint.x,
Expand All @@ -305,7 +314,7 @@ export function chartDonut({
const lineStart = offsetFromCenterPoint({
initX: arcMidPoint.x,
initY: arcMidPoint.y,
offset: userOptions.donutThickness! / 2,
offset: 0,
centerX: drawingArea.centerX,
centerY: drawingArea.centerY
})
Expand All @@ -319,39 +328,79 @@ export function chartDonut({
centerY: drawingArea.centerY
})

text({
options: {
x: labelSerieEndpoint.x,
y: labelSerieEndpoint.y,
fill: userOptions.dataLabelsColor!,
"font-size": userOptions.dataLabelsFontSize!,
"text-anchor": setTextAnchorFromCenterPoint({
x: labelSerieEndpoint.x,
centerX: drawingArea.centerX,
middleRange: 30
}),
content: arc.name,
id: `${globalUid}_marker_name_${i}`,
},
parent: markers
})
// DataLabels as foreignObjects

text({
options: {
if (userOptions.dataLabelsAsDivs) {
const labelDimensions = {
width: 64,
height: 32,
}
const labelItem = document.createElement('DIV');
const labelAnchor = setTextAnchorFromCenterPoint({
x: labelSerieEndpoint.x,
y: labelSerieEndpoint.y + userOptions.dataLabelsFontSize!,
fill: userOptions.dataLabelsColor!,
"font-size": userOptions.dataLabelsFontSize!,
"text-anchor": setTextAnchorFromCenterPoint({
centerX: drawingArea.centerX,
middleRange: 30,
isDiv: true
})

const fO = element({
el: SvgItem.FOREIGN_OBJECT,
options: {
className: "savyg-donut-datalabel",
x: labelSerieEndpoint.x - (labelAnchor === "right" ? labelDimensions.width : labelAnchor === 'center' ? labelDimensions.width / 2 : 0),
y: labelSerieEndpoint.y - labelDimensions.height / 2,
width: labelDimensions.width,
height: labelDimensions.height
},
parent: markers
})

fO.setAttribute('style', 'overflow: visible')

labelItem.classList.add('savyg-donut-label-item');
labelItem.setAttribute('id', `${globalUid}_marker_label_${i}`)
labelItem.setAttribute('width', '100%')
labelItem.setAttribute('height', '100%')
labelItem.style.fontSize = `${userOptions.dataLabelsFontSize!}px`
labelItem.style.textAlign = labelAnchor;
labelItem.innerHTML = `${arc.name} : ${fordinum(arc.proportion * 100, userOptions.dataLabelsRoundingPercentage, '%')} (${fordinum(arc.value, userOptions.dataLabelsRoundingValue)})`

fO.appendChild(labelItem)
} else {
text({
options: {
x: labelSerieEndpoint.x,
centerX: drawingArea.centerX,
middleRange: 30
}),
content: `${fordinum(arc.proportion * 100, userOptions.dataLabelsRoundingPercentage, '%')} (${fordinum(arc.value, userOptions.dataLabelsRoundingValue)})`,
id: `${globalUid}_marker_value_${i}`,
},
parent: markers
})
y: labelSerieEndpoint.y,
fill: userOptions.dataLabelsColor!,
"font-size": userOptions.dataLabelsFontSize!,
"text-anchor": setTextAnchorFromCenterPoint({
x: labelSerieEndpoint.x,
centerX: drawingArea.centerX,
middleRange: 30
}),
content: arc.name,
id: `${globalUid}_marker_name_${i}`,
},
parent: markers
})

text({
options: {
x: labelSerieEndpoint.x,
y: labelSerieEndpoint.y + userOptions.dataLabelsFontSize!,
fill: userOptions.dataLabelsColor!,
"font-size": userOptions.dataLabelsFontSize!,
"text-anchor": setTextAnchorFromCenterPoint({
x: labelSerieEndpoint.x,
centerX: drawingArea.centerX,
middleRange: 30
}),
content: `${fordinum(arc.proportion * 100, userOptions.dataLabelsRoundingPercentage, '%')} (${fordinum(arc.value, userOptions.dataLabelsRoundingValue)})`,
id: `${globalUid}_marker_value_${i}`,
},
parent: markers
})
}

line({
options: {
Expand Down Expand Up @@ -471,7 +520,7 @@ export function chartDonut({

if (userOptions.interactive) {
anArc.addEventListener("mouseenter", () => tooltip(i))
anArc.addEventListener("mouseleave", () => killTooltip(i))
anArc.addEventListener("mouseleave", () => killTooltip())
anArc.addEventListener('mousemove', (e) => setTooltipCoordinates(e))
if (callbacks?.onClickArc) {
anArc.addEventListener('click', () => clickArc(i))
Expand Down Expand Up @@ -663,6 +712,7 @@ export function chartDonut({
updateData,
arcs: arcs.map((a: any, i: number) => {
return {
...a,
pathElement: a.path,
arcMidPoint: findArcMidpoint(a.path),
name: formattedDataset[i].name,
Expand Down
55 changes: 34 additions & 21 deletions savyg/src/utils_common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export function matrixTimes([[a, b], [c, d]]: any, [x, y]: [number, number]) {
return [a * x + b * y, c * x + d * y];
}

export function createArc([cx, cy]: any, [rx, ry]: any, [position, ratio]: [number, number], phi: number, degrees: number = 360, piMult = 2) {
export function createArc([cx, cy]: any, [rx, ry]: any, [position, ratio]: [number, number], phi: number, degrees: number = 360, piMult = 2, reverse = false) {
ratio = ratio % (piMult * Math.PI);
const rotMatrix = rotateMatrix(phi);
const [sX, sY] = addVector(
Expand All @@ -149,20 +149,20 @@ export function createArc([cx, cy]: any, [rx, ry]: any, [position, ratio]: [numb
[cx, cy]
);
const fA = ratio > Math.PI ? 1 : 0;
const fS = ratio > 0 ? 1 : 0;
const fS = ratio > 0 ? reverse ? 0 : 1 : reverse ? 1 : 0;
return {
startX: sX,
startY: sY,
endX: eX,
endY: eY,
path: `M${sX} ${sY} A ${[
startX: reverse ? eX : sX,
startY: reverse ? eY : sY,
endX: reverse ? sX : eX,
endY: reverse ? sY : eY,
path: `M${reverse ? eX : sX} ${reverse ? eY : sY} A ${[
rx,
ry,
(phi / (piMult * Math.PI)) * degrees,
fA,
fS,
eX,
eY,
reverse ? sX : eX,
reverse ? sY : eY,
].join(" ")}`,
};
}
Expand All @@ -177,18 +177,20 @@ export function makeDonut({
piMult = 2,
arcAmpl = 1.45,
degrees = 360,
rotation = 105.25
rotation = 105.25,
size = 0
}: {
series: any,
cx: number,
cy: number,
rx: number,
ry: number,
piProportion?: number,
piMult?: number,
arcAmpl?: number
degrees?: number
rotation?: number
series: any;
cx: number;
cy: number;
rx: number;
ry: number;
piProportion?: number;
piMult?: number;
arcAmpl?: number;
degrees?: number;
rotation?: number;
size?: number
}) {
if (!series) {
return {
Expand Down Expand Up @@ -224,7 +226,18 @@ export function makeDonut({
piMult
);

const inner = createArc(
[cx, cy],
[rx - size, ry - size],
[acc, ratio],
rotation,
degrees,
piMult,
true
)

ratios.push({
arcSlice: `${path} L ${inner.startX} ${inner.startY} ${inner.path} L ${startX} ${startY}`,
cx,
cy,
...series[i],
Expand All @@ -242,7 +255,7 @@ export function makeDonut({
rotation,
degrees,
piMult
), // center of the arc, to display the marker. rx & ry are larger to be displayed with a slight offset
), // center of the arc, to display a marker. rx & ry are larger to be displayed with a slight offset
});
acc += ratio;
}
Expand Down

0 comments on commit fe5159b

Please sign in to comment.