/** * Dual-Axis Column + Line Chart Component * * Use for showing volume (bars) alongside a percentage/rate (line) over time. */ import Highcharts from "highcharts"; import HighchartsReact from "highcharts-react-official"; import { COLORS, CHART_STYLES, COMMON_CHART_OPTIONS, formatDateLabel, } from "@/lib/chart-constants"; // ============================================================================= // TYPES // ============================================================================= export interface DualAxisData { date: string; volume: number; rate: number; } export interface DualAxisChartProps { /** Array of data with date, volume, and rate */ data: DualAxisData[]; /** Chart title */ title?: string; /** Label for volume bars */ volumeLabel?: string; /** Label for rate line */ rateLabel?: string; /** Y-axis title for volume (left axis) */ volumeAxisTitle?: string; /** Y-axis title for rate (right axis) */ rateAxisTitle?: string; /** Color for volume bars */ volumeColor?: string; /** Color for rate line */ rateColor?: string; /** Height of the chart */ height?: number; /** Whether rate is a percentage (affects formatting) */ rateIsPercentage?: boolean; } // ============================================================================= // COMPONENT // ============================================================================= export const DualAxisChart = ({ data, title = "Volume and Rate Over Time", volumeLabel = "Volume", rateLabel = "Rate", volumeAxisTitle = "Volume", rateAxisTitle = "Rate (%)", volumeColor = COLORS.greenSecondary, rateColor = COLORS.bluePrimary, height = 400, rateIsPercentage = true, }: DualAxisChartProps) => { // Handle empty data if (!data || data.length === 0) { return (
No data available
); } // Sort by date const sortedData = [...data].sort((a, b) => a.date.localeCompare(b.date)); const options: Highcharts.Options = { chart: { height, backgroundColor: "transparent", animation: false, }, title: title ? COMMON_CHART_OPTIONS.title(title) : { text: undefined }, xAxis: { ...COMMON_CHART_OPTIONS.xAxis, categories: sortedData.map((d) => formatDateLabel(d.date)), }, yAxis: [ { // Primary Y-axis (left) - Volume ...COMMON_CHART_OPTIONS.yAxis(volumeAxisTitle), min: 0, }, { // Secondary Y-axis (right) - Rate title: { text: rateAxisTitle, style: { ...CHART_STYLES.text.tertiary, color: rateColor, }, }, labels: { style: CHART_STYLES.text.tertiary, formatter: function () { return rateIsPercentage ? `${this.value}%` : `${this.value}`; }, }, opposite: true, min: 0, max: rateIsPercentage ? 100 : undefined, gridLineWidth: 0, }, ], plotOptions: { column: { animation: false, borderWidth: 0, }, line: { animation: false, marker: { enabled: true, radius: 4, }, }, }, series: [ { name: volumeLabel, type: "column", yAxis: 0, data: sortedData.map((d) => d.volume), color: volumeColor, }, { name: rateLabel, type: "line", yAxis: 1, data: sortedData.map((d) => d.rate), color: rateColor, lineWidth: 2, }, ], legend: { ...COMMON_CHART_OPTIONS.legend, enabled: true, }, tooltip: { ...COMMON_CHART_OPTIONS.tooltip, shared: true, formatter: function () { const points = this.points || []; let html = `${this.x}
`; points.forEach((point) => { const isRate = point.series.name === rateLabel; const value = point.y ?? 0; const formatted = isRate && rateIsPercentage ? `${value.toFixed(1)}%` : value.toLocaleString(); html += `\u25CF ${point.series.name}: ${formatted}
`; }); return html; }, }, credits: COMMON_CHART_OPTIONS.credits, }; return ; }; // ============================================================================= // USAGE EXAMPLE // ============================================================================= /* import { DualAxisChart } from "@/components/charts/DualAxisChart"; // AI-assisted commits with acceptance rate // Sales volume and conversion rate // Non-percentage rate (e.g., average) */ export default DualAxisChart;