|
import m, { FactoryComponent } from 'mithril'; |
|
|
|
export interface DonutChartSlice { |
|
label: string; |
|
value: number; |
|
color: string; |
|
} |
|
|
|
export interface DonutChartAttrs { |
|
data: DonutChartSlice[]; |
|
innerRadius?: number; |
|
outerRadius?: number; |
|
} |
|
|
|
export const DonutChart: FactoryComponent<DonutChartAttrs> = () => { |
|
return { |
|
view: ({ attrs }) => { |
|
const { data, innerRadius = 50, outerRadius = 100 } = attrs; |
|
|
|
// Calculate total value |
|
const totalValue = data.reduce((acc, slice) => acc + slice.value, 0); |
|
|
|
// Calculate angles |
|
let startAngle = 0; |
|
let endAngle = 0; |
|
const arcs: { |
|
d: string; |
|
fill: string; |
|
label: string; |
|
value: number; |
|
cumulativeValue: number; |
|
}[] = []; |
|
data.forEach((slice) => { |
|
const angle = (slice.value / totalValue) * 360; |
|
endAngle = startAngle + angle; |
|
|
|
const startAngleRad = ((startAngle - 90) * Math.PI) / 180; |
|
const endAngleRad = ((endAngle - 90) * Math.PI) / 180; |
|
|
|
const x1 = Math.cos(startAngleRad) * outerRadius; |
|
const y1 = Math.sin(startAngleRad) * outerRadius; |
|
const x2 = Math.cos(endAngleRad) * outerRadius; |
|
const y2 = Math.sin(endAngleRad) * outerRadius; |
|
|
|
const largeArcFlag = angle <= 180 ? '0' : '1'; |
|
|
|
const pathData = [ |
|
`M ${x1} ${y1}`, |
|
`A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${x2} ${y2}`, |
|
`L ${Math.cos(endAngleRad) * innerRadius} ${Math.sin(endAngleRad) * innerRadius}`, |
|
`A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${ |
|
Math.cos(startAngleRad) * innerRadius |
|
} ${Math.sin(startAngleRad) * innerRadius}`, |
|
'Z', |
|
].join(' '); |
|
|
|
arcs.push({ |
|
d: pathData, |
|
fill: slice.color, |
|
label: slice.label, |
|
value: slice.value, |
|
cumulativeValue: |
|
startAngle === 0 ? slice.value : arcs[arcs.length - 1].cumulativeValue + slice.value, |
|
}); |
|
|
|
startAngle = endAngle; |
|
}); |
|
|
|
// Legend |
|
const legendItems = data.map((slice) => |
|
m('div.legend-item', [ |
|
m('div.legend-color', { style: { backgroundColor: slice.color } }), |
|
m('div.legend-label', slice.label), |
|
]) |
|
); |
|
|
|
return m('.donut-chart-container', [ |
|
m( |
|
'svg.donut-chart', |
|
{ viewBox: `-${outerRadius} -${outerRadius} ${outerRadius * 2} ${outerRadius * 2}` }, |
|
arcs.map((arc) => |
|
m( |
|
'g', |
|
{ title: `${arc.label}: ${arc.value}` }, |
|
m('path', { d: arc.d, fill: arc.fill }), |
|
m('title', `${arc.label}: ${arc.value}`) |
|
) |
|
), |
|
m('text.total-value', { x: '0', y: '15', 'text-anchor': 'middle' }, totalValue) |
|
), |
|
m('.donut-chart-tooltip', [ |
|
m('span.tooltip-label', 'Label:'), |
|
m('span.tooltip-value'), |
|
m('br'), |
|
m('span.tooltip-label', 'Value:'), |
|
m('span.tooltip-value'), |
|
]), |
|
m('.legend', legendItems), |
|
]); |
|
}, |
|
}; |
|
}; |