toArray()); } /** * Create a new timeline * * The given dataview must provide the following columns: * - name A string identifying an entry (Corresponds to the keys of "$identifiers") * - time A unix timestamp that defines where to place an entry on the timeline * * @param DataView $dataview The dataview to fetch entries from * @param array $identifiers The names by which to group entries */ public function __construct(DataView $dataview, array $identifiers) { $this->dataview = $dataview; $this->identifiers = $identifiers; } /** * Set the session to use * * @param SessionNamespace $session The session to use */ public function setSession(SessionNamespace $session) { $this->session = $session; } /** * Set the range of time for which to display elements * * @param TimeRange $range The range of time for which to display elements */ public function setDisplayRange(TimeRange $range) { $this->displayRange = $range; } /** * Set the range of time for which to calculate forecasts * * @param TimeRange $range The range of time for which to calculate forecasts */ public function setForecastRange(TimeRange $range) { $this->forecastRange = $range; } /** * Set the maximum diameter each circle can have * * @param string $width The diameter to set, suffixed with its unit * * @throws Exception If the given diameter is invalid */ public function setMaximumCircleWidth($width) { $matches = array(); if (preg_match('#([\d|\.]+)([a-z]+|%)#', $width, $matches)) { $this->circleDiameter = floatval($matches[1]); $this->diameterUnit = $matches[2]; } else { throw new IcingaException( 'Width "%s" is not a valid width', $width ); } } /** * Set the minimum diameter each circle can have * * @param string $width The diameter to set, suffixed with its unit * * @throws Exception If the given diameter is invalid or its unit differs from the maximum */ public function setMinimumCircleWidth($width) { $matches = array(); if (preg_match('#([\d|\.]+)([a-z]+|%)#', $width, $matches)) { if ($matches[2] === $this->diameterUnit) { $this->minCircleDiameter = floatval($matches[1]); } else { throw new IcingaException( 'Unit needs to be in "%s"', $this->diameterUnit ); } } else { throw new IcingaException( 'Width "%s" is not a valid width', $width ); } } /** * Return all known group types (identifiers) with their respective labels and classess as array * * @return array */ public function getGroupInfo() { $groupInfo = array(); foreach ($this->identifiers as $name => $attributes) { if (isset($attributes['groupBy'])) { $name = $attributes['groupBy']; } $groupInfo[$name]['class'] = $attributes['class']; $groupInfo[$name]['label'] = $attributes['label']; } return $groupInfo; } /** * Return the circle's diameter for the given event group * * @param TimeEntry $group The group for which to return a circle width * @param int $precision Amount of decimal places to preserve * * @return string */ public function calculateCircleWidth(TimeEntry $group, $precision = 0) { $base = $this->getCalculationBase(true); $factor = log($group->getValue() * $group->getWeight(), $base) / 100; $width = $this->circleDiameter * $factor; return sprintf( '%.' . $precision . 'F%s', $width > $this->minCircleDiameter ? $width : $this->minCircleDiameter, $this->diameterUnit ); } /** * Return an extrapolated circle width for the given event group * * @param TimeEntry $group The event group for which to return an extrapolated circle width * @param int $precision Amount of decimal places to preserve * * @return string */ public function getExtrapolatedCircleWidth(TimeEntry $group, $precision = 0) { $eventCount = 0; foreach ($this->displayGroups as $groups) { if (array_key_exists($group->getName(), $groups)) { $eventCount += $groups[$group->getName()]->getValue(); } } $extrapolatedCount = (int) $eventCount / count($this->displayGroups); if ($extrapolatedCount < $group->getValue()) { return $this->calculateCircleWidth($group, $precision); } return $this->calculateCircleWidth( TimeEntry::fromArray( array( 'value' => $extrapolatedCount, 'weight' => $group->getWeight() ) ), $precision ); } /** * Return the base that should be used to calculate circle widths * * @param bool $create Whether to generate a new base if none is known yet * * @return float|null */ public function getCalculationBase($create) { if ($this->calculationBase === null) { $calculationBase = $this->session !== null ? $this->session->get('calculationBase') : null; if ($create) { $new = $this->generateCalculationBase(); if ($new > $calculationBase) { $this->calculationBase = $new; if ($this->session !== null) { $this->session->calculationBase = $new; } } else { $this->calculationBase = $calculationBase; } } else { return $calculationBase; } } return $this->calculationBase; } /** * Generate a new base to calculate circle widths with * * @return float */ protected function generateCalculationBase() { $allEntries = $this->groupEntries( array_merge( $this->fetchEntries(), $this->fetchForecasts() ), new TimeRange( $this->displayRange->getStart(), $this->forecastRange->getEnd(), $this->displayRange->getInterval() ) ); $highestValue = 0; foreach ($allEntries as $groups) { foreach ($groups as $group) { if ($group->getValue() * $group->getWeight() > $highestValue) { $highestValue = $group->getValue() * $group->getWeight(); } } } return pow($highestValue, 1 / 100); // 100 == 100% } /** * Fetch all entries and forecasts by using the dataview associated with this timeline * * @return array The dataview's result */ private function fetchResults() { $hookResults = array(); foreach (Hook::all('timeline') as $timelineProvider) { $hookResults = array_merge( $hookResults, $timelineProvider->fetchEntries($this->displayRange), $timelineProvider->fetchForecasts($this->forecastRange) ); foreach ($timelineProvider->getIdentifiers() as $identifier => $attributes) { if (!array_key_exists($identifier, $this->identifiers)) { $this->identifiers[$identifier] = $attributes; } } } $query = $this->dataview; $filter = Filter::matchAll( Filter::where('type', array_keys($this->identifiers)), Filter::expression('timestamp', '<=', $this->displayRange->getStart()->getTimestamp()), Filter::expression('timestamp', '>', $this->displayRange->getEnd()->getTimestamp()) ); $query->applyFilter($filter); return array_merge($query->getQuery()->fetchAll(), $hookResults); } /** * Fetch all entries * * @return array The entries to display on the timeline */ protected function fetchEntries() { if ($this->resultset === null) { $this->resultset = $this->fetchResults(); } $range = $this->displayRange; return array_filter( $this->resultset, function ($e) use ($range) { return $range->validateTime($e->time); } ); } /** * Fetch all forecasts * * @return array The entries to calculate forecasts with */ protected function fetchForecasts() { if ($this->resultset === null) { $this->resultset = $this->fetchResults(); } $range = $this->forecastRange; return array_filter( $this->resultset, function ($e) use ($range) { return $range->validateTime($e->time); } ); } /** * Return the given entries grouped together * * @param array $entries The entries to group * @param TimeRange $timeRange The range of time to group by * * @return array displayGroups The grouped entries */ protected function groupEntries(array $entries, TimeRange $timeRange) { $counts = array(); foreach ($entries as $entry) { $entryTime = new DateTime(); $entryTime->setTimestamp($entry->time); $timestamp = $timeRange->findTimeframe($entryTime, true); if ($timestamp !== null) { if (array_key_exists($entry->name, $counts)) { if (array_key_exists($timestamp, $counts[$entry->name])) { $counts[$entry->name][$timestamp] += 1; } else { $counts[$entry->name][$timestamp] = 1; } } else { $counts[$entry->name][$timestamp] = 1; } } } $groups = array(); foreach ($counts as $name => $data) { foreach ($data as $timestamp => $count) { $dateTime = new DateTime(); $dateTime->setTimestamp($timestamp); $groupName = $name; if (isset($this->identifiers[$name]['groupBy'])) { $groupName = $this->identifiers[$name]['groupBy']; } if (isset($groups[$timestamp][$groupName])) { $groups[$timestamp][$groupName]->setValue( $groups[$timestamp][$groupName]->getValue() + $count ); } else { $groups[$timestamp][$groupName] = TimeEntry::fromArray( array( 'name' => $groupName, 'value' => $count, 'dateTime' => $dateTime, 'class' => $this->identifiers[$name]['class'], 'detailUrl' => $this->identifiers[$name]['detailUrl'], 'label' => $this->identifiers[$name]['label'] ) ); } } } return $groups; } /** * Return the contents of this timeline as array * * @return array */ protected function toArray() { $this->displayGroups = $this->groupEntries($this->fetchEntries(), $this->displayRange); $array = array(); foreach ($this->displayRange as $timestamp => $timeframe) { $array[] = array( $timeframe, array_key_exists($timestamp, $this->displayGroups) ? $this->displayGroups[$timestamp] : array() ); } return $array; } }