', esc_attr( airowp_chart_number( $y ) ), esc_attr( airowp_chart_number( $geo['plot_w'] ) ) ); $out .= sprintf( '%s', esc_attr( airowp_chart_number( $y ) ), esc_html( airowp_chart_format_value( $tick, $geo['format'] ?? array() ) ) ); } return $out; } } if ( ! function_exists( 'airowp_chart_zero_y' ) ) { /** * The plot-local y of the value zero, clamped into the plot. * * Bars grow from this line, not from the axis minimum, so a negative value * reads as a downward bar instead of a short upward one. * * @param array $geo Geometry context. * @return float Plot-local y coordinate. */ function airowp_chart_zero_y( array $geo ) { $zero = min( max( 0.0, (float) $geo['min'] ), (float) $geo['max'] ); return $geo['plot_h'] - airowp_chart_scale( $zero, $geo['min'], $geo['max'], 0, $geo['plot_h'] ); } } if ( ! function_exists( 'airowp_chart_label_interval' ) ) { /** * How many rows to skip between drawn category labels. * * Twelve categories of "September" overlap into an unreadable smear if all * are drawn, so past a density threshold only every Nth is written — the * axis still orients the reader, and `airowp_render_chart()` forces the * legend back on when this returns more than 1, so the complete set of * category names never disappears from the page. * * @param array $rows Chart rows. * @param float $plot_w Plot width in user units. * @return int Interval, one or greater. */ function airowp_chart_label_interval( array $rows, $plot_w ) { $count = count( $rows ); if ( $count < 2 ) { return 1; } $longest = 0; foreach ( $rows as $row ) { $longest = max( $longest, mb_strlen( $row['label'] ) ); } if ( 0 === $longest ) { return 1; } // Font size is 14 user units; ~0.6em per character is a safe average // for proportional text, plus a space of padding between neighbours. $needed = $longest * 14 * 0.6 + 8; $available = $plot_w / $count; return (int) max( 1, ceil( $needed / max( 1, $available ) ) ); } } if ( ! function_exists( 'airowp_chart_category_labels' ) ) { /** * Render the x-axis category labels. * * Category identity must not depend on the legend — the legend is optional * and colour alone cannot carry it. Bar and line charts label their axis * directly; the donut has no axis and keeps its legend instead. * * @param array $rows Chart rows. * @param array $geo Geometry context. * @param string $type Chart type. * @return string SVG fragment. */ function airowp_chart_category_labels( array $rows, array $geo, $type ) { $count = count( $rows ); if ( 0 === $count ) { return ''; } $out = ''; $slot = $geo['plot_w'] / $count; $step = $count > 1 ? $geo['plot_w'] / ( $count - 1 ) : 0; $y = $geo['plot_h'] + 16; $interval = airowp_chart_label_interval( $rows, $geo['plot_w'] ); foreach ( array_values( $rows ) as $i => $row ) { if ( '' === $row['label'] || 0 !== $i % $interval ) { continue; } if ( 'line' === $type ) { $x = $count > 1 ? $i * $step : $geo['plot_w'] / 2; } else { $x = $i * $slot + $slot / 2; } // Keep the outermost line labels inside the viewport. $anchor = 'middle'; if ( 'line' === $type && $count > 1 && 0 === $i ) { $anchor = 'start'; } elseif ( 'line' === $type && $count > 1 && $count - 1 === $i ) { $anchor = 'end'; } $out .= sprintf( '%s', esc_attr( airowp_chart_number( $x ) ), esc_attr( airowp_chart_number( $y ) ), esc_attr( $anchor ), esc_html( $row['label'] ) ); } return $out; } }