. */ class SEATT_Charts { const PALETTE = array( '#2271b1', '#00a32a', '#dba617', '#d63638', '#8c5cd6', '#00a0d2', '#e26f56', '#3c434a' ); /** Escapes a chart built by this class (inline SVG + a few wrappers). */ public static function allowed_html() { $common = array( 'class' => true, 'style' => true, 'id' => true, 'role' => true, 'aria-label' => true, 'aria-hidden' => true ); $shape = $common + array( 'x' => true, 'y' => true, 'x1' => true, 'x2' => true, 'y1' => true, 'y2' => true, 'width' => true, 'height' => true, 'r' => true, 'cx' => true, 'cy' => true, 'rx' => true, 'ry' => true, 'd' => true, 'points' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-linecap' => true, 'transform' => true, 'opacity' => true, 'text-anchor' => true, 'font-size' => true, 'font-weight' => true, 'dominant-baseline' => true ); $allowed = array( 'svg' => $common + array( 'viewbox' => true, 'width' => true, 'height' => true, 'xmlns' => true, 'preserveaspectratio' => true, 'focusable' => true ), 'g' => $shape, 'line' => $shape, 'rect' => $shape, 'circle' => $shape, 'path' => $shape, 'polyline' => $shape, 'polygon' => $shape, 'text' => $shape, 'tspan' => $shape, 'title' => array(), 'div' => $common, 'span' => $common, 'strong' => $common, 'small' => $common, 'p' => $common, 'b' => $common, 'em' => $common, 'i' => $common, 'ul' => $common, 'ol' => $common, 'li' => $common, 'table' => $common, 'thead' => $common, 'tbody' => $common, 'tr' => $common, 'th' => $common, 'td' => $common, ); return $allowed; } public static function color( $i ) { return self::PALETTE[ $i % count( self::PALETTE ) ]; } /** Short money label for axis ticks: Rp 1,2 jt / Rp 950 rb style * would be locale-specific, so keep it generic: 1.2M / 950K. */ private static function short_num( $v ) { $v = (float) $v; if ( $v >= 1000000 ) { return rtrim( rtrim( number_format( $v / 1000000, 1, '.', '' ), '0' ), '.' ) . 'M'; } if ( $v >= 1000 ) { return rtrim( rtrim( number_format( $v / 1000, 1, '.', '' ), '0' ), '.' ) . 'K'; } return (string) round( $v ); } /** Round the axis maximum up to a "nice" number (1, 2, 2.5, 5 × 10^n). */ private static function nice_max( $v ) { if ( $v <= 0 ) { return 1; } $exp = floor( log10( $v ) ); $base = pow( 10, $exp ); foreach ( array( 1, 2, 2.5, 5, 10 ) as $m ) { if ( $v <= $m * $base ) { return $m * $base; } } return 10 * $base; } /** * Revenue trend: bars = revenue per day, line = bookings per day * (right axis). $days: list of objects { day: 'Y-m-d', revenue, bookings }, * oldest first, every day present (zeros included). */ public static function trend( $days ) { $w = 760; $h = 280; $pl = 56; $pr = 40; $pt = 16; $pb = 34; $cw = $w - $pl - $pr; $ch = $h - $pt - $pb; $n = max( 1, count( $days ) ); $max_rev = self::nice_max( max( array_merge( array( 0 ), array_map( fn( $d ) => (float) $d->revenue, $days ) ) ) ); // Bookings are whole numbers: a multiple of 4 keeps every one of // the 4 gridlines on an integer (no "3.8 bookings" ticks). $max_bk = max( 4, (int) ceil( max( array_merge( array( 0 ), array_map( fn( $d ) => (int) $d->bookings, $days ) ) ) / 4 ) * 4 ); $slot = $cw / $n; $bw = max( 2, min( 22, $slot * 0.62 ) ); ob_start(); ?>