%s', esc_attr( airowp_chart_number( $x ) ), esc_attr( airowp_chart_number( $y ) ), esc_attr( $anchor ), '' === $baseline ? '' : ' dominant-baseline="' . esc_attr( $baseline ) . '"', esc_html( $text ) ); } } if ( ! function_exists( 'airowp_chart_bars' ) ) { /** * Render a bar series. * * @param array $rows Chart rows. * @param array $colors Series colours. * @param array $geo Geometry context. * @return string SVG fragment. */ function airowp_chart_bars( array $rows, array $colors, array $geo ) { $slot = $geo['plot_w'] / count( $rows ); $gap = $slot * 0.2; $zero_y = airowp_chart_zero_y( $geo ); $out = ''; // A baseline only exists to mark zero when the axis crosses it; without // one a downward bar is indistinguishable from a short upward one. if ( $geo['min'] < 0 ) { $out .= sprintf( '', esc_attr( airowp_chart_number( $zero_y ) ), esc_attr( airowp_chart_number( $geo['plot_w'] ) ) ); } foreach ( array_values( $rows ) as $i => $row ) { $value_y = $geo['plot_h'] - airowp_chart_scale( $row['value'], $geo['min'], $geo['max'], 0, $geo['plot_h'] ); // Bars span zero → value, so the sign decides which way they grow. $top = min( $zero_y, $value_y ); $bar_h = abs( $value_y - $zero_y ); $out .= sprintf( '', esc_attr( airowp_chart_number( $i * $slot + $gap / 2 ) ), esc_attr( airowp_chart_number( $top ) ), esc_attr( airowp_chart_number( $slot - $gap ) ), esc_attr( airowp_chart_number( $bar_h ) ), esc_attr( $colors[ $i ] ) ); if ( $geo['show_values'] ) { // Label below a negative bar, above a positive one. $label_y = $row['value'] < 0 ? $top + $bar_h + 14 : $top - 6; $out .= airowp_chart_value_label( $i * $slot + $slot / 2, $label_y, airowp_chart_format_value( $row['value'], $geo['format'] ?? array() ) ); } } return $out; } } if ( ! function_exists( 'airowp_chart_line' ) ) { /** * Render a line series. * * @param array $rows Chart rows. * @param array $colors Series colours. * @param array $geo Geometry context. * @return string SVG fragment. */ function airowp_chart_line( array $rows, array $colors, array $geo ) { $values = wp_list_pluck( $rows, 'value' ); $count = count( $values ); $out = sprintf( '', esc_attr( airowp_chart_line_points( $values, $geo['plot_w'], $geo['plot_h'], $geo['min'], $geo['max'] ) ), esc_attr( $colors[0] ) ); $step = $count > 1 ? $geo['plot_w'] / ( $count - 1 ) : 0; foreach ( array_values( $values ) as $i => $value ) { $x = $count > 1 ? $i * $step : $geo['plot_w'] / 2; $y = $geo['plot_h'] - airowp_chart_scale( $value, $geo['min'], $geo['max'], 0, $geo['plot_h'] ); // Each point takes its own palette slot so the legend's swatches // describe the chart rather than contradicting it. $out .= sprintf( '', esc_attr( airowp_chart_number( $x ) ), esc_attr( airowp_chart_number( $y ) ), esc_attr( isset( $colors[ $i ] ) ? $colors[ $i ] : $colors[0] ) ); if ( $geo['show_values'] ) { // Pull the end labels inwards so they do not overhang the axis. $anchor = 'middle'; if ( $count > 1 && 0 === $i ) { $anchor = 'start'; } elseif ( $count > 1 && $count - 1 === $i ) { $anchor = 'end'; } $out .= airowp_chart_value_label( $x, $y - 8, airowp_chart_format_value( $value, $geo['format'] ?? array() ), $anchor ); } } return $out; } } if ( ! function_exists( 'airowp_chart_donut' ) ) { /** * Render a donut series. * * @param array $rows Chart rows. * @param array $colors Series colours. * @param array $geo Geometry context. * @return string SVG fragment. */ function airowp_chart_donut( array $rows, array $colors, array $geo ) { // Rows reaching here are already filtered to positive values by // airowp_chart_donut_rows(); a share of a negative has no meaning. $total = array_sum( wp_list_pluck( $rows, 'value' ) ); // Labels sit outside the ring so they read against the page background // rather than against whatever colour the author gave the slice — dark // text on a mid-tone fill is unreadable and cannot be predicted here. $label_room = $geo['show_values'] ? 26 : 0; $radius = min( $geo['width'], $geo['height'] ) / 2 - 4 - $label_room; $cx = $geo['width'] / 2; $cy = $geo['height'] / 2; $cursor = 0.0; $out = ''; foreach ( array_values( $rows ) as $i => $row ) { $share = $total > 0 ? $row['value'] / $total : 0.0; $sweep = $share * 360; $path = airowp_chart_arc_path( $cx, $cy, $radius, $radius * 0.4, $cursor, $cursor + $sweep ); $mid = $cursor + $sweep / 2; $cursor += $sweep; if ( '' === $path ) { continue; } $out .= sprintf( '', esc_attr( $path ), esc_attr( $colors[ $i ] ) ); if ( $geo['show_values'] ) { $rad = deg2rad( $mid - 90 ); $edge = $radius + 10; $dx = cos( $rad ); // Push the text away from the ring on whichever side it sits. $anchor = 'middle'; if ( $dx > 0.25 ) { $anchor = 'start'; } elseif ( $dx < -0.25 ) { $anchor = 'end'; } $out .= airowp_chart_value_label( $cx + $edge * $dx, $cy + $edge * sin( $rad ), airowp_chart_format_value( $share * 100 ) . '%', $anchor, 'middle' ); } } return $out; } }