%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;
}
}