react-native-chart-kit
Version:
Beautiful React Native charts for dashboards, reports, and data-rich mobile apps.
192 lines (191 loc) • 7.97 kB
JavaScript
const defaultArcLabelFontSize = 11;
const defaultArcLabelMinPercentage = 0.07;
const defaultArcLabelOffset = 10;
const defaultConnectorLength = 12;
const defaultConnectorOpacity = 0.84;
const defaultConnectorWidth = 1.35;
const defaultReservedWidth = 86;
const minConnectorLabelGap = 6;
const labelEdgePadding = 10;
const estimatedCharacterWidthRatio = 0.78;
export const getPieChartArcLabelsVisible = (arcLabels) => {
if (arcLabels === undefined || arcLabels === false) {
return false;
}
if (typeof arcLabels === "object") {
return arcLabels.visible !== false;
}
return true;
};
export const resolvePieChartArcLabelsConfig = (arcLabels) => {
const config = typeof arcLabels === "object" ? arcLabels : {};
return {
connectorColor: config.connectorColor,
connectorLength: typeof config.connectorLength === "number" &&
Number.isFinite(config.connectorLength)
? Math.max(0, config.connectorLength)
: defaultConnectorLength,
connectorLines: config.connectorLines !== false,
connectorOpacity: typeof config.connectorOpacity === "number" &&
Number.isFinite(config.connectorOpacity)
? clamp(config.connectorOpacity, 0, 1)
: defaultConnectorOpacity,
connectorWidth: typeof config.connectorWidth === "number" &&
Number.isFinite(config.connectorWidth)
? Math.max(0, config.connectorWidth)
: defaultConnectorWidth,
fontSize: typeof config.fontSize === "number" && Number.isFinite(config.fontSize)
? Math.max(8, config.fontSize)
: defaultArcLabelFontSize,
formatLabel: config.formatLabel,
minPercentage: typeof config.minPercentage === "number" &&
Number.isFinite(config.minPercentage)
? Math.max(0, config.minPercentage)
: defaultArcLabelMinPercentage,
offset: typeof config.offset === "number" && Number.isFinite(config.offset)
? Math.max(0, config.offset)
: defaultArcLabelOffset,
reservedWidth: typeof config.reservedWidth === "number" &&
Number.isFinite(config.reservedWidth)
? Math.max(0, config.reservedWidth)
: defaultReservedWidth,
visible: getPieChartArcLabelsVisible(arcLabels)
};
};
const getMidAngle = (arc) => arc.startAngle + (arc.endAngle - arc.startAngle) / 2;
const clamp = (value, min, max) => {
if (max < min) {
return min;
}
return Math.min(Math.max(value, min), max);
};
const estimateLabelWidth = ({ fontSize, text }) => text.length * fontSize * estimatedCharacterWidthRatio;
export const getPieChartArcLabelHorizontalReserve = (config) => config.reservedWidth + labelEdgePadding;
const spreadArcLabelsOnSide = ({ chartHeight, labels, minGap }) => {
const minY = labelEdgePadding + minGap / 2;
const maxY = Math.max(minY, chartHeight - labelEdgePadding);
const sorted = [...labels].sort((a, b) => a.y - b.y);
let cursorY = minY;
sorted.forEach((label) => {
const y = clamp(Math.max(label.y, cursorY), minY, maxY);
label.y = y;
label.connectorBendY = y;
label.connectorEndY = y;
cursorY = y + minGap;
});
for (let index = sorted.length - 2; index >= 0; index -= 1) {
const label = sorted[index];
const nextLabel = sorted[index + 1];
if (!label || !nextLabel) {
continue;
}
const maxAllowedY = nextLabel.y - minGap;
if (label.y > maxAllowedY) {
label.y = clamp(maxAllowedY, minY, maxY);
label.connectorBendY = label.y;
label.connectorEndY = label.y;
}
}
};
const alignArcLabelConnectors = (labels) => {
labels.forEach((label) => {
label.connectorBendX =
label.connectorStartX + (label.connectorEndX - label.connectorStartX) / 2;
label.connectorBendY =
label.connectorStartY + (label.connectorEndY - label.connectorStartY) / 2;
});
};
export const buildPieChartArcLabels = ({ arcs, chartHeight, chartWidth, centerX, centerY, config, legendItems, radius, selectedIndex, theme }) => {
if (!config.visible) {
return [];
}
const legendByIndex = new Map(legendItems.map((item) => [item.index, item]));
const labels = arcs.flatMap((arc) => {
const legendItem = legendByIndex.get(arc.index);
if (!arc.defined || !legendItem || arc.percentage < config.minPercentage) {
return [];
}
const angle = getMidAngle(arc);
const side = Math.cos(angle) >= 0 ? 1 : -1;
const startRadius = radius;
const bendRadius = radius + config.offset;
const preferredLabelX = centerX + side * (radius + config.offset + config.connectorLength + 2);
const labelY = centerY + bendRadius * Math.sin(angle);
const renderProps = {
arc,
color: legendItem.color,
index: legendItem.index,
label: legendItem.label,
percentageLabel: legendItem.percentageLabel,
selected: selectedIndex === arc.index,
theme,
valueLabel: legendItem.valueLabel
};
const text = config.formatLabel?.(renderProps) ??
`${legendItem.label} ${legendItem.percentageLabel}`;
if (text.length === 0) {
return [];
}
const labelWidth = estimateLabelWidth({ fontSize: config.fontSize, text });
const outsideRightX = centerX +
radius +
config.offset +
config.connectorLength +
minConnectorLabelGap;
const outsideLeftX = centerX -
radius -
config.offset -
config.connectorLength -
minConnectorLabelGap;
const rightEdgeLabelX = chartWidth - labelEdgePadding - labelWidth;
const leftEdgeLabelX = labelEdgePadding + labelWidth;
const labelX = side > 0
? outsideRightX <= rightEdgeLabelX
? clamp(preferredLabelX, outsideRightX, rightEdgeLabelX)
: rightEdgeLabelX
: leftEdgeLabelX <= outsideLeftX
? clamp(preferredLabelX, leftEdgeLabelX, outsideLeftX)
: leftEdgeLabelX;
const rawConnectorEndX = labelX - side * 4;
const outerSliceEdgeX = centerX + side * radius;
const connectorEndX = side > 0
? Math.max(rawConnectorEndX, outerSliceEdgeX + 1)
: Math.min(rawConnectorEndX, outerSliceEdgeX - 1);
return [
{
arc,
color: legendItem.color,
connectorColor: config.connectorColor ?? theme.mutedText,
connectorBendX: centerX + side * bendRadius * Math.cos(angle),
connectorBendY: centerY + bendRadius * Math.sin(angle),
connectorEndX,
connectorEndY: labelY,
connectorOpacity: config.connectorOpacity,
connectorStartX: centerX + startRadius * Math.cos(angle),
connectorStartY: centerY + startRadius * Math.sin(angle),
connectorVisible: config.connectorLines,
connectorWidth: config.connectorWidth,
fontSize: config.fontSize,
index: arc.index,
key: `${arc.index}-${arc.label}-arc-label`,
text,
textAnchor: side > 0 ? "start" : "end",
x: labelX,
y: labelY
}
];
});
const minGap = config.fontSize + 5;
spreadArcLabelsOnSide({
chartHeight,
labels: labels.filter((label) => label.textAnchor === "start"),
minGap
});
spreadArcLabelsOnSide({
chartHeight,
labels: labels.filter((label) => label.textAnchor === "end"),
minGap
});
alignArcLabelConnectors(labels);
return labels;
};