analytica-frontend-lib
Version:
Repositório público dos componentes utilizados nas plataformas da Analytica Ensino
1 lines • 16.7 kB
Source Map (JSON)
{"version":3,"sources":["../src/components/shared/ChartComponents.tsx"],"sourcesContent":["import { useState } from 'react';\nimport Text from '../Text/Text';\nimport { cn } from '../../utils/utils';\nimport {\n bgClassToCssVar,\n polarToCartesian,\n describeArc,\n} from '../../utils/chartUtils';\n\n/**\n * Bar item configuration used by chart components\n */\nexport interface BarItemConfig {\n key: string;\n label: string;\n legendLabel: string;\n value: number;\n colorClass: string;\n}\n\n/**\n * Calculate Y-axis tick values based on max value.\n * Rounds up to the nearest multiple of 10 and generates 5 evenly spaced ticks.\n */\nexport const calculateYAxisTicks = (maxValue: number): number[] => {\n if (maxValue <= 0) return [0];\n\n const niceMax = Math.ceil(maxValue / 10) * 10;\n const step = niceMax / 4;\n return [\n niceMax,\n Math.round(step * 3),\n Math.round(step * 2),\n Math.round(step),\n 0,\n ];\n};\n\n/**\n * Legend dot + label for chart legends\n */\nexport const LegendItem = ({\n color,\n label,\n}: {\n color: string;\n label: string;\n}) => (\n <div className=\"flex flex-row items-center gap-2\">\n <div className={cn('w-2 h-2 rounded-full', color)} />\n <Text size=\"xs\" weight=\"medium\" className=\"text-text-600\">\n {label}\n </Text>\n </div>\n);\n\n/**\n * Individual vertical bar component\n */\nexport const DataBar = ({\n label,\n value,\n maxValue,\n colorClass,\n chartHeight,\n}: {\n label: string;\n value: number;\n maxValue: number;\n colorClass: string;\n chartHeight: number;\n}) => {\n const percentage = maxValue === 0 ? 0 : (value / maxValue) * 100;\n const barHeight = (percentage / 100) * chartHeight;\n\n return (\n <div className=\"flex flex-col items-center gap-2 flex-1\">\n <div\n className=\"w-full flex items-end justify-center\"\n style={{ height: chartHeight }}\n >\n <div\n className={cn(\n 'w-16 rounded-lg transition-all duration-300',\n colorClass\n )}\n style={{ height: `${barHeight}px` }}\n />\n </div>\n <Text size=\"xs\" weight=\"medium\" className=\"text-text-600 text-center\">\n {label}\n </Text>\n </div>\n );\n};\n\n/**\n * Horizontal dashed grid lines for chart background\n */\nexport const GridLines = ({\n ticks,\n chartHeight,\n}: {\n ticks: number[];\n chartHeight: number;\n}) => (\n <div\n className=\"absolute inset-0 flex flex-col justify-between pointer-events-none\"\n style={{ height: chartHeight }}\n >\n {ticks.map((tick, index) => (\n <div\n key={`${tick}-${index}`}\n className=\"w-full border-t border-dashed border-border-200\"\n style={{ marginTop: index === 0 ? 0 : undefined }}\n />\n ))}\n </div>\n);\n\n/**\n * Y-Axis tick labels\n */\nexport const YAxis = ({\n ticks,\n chartHeight,\n}: {\n ticks: number[];\n chartHeight: number;\n}) => (\n <div\n className=\"flex flex-col justify-between items-end pr-3\"\n style={{ height: chartHeight }}\n aria-hidden=\"true\"\n >\n {ticks.map((tick, index) => (\n <Text\n key={`${tick}-${index}`}\n size=\"xs\"\n weight=\"medium\"\n className=\"text-text-500\"\n >\n {tick}\n </Text>\n ))}\n </div>\n);\n\n// ─── Pie chart ────────────────────────────────────────────────\n\n/**\n * A single slice of a pie chart.\n * Provide either `colorClass` (Tailwind bg-* class) or `color` (direct CSS value).\n * When `color` is set it takes precedence over `colorClass`.\n */\nexport interface PieSlice {\n /** Unique key for the slice (uses label if not provided) */\n key?: string;\n label: string;\n /** Drives the slice's angle. Always the number — never a formatted string. */\n value: number;\n colorClass: string;\n /** Direct CSS color value — takes precedence over colorClass when provided */\n color?: string;\n /**\n * What the legend shows, when it differs from `value`.\n *\n * The pie is sized by the percentage, but the legend often needs to read as\n * something else — \"1h 20min\" rather than \"45\". Falls back to `value`.\n */\n displayValue?: string | number;\n}\n\n/** Default radius ratio (44% of size) */\nconst PIE_RADIUS_RATIO = 0.44;\n\n/** Default label radius ratio (62% from center) */\nconst PIE_LABEL_RADIUS_RATIO = 0.62;\n\n/** Default minimum percentage to show label */\nconst PIE_MIN_PERCENTAGE_FOR_LABEL = 5;\n\n/** Threshold to consider a slice as whole pie */\nconst PIE_WHOLE_THRESHOLD = 99.9999;\n\n/**\n * Props for SimplePieChart component\n */\nexport interface SimplePieChartProps {\n /** Array of slices to render */\n slices: PieSlice[];\n /** Chart size in pixels (default: 130) */\n size?: number;\n /** Text to show when all values are zero */\n emptyText?: string;\n /** Minimum percentage to display label inside slice (default: 5) */\n minPercentageForLabel?: number;\n /** Label position as ratio of radius (default: 0.62) */\n labelRadiusRatio?: number;\n /** Label text color (default: var(--color-text-950)) */\n labelColor?: string;\n /** Label font weight (default: 500) */\n labelFontWeight?: number;\n /** Label text shadow (default: none) */\n labelTextShadow?: string;\n /** Hover overlay opacity (default: 0.4) */\n hoverOpacity?: number;\n /** External hover state (controlled mode) */\n hoveredSlice?: string | null;\n /** Callback when slice is hovered */\n onSliceHover?: (key: string | null) => void;\n}\n\n/**\n * Interactive SVG pie chart built from PieSlice data.\n * Shows percentage labels for slices above minimum threshold.\n * Renders a grey circle when all values are zero.\n */\nexport const SimplePieChart = ({\n slices,\n size = 130,\n emptyText,\n minPercentageForLabel = PIE_MIN_PERCENTAGE_FOR_LABEL,\n labelRadiusRatio = PIE_LABEL_RADIUS_RATIO,\n labelColor = 'var(--color-text-950)',\n labelFontWeight = 500,\n labelTextShadow,\n hoverOpacity = 0.4,\n hoveredSlice: externalHovered,\n onSliceHover,\n}: SimplePieChartProps) => {\n const [internalHovered, setInternalHovered] = useState<string | null>(null);\n\n // Use external hover state if provided (controlled mode)\n const isControlled = externalHovered !== undefined;\n const hovered = isControlled ? externalHovered : internalHovered;\n\n const handleSliceHover = (label: string | null) => {\n if (!isControlled) {\n setInternalHovered(label);\n }\n onSliceHover?.(label);\n };\n\n const total = slices.reduce((sum, s) => sum + s.value, 0);\n const radius = size * PIE_RADIUS_RATIO;\n const center = size / 2;\n\n if (total === 0) {\n return (\n <svg\n width={size}\n height={size}\n viewBox={`0 0 ${size} ${size}`}\n aria-hidden={!emptyText}\n aria-label={emptyText || undefined}\n >\n <circle\n cx={center}\n cy={center}\n r={radius}\n className=\"fill-background-200\"\n />\n {emptyText && (\n <text\n x={center}\n y={center}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n fill=\"var(--color-text-400)\"\n style={{\n fontSize: 12,\n fontWeight: 500,\n fontFamily: 'Roboto, sans-serif',\n }}\n >\n {emptyText}\n </text>\n )}\n </svg>\n );\n }\n\n let cumAngle = 0;\n const computed = slices\n .map((s) => {\n const pct = (s.value / total) * 100;\n const startAngle = cumAngle;\n cumAngle += (pct / 100) * 360;\n const endAngle = cumAngle;\n const midAngle = startAngle + (endAngle - startAngle) / 2;\n return { ...s, pct, startAngle, endAngle, midAngle };\n })\n .filter((s) => s.pct > 0);\n\n return (\n <svg\n width={size}\n height={size}\n viewBox={`0 0 ${size} ${size}`}\n aria-hidden=\"true\"\n onMouseLeave={() => handleSliceHover(null)}\n >\n {computed.map((slice) => {\n const sliceKey = slice.key ?? slice.label;\n const isHovered = hovered === sliceKey;\n const isWhole = slice.pct >= PIE_WHOLE_THRESHOLD;\n const arcPath = isWhole\n ? undefined\n : describeArc(\n center,\n center,\n radius,\n slice.startAngle,\n slice.endAngle\n );\n const labelPos = polarToCartesian(\n center,\n center,\n radius * labelRadiusRatio,\n slice.midAngle\n );\n const fill = slice.color ?? bgClassToCssVar(slice.colorClass);\n\n return (\n <g\n key={sliceKey}\n onMouseEnter={() => handleSliceHover(sliceKey)}\n className=\"cursor-pointer\"\n >\n {isWhole ? (\n <circle cx={center} cy={center} r={radius} fill={fill} />\n ) : (\n <path d={arcPath} fill={fill} />\n )}\n {isHovered &&\n (isWhole ? (\n <circle\n cx={center}\n cy={center}\n r={radius}\n fill=\"white\"\n opacity={hoverOpacity}\n style={{ pointerEvents: 'none' }}\n />\n ) : (\n <path\n d={arcPath}\n fill=\"white\"\n opacity={hoverOpacity}\n style={{ pointerEvents: 'none' }}\n />\n ))}\n {slice.pct >= minPercentageForLabel && (\n <text\n x={labelPos.x}\n y={labelPos.y}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n fill={labelColor}\n style={{\n fontSize: 14,\n fontWeight: labelFontWeight,\n fontFamily: 'Roboto, sans-serif',\n lineHeight: 1,\n letterSpacing: 0,\n pointerEvents: 'none',\n textShadow: labelTextShadow,\n }}\n >\n {`${Math.round(slice.pct)}%`}\n </text>\n )}\n </g>\n );\n })}\n </svg>\n );\n};\n\n/**\n * A single legend row: colour dot + label + value.\n */\nexport const LegendRow = ({\n colorClass,\n color,\n label,\n value,\n displayValue,\n}: {\n colorClass: string;\n color?: string;\n label: string;\n value: number;\n displayValue?: string | number;\n}) => (\n <div className=\"flex items-center gap-2\">\n <div\n className={cn('w-2 h-2 rounded-full shrink-0', !color && colorClass)}\n style={color ? { backgroundColor: color } : undefined}\n />\n <Text size=\"sm\" weight=\"medium\" className=\"text-text-950 flex-1\">\n {label}\n </Text>\n <Text size=\"sm\" weight=\"medium\" className=\"text-text-600 ml-3\">\n {displayValue ?? value}\n </Text>\n </div>\n);\n\n/**\n * Card with a legend list and a pie chart side by side.\n */\nexport const LegendPieCard = ({ slices }: { slices: PieSlice[] }) => (\n <div className=\"flex items-center justify-between gap-4 p-4 bg-background border border-border-50 rounded-xl\">\n <div className=\"flex flex-col gap-2\">\n {slices.map((s) => (\n <LegendRow\n key={s.key ?? s.label}\n colorClass={s.colorClass}\n color={s.color}\n label={s.label}\n value={s.value}\n displayValue={s.displayValue}\n />\n ))}\n </div>\n <SimplePieChart slices={slices} />\n </div>\n);\n"],"mappings":"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","names":[]}