xaizek / rocketgit (License: AGPLv3+) (since 2018-12-09)
Light and fast Git hosting solution suitable to serve both as a hub or as a personal code storage with its tickets, pull requests, API and much more.
<root> / inc / graph.inc.php (6911cc6fd338bac3e9be901b45b4ef2806ba2cac) (9,507B) (mode 100644) [raw]
<?php

$rg_graph_error = '';

function rg_graph_set_error($str)
{
	global $rg_graph_error;
	$rg_graph_error = $str;
	rg_log('graph_set_error: ' . $str);
}

function rg_graph_error()
{
	global $rg_graph_error;
	return $rg_graph_error;
}

/*
 * Helper TODO
 */
function rg_graph_unit2info($unit)
{
	$ret = array();

	switch ($unit) {
	case 'minute':
		$ret['time_format'] = 'H';
		break;

	case 'hour':
		$ret['time_format'] = 'Y-m-d';
		break;

	case 'day':
		$ret['time_format'] = 'Y-m';
		break;

	case 'month':
		$ret['time_format'] = 'Y';
		break;

	case 'year':
		$ret['time_format'] = 'Y';
		break;

	default:
		$ret['time_format'] = 'Y-m-d';
		break;
	}

	return $ret;
}

/*
 * Helper for rg_graph_data_query to position a value in the correct interval
 */
function rg_graph_pos($itime, $unit)
{
	$y = gmdate('Y', $itime);
	$m = gmdate('m', $itime);
	$d = gmdate('d', $itime);
	$h = gmdate('H', $itime);
	$i = gmdate('i', $itime);

	switch ($unit) {
	case 'minute':
		$pos = gmmktime($h, $i, 0, $m, $d, $y);
		$next = gmmktime($h, $i + 1, 0, $m, $d, $y);
		$mark = gmmktime($h, 0, 0, $m, $d, $y);
		break;

	case 'hour':
		$pos = gmmktime($h, 0, 0, $m, $d, $y);
		$next = gmmktime($h + 1, 0, 0, $m, $d, $y);
		$mark = gmmktime(0, 0, 0, $m, $d, $y);
		break;

	case 'day':
		$pos = gmmktime(0, 0, 0, $m, $d, $y);
		$next = gmmktime(0, 0, 0, $m, $d + 1, $y);
		$mark = gmmktime(0, 0, 0, $m, 1, $y);
		break;

	case 'month':
		$pos = gmmktime(0, 0, 0, $m, 0, $y);
		$next = gmmktime(0, 0, 0, $m + 1, 0, $y);
		$mark = gmmktime(0, 0, 0, 1, 1, $y);
		break;

	case 'year':
		$pos = gmmktime(0, 0, 0, 0, 0, $y);
		$next = gmmktime(0, 0, 0, 0, 0, $y + 1);
		$mark = 0;
		break;

	default:
		rg_graph_set_error('invalid unit ' . $unit);
		$pos = FALSE;
	}

	if ($pos === FALSE)
		return FALSE;

	$ret = array('pos' => $pos, 'next' => $next);

	if ($mark == $pos)
		$ret['mark'] = $pos;

	return $ret;
}

/*
 * Loads data for graphs (helper)
 * TODO: what about DST?
 * @mode - 'avg' if we need averages per bar, 'sum' for sum
 * @decode_func - optional, a function to decode the data loaded from the database
 */
function rg_graph_query($db, $start, $end, $unit, $mode, $query, $params,
	$decode_func)
{
	rg_log_enter('graph_query start=' . $start . ' end=' . $end
		. ' unit=' . $unit . ' mode=' . $mode . ' query=' . $query);

	$ret = FALSE;
	$error = FALSE;
	while (1) {
		$res = rg_sql_query_params($db, $query, $params);
		if ($res === FALSE) {
			rg_user_set_error('query error: ' . rg_sql_error());
			break;
		}

		$ret = array(
			'list' => array(),
			'markers' => array(),
			'min' => 0,
			'max' => 0,
			'unit' => $unit
		);

		$x = rg_graph_pos(time(), $unit);
		if ($x !== FALSE)
			$ret['now_pos'] = $x['pos'];

		$counts = array();
		while (($row = rg_sql_fetch_array($res))) {
			$p = rg_graph_pos($row['itime'], $unit);
			if ($p === FALSE)
				break;

			if (!empty($decode_func)) {
				$_x = $decode_func($row['value']);
				if ($_x === FALSE) {
					$error = TRUE;
					break;
				}

				$row['value'] = $_x;
			}

			$pos = $p['pos'];
			if (!isset($ret['list'][$pos]))
				$ret['list'][$pos] = $row['value'];
			else
				$ret['list'][$pos] += $row['value'];

			if (!isset($counts[$pos]))
				$counts[$pos] = 1;
			else
				$counts[$pos]++;
		}
		rg_sql_free_result($res);
		if ($error)
			break;

		$pos = $start;
		while ($pos < $end) {
			$p = rg_graph_pos($pos, $unit);
			if ($p === FALSE) {
				$ret = FALSE;
				break;
			}

			$pos = $p['pos'];
			$next = $p['next'];

			if (isset($p['mark'])) {
				$mark = $p['mark'];
				$ret['markers'][$mark] = 1;
			}

			if (!isset($ret['list'][$pos])) {
				$ret['list'][$pos] = 0;
			} else if (strcmp($mode, 'avg') == 0) {
				$ret['list'][$pos] = intval($ret['list'][$pos] / $counts[$pos]);
			}

			if ($ret['min'] > $ret['list'][$pos])
				$ret['min'] = $ret['list'][$pos];
			if ($ret['max'] < $ret['list'][$pos])
				$ret['max'] = $ret['list'][$pos];

			$ret['last_data_pos'] = $pos;
			$pos = $next;
		}

		// Insert a fake entry to be able to show last time marker
		$ret['list'][$end + 1] = 0;

		ksort($ret['list']);
		break;
	}

	// final marker
	$p = rg_graph_pos($end + 1, $unit);
	if (($p !== FALSE) && isset($p['mark'])) {
		$mark = $p['mark'];
		$ret['markers'][$mark] = 1;
	}

	rg_log_exit();
	return $ret;
}

/*
 * Creates a SVG graph
 * TODO: user should be able to overwrite styles! Now we overwrite it.
 */
function rg_graph($a)
{
	rg_log_ml('DEBUG: a[data]: ' . print_r($a['data'], TRUE));
	if (!isset($a['scale_style']))
		$a['scale_style'] = 'font-size: 8pt';
	if (!isset($a['now_style']))
		$a['now_style'] = 'fill: #999';
	if (!isset($a['time_marker_line_style']))
		$a['time_marker_line_style'] = 'fill: #bbb';
	if (!isset($a['time_marker_text_style']))
		$a['time_marker_text_style'] = 'font-color: #000';
	if (!isset($a['footer_style']))
		$a['footer_style'] = '';

	$now = time();
	$data_start_x = 10;
	$data_start_y = 40;

	// TODO: devide by 1000, 1000^2 etc.
	$len = max(4, strlen(sprintf("%s", $a['data']['max'])));
	$data_start_x += $len * 8 + 3;

	$c = count($a['data']['list']);
	$a['w'] = ($c - 1) * $a['gap'] + $c * $a['data_line_width'];
	rg_log('DEBUG: count=' . count($a['data']['list']) . ' len=' . $len);
	rg_log('DEBUG: a[w]=' . $a['w']);

	$time_date_height = 25;
	$w = $data_start_x + 2 + $a['w'] + 2 + 10;
	$h = $data_start_y + 2 + $a['h'] + 1 + 2 + $time_date_height + 20;
	rg_log('DEBUG: w=' . $w);

	$ret = '<svg width="' . $w . '" height="' . $h . '"'
		. ' viewbox="0 0 ' . $w . ' ' . $h . '"'
		. ' class="stats_svg">' . "\n";

	// Background
	$ret .= '<rect x="0" y="0" width="100%" height="100%"'
		. ' style="' . $a['bg_style'] . '" />' . "\n";

	// Title
	$ret .= '<text x="10" y="20" style="' . $a['title_style']
		. '">' . $a['title'] . '</text>' . "\n";

	// Subtitle
	$a['subtitle_style'] .= '; text-anchor: end';
	$ret .= '<text x="' . ($data_start_x + 2 + $a['w'] + 2) . '"'
		. ' y="' . ($data_start_y - 5) . '"'
		. ' style="' . $a['subtitle_style'] . '">'
		. $a['subtitle'] . '</text>' . "\n";

	// Footer left
	$a['footer_left_style'] = $a['footer_style'] . '; text-anchor: begin';
	$t = '1 unit = 1 ' . $a['data']['unit'];
	$ret .= '<text x="' . ($data_start_x + 2) . '"'
		. ' y="' . ($data_start_y + 2 + $a['h'] + 3 + 12 + $time_date_height) . '"'
		. ' style="' . $a['footer_left_style']
		. '">' . $t . '</text>' . "\n";

	// Footer right
	$a['footer_right_style'] = $a['footer_style'] . '; text-anchor: end';
	$t = 'Generated at ' . gmdate('Y-m-d H:i') . ' UTC';
	$ret .= '<text x="' . ($data_start_x + 2 + $a['w'] + 2) . '"'
		. ' y="' . ($data_start_y + 2 + $a['h'] + 3 + 12 + $time_date_height) . '"'
		. ' style="' . $a['footer_right_style']
		. '">' . $t . '</text>' . "\n";

	// Left scale
	$a['scale_style'] .= '; text-anchor: end';
	$ret .= '<text x="' . ($data_start_x - 3) . '"'
		. ' y="' . ($data_start_y + 8) . '"'
		. ' style="' . $a['scale_style']
		. '">' . $a['data']['max'] . '</text>' . "\n";
	$ret .= '<text x="' . ($data_start_x - 3) . '"'
		. ' y="' . ($data_start_y + $a['h'] + 3) . '"'
		. ' style="' . $a['scale_style']
		. '">' . $a['data']['min'] . '</text>' . "\n";

	// Data rectangle
	$ret .= '<g>' . "\n";
	$ret .= '<rect x="' . $data_start_x . '"'
		. ' y="' . $data_start_y . '"'
		. ' width="' . (2 + $a['w'] + 2) . '"'
		. ' height="' . (2 + $a['h'] + 1 + 2) . '"'
		. ' style="' . $a['data_style'] . '" />' . "\n";

	// Base black line
	$ret .= '<rect'
		. ' x="' . ($data_start_x + 2) . '"'
		. ' y="' . ($data_start_y + 2 + $a['h']) . '"'
		. ' width="' . $a['w'] . '"'
		. ' height="1"'
		. ' style="fill: #333"'
		. '/>' . "\n";


	if ($a['data']['max'] == 0)
		$factor = 1;
	else
		$factor = $a['h'] / $a['data']['max'];
	$unit_info = rg_graph_unit2info($a['data']['unit']);
	$time_format = $unit_info['time_format'];
	$x = $data_start_x + 2;
	$y_base = $data_start_y + 2;
	foreach ($a['data']['list'] as $pos => $v0) {
		// Time markers
		if (isset($a['data']['markers'][$pos])) {
			$ret .= '<rect'
				. ' x="' . $x . '"'
				. ' y="' . $y_base . '"'
				. ' width="' . $a['data_line_width'] . '"'
				. ' height="' . $a['h'] . '"'
				. ' style="' . $a['time_marker_line_style'] . '"'
				. '/>' . "\n";

			$_x = $x + 3; $_y = $y_base + $a['h'] + 7;
			$ret .= '<text x="' . $_x . '"'
				. ' y="' . $_y . '"'
				. ' style="' . $a['time_marker_text_style'] . '; text-anchor: end; font-size: 4pt" transform="rotate(-45,' . $_x . ',' . $_y . ')">'
				. gmdate($time_format, $pos) . '</text>' . "\n";
		}

		// 'now' marker
		if ($pos == $a['data']['now_pos']) {
			$ret .= '<rect'
				. ' x="' . $x . '"'
				. ' y="' . $y_base . '"'
				. ' width="' . $a['data_line_width'] . '"'
				. ' height="' . $a['h'] . '"'
				. ' style="' . $a['now_style'] . '"'
				. '/>' . "\n";

			$ret .= '<text x="' . $x . '"'
				. ' y="' . ($y_base + 6) . '"'
				. ' style="' . $a['now_style'] . '; text-anchor: end; font-size: 5pt" transform="rotate(270,' . $x . ',' . $y_base . ')">'
				. 'now</text>' . "\n";
		}

		if ($pos > $a['data']['last_data_pos'])
			break;

		// value
		$v = sprintf('%u', $v0 * $factor);
		$y = $y_base + $a['h'] - $v;
		// TODO: data_line_style should be moved on the <g> tag!
		$ret .= '<rect'
			. ' x="' . $x . '"'
			. ' y="' . $y . '"'
			. ' width="' . $a['data_line_width'] . '"'
			. ' height="' . $v . '"'
			. ' style="' . $a['data_line_style'] . '"'
			. '/>' . "\n";

		$x += $a['data_line_width'] + $a['gap'];
	}
	$ret .= '</g>' . "\n";

	$ret .= '</svg>' . "\n";
	return $ret;
}

Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/rocketgit

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/rocketgit

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master