isset( $attributes['valuePrefix'] ) ? (string) $attributes['valuePrefix'] : '',
'suffix' => isset( $attributes['valueSuffix'] ) ? (string) $attributes['valueSuffix'] : '',
'group' => ! empty( $attributes['groupThousands'] ),
);
}
}
if ( ! function_exists( 'airowp_chart_geometry' ) ) {
/**
* Build the geometry context for a chart.
*
* The plot is inset from the viewport to leave room for axis tick labels
* on the left and value labels above the tallest bar or point.
*
* @param array $attributes Block attributes.
* @param array $rows Chart rows.
* @param bool $has_grid Whether the grid is drawn.
* @param string $type Chart type.
* @return array Geometry context.
*/
function airowp_chart_geometry( array $attributes, array $rows, $has_grid, $type = 'bar' ) {
$height = max( 80, min( 800, (int) ( isset( $attributes['height'] ) ? $attributes['height'] : 240 ) ) );
$show_values = ! isset( $attributes['showValues'] ) || (bool) $attributes['showValues'];
$pad_top = $show_values ? 18 : 0;
$bounds = airowp_chart_bounds( wp_list_pluck( $rows, 'value' ) );
$tick_count = 5;
$format = airowp_chart_value_format( $attributes );
// Reserve room under the plot for the x-axis category labels, but only
// when there is something to write there.
$has_categories = 'donut' !== $type
&& (bool) array_filter( wp_list_pluck( $rows, 'label' ), 'strlen' );
$pad_bottom = $has_categories ? 22 : 0;
// Round the axis outwards so bars and gridlines land on round numbers.
// Applied whether or not the grid is drawn, so toggling it never
// rescales the series.
if ( 'donut' !== $type ) {
$nice = airowp_chart_nice_bounds( $bounds['min'], $bounds['max'], $tick_count );
$bounds = $nice;
$tick_count = $nice['count'];
}
// Size the left gutter to the labels that will actually be drawn.
// Formatting can widen a tick well past the old fixed 44 ("1000000"
// becomes "$1,000,000"), and the canvas does not clip, so a gutter
// that is too narrow paints the axis outside the block.
$pad_left = 0;
if ( $has_grid ) {
$labels = array();
foreach ( airowp_chart_ticks( $bounds['min'], $bounds['max'], $tick_count ) as $tick ) {
$labels[] = airowp_chart_format_value( $tick, $format );
}
$pad_left = airowp_chart_tick_gutter( $labels );
}
return array(
'width' => 600,
'height' => $height,
'pad_left' => $pad_left,
'pad_top' => $pad_top,
'pad_bottom' => $pad_bottom,
'plot_w' => 600 - $pad_left,
'plot_h' => max( 1, $height - $pad_top - $pad_bottom ),
'min' => $bounds['min'],
'max' => $bounds['max'],
'tick_count' => $tick_count,
'show_values' => $show_values,
'format' => $format,
);
}
}
if ( ! function_exists( 'airowp_render_chart' ) ) {
/**
* Render the chart block.
*
* @param array $attributes Block attributes.
* @param string $content Inner content, unused.
* @param WP_Block $block Block instance.
* @return string Rendered markup.
*/
function airowp_render_chart( $attributes, $content = '', $block = null ) {
$attributes = (array) $attributes;
$total = 0;
$rows = airowp_chart_rows( $attributes, $block, $total );
if ( empty( $rows ) ) {
return '';
}
$type = isset( $attributes['chartType'] ) ? $attributes['chartType'] : 'bar';
if ( ! in_array( $type, array( 'bar', 'line', 'donut' ), true ) ) {
$type = 'bar';
}
// A donut has no axes, so it never draws a grid.
$has_grid = 'donut' !== $type
&& ( ! isset( $attributes['showGrid'] ) || (bool) $attributes['showGrid'] );
$colors = airowp_chart_palette( $attributes, count( $rows ) );
if ( 'donut' === $type ) {
$filtered = airowp_chart_donut_rows( $rows, $colors );
$rows = $filtered['rows'];
$colors = $filtered['colors'];
if ( empty( $rows ) ) {
return '';
}
}
$geo = airowp_chart_geometry( $attributes, $rows, $has_grid, $type );
// True when the axis carries every category name, which is what lets
// the legend be optional in the first place.
$axis_names_all = false;
if ( 'donut' === $type ) {
$svg = airowp_chart_donut( $rows, $colors, $geo );
} else {
$plot = $has_grid ? airowp_chart_grid( $geo ) : '';
$plot .= 'line' === $type
? airowp_chart_line( $rows, $colors, $geo )
: airowp_chart_bars( $rows, $colors, $geo );
$plot .= airowp_chart_category_labels( $rows, $geo, $type );
$axis_names_all = 1 === airowp_chart_label_interval( $rows, $geo['plot_w'] );
$svg = sprintf(
'%s',
esc_attr( (string) $geo['pad_left'] ),
esc_attr( (string) $geo['pad_top'] ),
$plot // Every interpolation is escaped by the series renderers.
);
}
// The legend may only be turned off when something else on the page
// still names every category. A donut has no axis at all, and a dense
// axis drops names to stay readable — in both cases the legend is the
// only remaining sighted route from a mark to its category.
$show_legend = ! $axis_names_all
|| ! isset( $attributes['showLegend'] )
|| (bool) $attributes['showLegend'];
$legend = $show_legend ? airowp_chart_legend( $rows, $colors ) : '';
$label = isset( $attributes['label'] ) ? (string) $attributes['label'] : '';
// Only the cap is worth disclosing. Rows also fall out for being
// non-numeric, or negative on a donut, and reporting those as a
// shortfall would cry wolf on charts that are behaving exactly as
// documented.
$disclosed = $total > airowp_chart_max_rows() ? $total : 0;
$wrapper = get_block_wrapper_attributes(
array( 'class' => 'airo-wp-chart airo-wp-chart--' . $type )
);
return sprintf(
'%5$s%6$s',
$wrapper, // Escaped by get_block_wrapper_attributes().
(int) $geo['width'],
(int) $geo['height'],
$svg, // Every interpolation above is individually escaped.
$legend,
airowp_chart_data_table( $rows, $label, $disclosed, airowp_chart_value_format( $attributes ) )
);
}
}
echo airowp_render_chart( $attributes, $content, $block ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- All interpolations escaped inside the renderer.