> $rows Rows of one product and currency, oldest first. */ public static function render( array $rows, Reference $reference, int $window_days, int $now ): string { if ( empty( $rows ) ) { return ''; } $segments = array(); foreach ( $rows as $row ) { $from = Repository::to_timestamp( $row['valid_from'] ); $to = null === $row['valid_to'] ? $now : Repository::to_timestamp( $row['valid_to'] ); if ( $to > $from ) { $segments[] = array( 'from' => $from, 'to' => $to, 'price' => (float) $row['price'], 'reduced' => (bool) $row['is_reduced'], ); } } if ( empty( $segments ) ) { return ''; } $t0 = $reference->t0; $window_start = null === $t0 ? null : $t0 - $window_days * DAY_IN_SECONDS; $desired = null === $window_start ? $now - 60 * DAY_IN_SECONDS : min( $window_start - 5 * DAY_IN_SECONDS, $now - 45 * DAY_IN_SECONDS ); $start = max( $segments[0]['from'], $desired ); if ( $now - $start < DAY_IN_SECONDS ) { $start = $now - DAY_IN_SECONDS; } $end = $now + (int) round( ( $now - $start ) * 0.02 ); // Value range of everything visible. $values = array(); foreach ( $segments as $segment ) { if ( $segment['to'] > $start ) { $values[] = $segment['price']; } } if ( $reference->has_price() ) { $values[] = (float) $reference->price; } $low = min( $values ); $high = max( $values ); $pad = $high > $low ? ( $high - $low ) * 0.25 : max( 1, $high * 0.1 ); $low = max( 0, $low - $pad ); $high = $high + $pad; $plot_w = self::WIDTH - self::LEFT - self::RIGHT; $plot_h = self::HEIGHT - self::TOP - self::BOTTOM; $x = static function ( int $ts ) use ( $start, $end, $plot_w ) { return round( self::LEFT + ( max( $start, min( $end, $ts ) ) - $start ) / ( $end - $start ) * $plot_w, 1 ); }; $y = static function ( float $price ) use ( $low, $high, $plot_h ) { return round( self::TOP + ( 1 - ( $price - $low ) / ( $high - $low ) ) * $plot_h, 1 ); }; $bottom = self::TOP + $plot_h; $svg = ''; } public static function legend( bool $reduced ): string { $html = '
' . esc_html__( 'Price incl. VAT', 'strikewise' ) . ''; if ( $reduced ) { $html .= '' . esc_html__( 'Reduced', 'strikewise' ) . ''; $html .= '' . esc_html__( '30 days before the reduction', 'strikewise' ) . ''; $html .= '' . esc_html__( 'Lowest price in that window', 'strikewise' ) . ''; } return $html . '
'; } public static function price( float $amount ): string { return html_entity_decode( wp_strip_all_tags( wc_price( $amount ) ), ENT_QUOTES, 'UTF-8' ); } /** * Two to four round values inside the range: steps of 1, 2, 2.5 or 5 times a power of ten. * * @return float[] */ private static function ticks( float $low, float $high ): array { $raw = ( $high - $low ) / 3; $power = 10 ** floor( log10( $raw ) ); $step = 10 * $power; foreach ( array( 1, 2, 2.5, 5 ) as $factor ) { if ( $factor * $power >= $raw ) { $step = $factor * $power; break; } } $ticks = array(); for ( $value = ceil( $low / $step ) * $step; $value < $high; $value += $step ) { if ( $value > $low ) { $ticks[] = round( $value, 2 ); } } return $ticks; } private static function short_price( float $amount ): string { return number_format_i18n( $amount, $amount >= 100 || floor( $amount ) === $amount ? 0 : 2 ) . ' ' . html_entity_decode( get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8' ); } private static function day( int $timestamp ): string { return wp_date( _x( 'M j', 'short date in the price chart', 'strikewise' ), $timestamp ); } }