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 = intval($matches[1]); $this->diameterUnit = $matches[2]; } else { throw new Exception('Width "' . $width . '" is not a valid width'); } } /** * 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; return sprintf('%.' . $precision . 'F%s', $this->circleDiameter * $factor, $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) { if ($this->session !== null) { // TODO: Do not use this if the interval has changed or the user did a reload $this->calculationBase = $this->session->get('calculationBase'); } if ($create) { $new = $this->generateCalculationBase(); if ($new > $this->calculationBase) { $this->calculationBase = $new; if ($this->session !== null) { $this->session->calculationBase = $new; Session::getSession()->write(); // TODO: Should it be possible to call write() on the namespace? } } } } 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->getQuery(); $queryColumns = $query->getColumns(); $query->where( $query->isValidFilterTarget('name') ? 'name' : $queryColumns['name'], array_keys($this->identifiers) )->where('raw_timestamp <= ?', $this->displayRange->getStart()->getTimestamp()) ->where('raw_timestamp > ?', $this->forecastRange->getEnd()->getTimestamp()); return array_merge($query->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); $groups[$timestamp][$name] = TimeEntry::fromArray( array_merge( $this->identifiers[$name], array( 'name' => $name, 'value' => $count, 'dateTime' => $dateTime ) ) ); } } 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; } /** * Build the legend */ private function buildLegend() { // TODO: Put this in some sort of dedicated stylesheet $circleStyle = 'width:75px;height:75px;border-radius:50%;box-shadow:4px 4px 8px grey;border:2px solid;margin:auto;'; $labelStyle = 'font-size:12px;margin-top:10px;text-align:center;'; $titleStyle = 'margin-left:25px;'; $elements = array(); foreach ($this->getGroups() as $groupName => $groupInfo) { $groupColor = $groupInfo['color'] !== null ? $groupInfo['color'] : $this->getRandomCssColor(); $elements[] = '' . '
' . '

' . $groupName . '

'; } $legend = '' . '

' . t('Shown event groups') . '

' . '
' . implode( '', array_map( function ($e) { return '
' . $e . '
'; }, $elements ) ) . '
'; return $legend; } /** * Build the timeline */ public function buildTimeline() { $timelineGroups = array(); foreach ($this->displayData as $group) { $timestamp = $group->getDateTime()->getTimestamp(); if (!array_key_exists($timestamp, $timelineGroups)) { $timelineGroups[$timestamp] = array(); } $timelineGroups[$timestamp][] = $group; } $elements = array(); foreach ($this->range as $timestamp => $timeframe) { $elementGroups = array(); $biggestWidth = 0; if (array_key_exists($timestamp, $timelineGroups)) { foreach ($timelineGroups[$timestamp] as $group) { $circleWidth = $this->calculateCircleWidth( empty($elements) ? $this->extrapolateEventCount($group, 4) : $group->getValue() ); $groupColor = $group->getColor() !== null ? $group->getColor() : $this->getRandomCssColor(); $elementGroups[] = sprintf( '
' . ' ' . '
' . ' %3$s' . '
' . '
' . '
', $group->getDetailUrl(), $this->diameterUnit, $group->getValue(), $circleWidth, $groupColor ); if ($circleWidth > $biggestWidth) { $biggestWidth = $circleWidth; } } } $timeframeUrl = '';/*$this->getRequest()->getBaseUrl() . '/monitoring/list/eventhistory?timestamp<=' . $timeframe->start->getTimestamp() . '×tamp>=' . $timeframe->end->getTimestamp();*/ $elements[] = sprintf( '
%1$s
', implode('', $elementGroups), $this->diameterUnit, $biggestWidth ); $elements[] = '
'; $elements[] = '
' . $timeframe->end->format($this->getIntervalFormat()) . '' . '
'; } $elements[] = ''; return implode('', $elements); } }