max, line, area, points. */ public static function line( array $values, float $width, float $height, float $max = 0.0 ): array { $values = array_values( array_map( 'floatval', $values ) ); $count = count( $values ); if ( 0 === $count ) { return array( 'max' => 0.0, 'line' => '', 'area' => '', 'points' => array(), ); } if ( $max <= 0.0 ) { $max = self::nice_max( max( $values ) ); } // A single reading has no slope to draw, so it becomes a flat line across the full width — // truthful, and it keeps the shape of the card the same as every other day's. $step = $count > 1 ? $width / ( $count - 1 ) : 0.0; $points = array(); $line = ''; foreach ( $values as $index => $value ) { $x = $count > 1 ? $index * $step : $width / 2; $y = $height - ( $max > 0.0 ? ( $value / $max ) * $height : 0.0 ); $points[] = array( 'x' => self::round( $x ), 'y' => self::round( $y ), 'value' => $value, ); $line .= ( '' === $line ? 'M' : ' L' ) . self::round( $x ) . ',' . self::round( $y ); } if ( 1 === $count ) { $line .= ' L' . self::round( $width ) . ',' . self::round( $points[0]['y'] ); } $first = $points[0]; $last = $points[ count( $points ) - 1 ]; $right = 1 === $count ? self::round( $width ) : $last['x']; $area = 'M' . $first['x'] . ',' . self::round( $height ) . ' L' . substr( $line, 1 ) . ' L' . $right . ',' . self::round( $height ) . ' Z'; return array( 'max' => $max, 'line' => $line, 'area' => $area, 'points' => $points, ); } /** * Gridline values from zero to the top of the chart. * * @param float $max Value at the top. * @param int $count Number of intervals. * @return float[] Ascending, including both ends. */ public static function ticks( float $max, int $count = 4 ): array { $count = max( 1, $count ); if ( $max <= 0.0 ) { return array( 0.0 ); } $ticks = array(); for ( $i = 0; $i <= $count; $i++ ) { $ticks[] = $max * ( $i / $count ); } return $ticks; } /** * Round a maximum up to a value a reader can divide in their head. * * An axis topping out at 4,873 makes every gridline a number nobody can compare against; one * topping out at 5,000 makes the middle of the chart mean 2,500. * * @param float $max Largest value in the series. * @return float */ public static function nice_max( float $max ): float { if ( $max <= 0.0 ) { // An empty chart still needs an axis, or the line sits on the frame. return 1.0; } $magnitude = (float) pow( 10, floor( log10( $max ) ) ); $normal = $max / $magnitude; foreach ( array( 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 7.5, 10.0 ) as $step ) { if ( $normal <= $step ) { return $step * $magnitude; } } return 10.0 * $magnitude; } /** * Split a whole into ring segments, as dash lengths along a circle. * * Drawn with one stroked circle per segment rather than with arc paths, because a stroke keeps * its width however the ring is scaled and needs no trigonometry to stay closed. * * @param float[] $values Segment sizes. * @param float $circumference Length once round the circle. * @return array> length, gap, offset and share for each segment. */ public static function segments( array $values, float $circumference ): array { $values = array_values( array_map( 'floatval', $values ) ); $total = array_sum( $values ); if ( $total <= 0.0 || $circumference <= 0.0 ) { return array(); } $segments = array(); $consumed = 0.0; foreach ( $values as $value ) { $share = $value / $total; $length = $share * $circumference; $segments[] = array( 'length' => self::round( $length ), 'gap' => self::round( $circumference - $length ), // Dash offsets run backwards around the circle, which is why this is negative. 'offset' => self::round( -$consumed ), 'share' => $share, ); $consumed += $length; } return $segments; } /** * A value as a percentage of a total, guarding the division. * * @param float $value Part. * @param float $total Whole. * @return float Zero to one hundred. */ public static function share( float $value, float $total ): float { return $total > 0.0 ? ( $value / $total ) * 100.0 : 0.0; } /** * Trim a coordinate to the precision the markup keeps. * * @param float $value Raw coordinate. * @return float */ private static function round( float $value ): float { return round( $value, self::PRECISION ); } }