. */ 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(); ?> $d ) : $cx = $pl + $slot * $i + $slot / 2; $bh = $max_rev > 0 ? ( (float) $d->revenue / $max_rev ) * $ch : 0; $line[] = round( $cx, 1 ) . ',' . round( $pt + $ch - ( (int) $d->bookings / $max_bk ) * $ch, 1 ); $tip = mysql2date( 'j M Y', $d->day ) . ' — ' . seatt_format_price( $d->revenue ) . ' · ' . sprintf( /* translators: %d: number */ _n( '%d booking', '%d bookings', (int) $d->bookings, 'seathive' ), (int) $d->bookings ); ?> 0 ) : ?> 20 ? 5 : ( $n > 10 ? 2 : 1 ); if ( 0 === $i % $every || $i === $n - 1 ) : ?> day ) ); ?> $pt_xy ) : list( $x, $y ) = explode( ',', $pt_xy ); if ( ! (int) array_values( $days )[ $li ]->bookings ) { continue; } ?>
max( 0, (float) $s['value'] ), $slices ) ); $r = 70; $c = 2 * M_PI * $r; $offset = 0; ob_start(); ?>
0 ) : foreach ( $slices as $s ) : $v = max( 0, (float) $s['value'] ); if ( $v <= 0 ) { continue; } $len = $c * $v / $total; ?>
(float) $s->pos_x, $seats ); $ys = array_map( fn( $s ) => (float) $s->pos_y, $seats ); $minx = min( $xs ); $maxx = max( $xs ); $miny = min( $ys ); $maxy = max( $ys ); $pad = 16; $w = max( 1, $maxx - $minx ); $h = max( 1, $maxy - $miny ); $W = 760; $k = min( ( $W - 2 * $pad ) / $w, 2.2 ); $H = max( 80, $h * $k + 2 * $pad ); // Seat radius from typical spacing, so dense maps don't overlap. $r = max( 2.2, min( 8, 10 * $k ) ); $colors = array( 'sold' => '#2271b1', 'comp' => '#8c5cd6', 'held' => '#dba617', 'available' => '#dcdcde' ); $labels = array( 'sold' => __( 'Sold', 'seathive' ), 'comp' => __( 'Complimentary', 'seathive' ), 'held' => __( 'On hold', 'seathive' ), 'available' => __( 'Available', 'seathive' ) ); $count = array_fill_keys( array_keys( $colors ), 0 ); ob_start(); ?>
status ? ( 'comp' === $s->paid_via ? 'comp' : 'sold' ) : ( 'held' === $s->status ? 'held' : 'available' ); $count[ $state ]++; $cx = $pad + ( (float) $s->pos_x - $minx ) * $k; $cy = $pad + ( (float) $s->pos_y - $miny ) * $k; ?> row_name, $s->label ) . ' — ' . $labels[ $state ] ); ?>
$c ) : if ( ! $count[ $key ] && 'comp' === $key ) { continue; } ?>
0 ? round( $done / $total * 100 ) : 0; ob_start(); ?>
· %