Esempio n. 1
0
function api_v1_graphs($graph)
{
    $start_time = microtime(true);
    $result = array();
    /**
     * Graph rendering goes like this:
     * 0. check graph rendering permissions
     * 1. get raw graph data (from a {@link GraphRenderer} through {@link construct_graph_renderer()})
     * 2. apply deltas as necessary
     * 3. add technicals as necessary
     * 4. strip dates outside of the requested ?days parameter (e.g. from extra_days)
     * 5. construct heading and links
     * 6. construct subheading and revise last_updated
     * 7. return data
     * that is, deltas and technicals are done on the server-side; not the client-side.
     */
    $renderer = construct_graph_renderer($graph['graph_type'], $graph['arg0'], $graph['arg0_resolved']);
    // 0. check graph rendering permissions
    if ($renderer->requiresUser()) {
        if (!isset($graph['user_id']) || !$graph['user_id']) {
            throw new GraphException("No user specified for authenticated graph");
        }
        if (!isset($graph['user_hash']) || !$graph['user_hash']) {
            throw new GraphException("No user hash specified for authenticated graph");
        }
        $user = get_user($graph['user_id']);
        if (!$user) {
            throw new GraphException("No such user found");
        }
        if (!has_expected_user_graph_hash($graph['user_hash'], $user)) {
            throw new GraphException("Mismatched user hash for user " . $graph['user_id'] . " with graph type " . $graph['graph_type']);
        }
        if ($renderer->requiresAdmin()) {
            if (!$user['is_admin']) {
                throw new GraphException("Graph requires administrator privileges");
            }
        }
        $renderer->setUser($user['id']);
    }
    if ($renderer->usesDays()) {
        // 0.5 limit 'days' parameter as necessary
        $get_permitted_days = get_permitted_days();
        $has_valid_days = false;
        foreach ($get_permitted_days as $key => $days) {
            if ($days['days'] == $graph['days']) {
                $has_valid_days = true;
            }
        }
        if (!$has_valid_days) {
            throw new GraphException("Invalid days '" . $graph['days'] . "' for graph that requires days");
        }
    }
    // 1. get raw graph data
    try {
        $data = $renderer->getData($graph['days']);
        $original_count = count($data['data']);
        $result['type'] = $renderer->getChartType();
        // 2. apply deltas as necessary
        $data['data'] = calculate_graph_deltas($graph, $data['data'], false);
        // if there is no data, bail out early
        if (count($data['data']) == 0) {
            $result['type'] = 'nodata';
        } else {
            if ($renderer->canHaveTechnicals()) {
                // 3. add technicals as necessary
                // (only if there is at least one point of data, otherwise calculate_technicals() will throw an error)
                $technicals = calculate_technicals($graph, $data['data'], $data['columns'], false);
                $data['columns'] = $technicals['headings'];
                $data['data'] = $technicals['data'];
            }
        }
        // 4. discard early data
        if ($renderer->usesDays()) {
            $data['data'] = discard_early_data($data['data'], $graph['days']);
            $after_discard_count = count($data['data']);
        }
        $result['columns'] = $data['columns'];
        $result['key'] = $data['key'];
        $result['data'] = $data['data'];
        // clean up columns
        foreach ($result['columns'] as $key => $value) {
            $result['columns'][$key]['technical'] = isset($result['columns'][$key]['technical']) && $result['columns'][$key]['technical'] ? true : false;
            if ($result['columns'][$key]['technical']) {
                if (!isset($result['columns'][$key]['type'])) {
                    $result['columns'][$key]['type'] = 'number';
                }
            }
        }
    } catch (NoDataGraphException_AddAccountsAddresses $e) {
        $result['type'] = 'nodata';
        $result['text'] = ct("Either you have not specified any accounts or addresses, or these addresses and accounts have not yet been updated by :site_name.");
        $result['args'] = array(':site_name' => get_site_config('site_name'));
        $result['data'] = array();
        $data['last_updated'] = false;
        $data['add_accounts_addresses'] = true;
    } catch (NoDataGraphException_AddCurrencies $e) {
        $result['type'] = 'nodata';
        $result['text'] = ct("Either you have not enabled this currency, or your summaries for this currency have not yet been updated by :site_name.");
        $result['args'] = array(':site_name' => get_site_config('site_name'));
        $result['data'] = array();
        $data['last_updated'] = false;
        $data['add_more_currencies'] = true;
    }
    // 5. construct heading and links
    $result['heading'] = array('label' => $renderer->getTitle(), 'args' => $renderer->getTitleArgs(), 'url' => $renderer->getURL(), 'title' => $renderer->getLabel());
    if (isset($data['h1'])) {
        $result['h1'] = $data['h1'];
    }
    if (isset($data['h2'])) {
        $result['h2'] = $data['h2'];
    }
    if (isset($data['no_header'])) {
        $result['noHeader'] = $data['no_header'];
    }
    // 6. construct subheading and revise last_updated\
    if ($result['type'] != 'nodata' && $renderer->hasSubheading()) {
        $suffix = "";
        if ($graph['delta'] == 'percent') {
            $suffix .= '%';
        }
        if ($renderer->getCustomSubheading() !== false) {
            $result['subheading'] = number_format_html($renderer->getCustomSubheading(), 4, $suffix);
        } else {
            if ($result['type'] == 'piechart') {
                // sum up the first row and use that as a total
                if (count($data['data']) != 1) {
                    throw new GraphException("Expected one row of data for a piechart, got " . count($data['data']));
                }
                $sum = 0;
                foreach ($data['data'] as $ignored => $row) {
                    foreach ($row as $value) {
                        $sum += $value;
                    }
                }
                $result['subheading'] = number_format_html($sum, 4, $suffix);
            } else {
                $result['subheading'] = format_subheading_values_objects($graph, $data['data'], $data['columns']);
            }
        }
    }
    $result['lastUpdated'] = recent_format_html($data['last_updated']);
    $result['timestamp'] = iso_date();
    $result['classes'] = $renderer->getClasses();
    $result['graph_type'] = $graph['graph_type'];
    if (is_localhost()) {
        $result['_debug'] = $graph;
        if (isset($after_discard_count)) {
            $result['_debug']['data_discarded'] = $original_count - $after_discard_count;
        } else {
            $result['_debug']['data_not_discarded'] = true;
        }
    }
    // make sure that all 'number'-typed data is numeric
    foreach ($result['data'] as $i => $row) {
        foreach ($row as $key => $value) {
            $column = $result['columns'][$key];
            if ($column['type'] == 'number' || $column['type'] == 'percent') {
                $result['data'][$i][$key] = (double) $value;
                if (is_localhost()) {
                    $result['_debug']['number_formatted'] = true;
                }
            }
        }
    }
    // make sure that all data rows are numeric arrays and not objects
    // i.e. reindex everything to be numeric arrays, so they aren't output as JSON objects
    foreach ($result['data'] as $i => $row) {
        $new_row = array_values($row);
        foreach ($row as $key => $value) {
            $new_row[$key] = $value;
        }
        $result['data'][$i] = $new_row;
    }
    // format any extra text from the result
    if (isset($data['add_more_currencies'])) {
        $result['extra'] = array('classes' => 'add_accounts', 'href' => url_for('wizard_currencies'), 'label' => ct("Add more currencies"), 'args' => array());
    }
    if (isset($data['add_accounts_addresses'])) {
        $result['extra'] = array('classes' => 'add_accounts', 'href' => url_for('wizard_accounts'), 'label' => ct("Add accounts and addresses"), 'args' => array());
    }
    // 7. calculate if the graph data may be out of date
    if ($renderer->requiresUser() && $renderer->getUser()) {
        $user = get_user($renderer->getUser());
        if ($user && $renderer->usesSummaries() && (!$user['has_added_account'] || !$user['is_first_report_sent'] || strtotime($user['last_account_change']) > strtotime($user['last_sum_job']))) {
            $result['outofdate'] = true;
        }
    }
    $end_time = microtime(true);
    $time_diff = ($end_time - $start_time) * 1000;
    $result['time'] = (double) number_format_autoprecision($time_diff, 1, '.', '');
    $result['hash'] = $graph['hash'];
    // 7. return data
    return $result;
}
Esempio n. 2
0
/**
 * Renders a collection of $sources with a given set of arguments $args, a user ID $user_id
 * and a heading callback function $get_heading_title.
 *
 * @param $has_subheadings true (default), false (no subheading), 'last_total' (total the most recent data)
 * @param $stacked if true, renders the graph as a stacked graph rather than line graph. defaults to false.
 * @param $make_proportional if true, converts all values to proportional data w.r.t. each date point, up to 100%. defaults to false.
 */
function render_sources_graph($graph, $sources, $args, $user_id, $get_heading_title, $has_subheadings = true, $stacked = false, $make_proportional = false)
{
    $data = array();
    $last_updated = false;
    $days = get_graph_days($graph);
    $extra_days = extra_days_necessary($graph);
    $exchanges_found = array();
    $maximum_balances = array();
    // only used to check for non-zero accounts
    $data_temp = array();
    $hide_missing_data = !require_get("debug_show_missing_data", false);
    $latest = array();
    foreach ($sources as $source) {
        $q = db()->prepare($source['query']);
        $q_args = $args;
        $q_args['user_id'] = $user_id;
        $q->execute($q_args);
        while ($ticker = $q->fetch()) {
            $key = date('Y-m-d', strtotime($ticker[$source['key']]));
            if (!isset($data_temp[$key])) {
                $data_temp[$key] = array();
            }
            if (!isset($data_temp[$key][$ticker['exchange']])) {
                $data_temp[$key][$ticker['exchange']] = 0;
            }
            $data_temp[$key][$ticker['exchange']] += $ticker[$source['balance_key']];
            $last_updated = max($last_updated, strtotime($ticker['created_at']));
            $exchanges_found[$ticker['exchange']] = $ticker['exchange'];
            if (!isset($maximum_balances[$ticker['exchange']])) {
                $maximum_balances[$ticker['exchange']] = 0;
            }
            $maximum_balances[$ticker['exchange']] = max($ticker[$source['balance_key']], $maximum_balances[$ticker['exchange']]);
            if (!isset($latest[$ticker['exchange']])) {
                $latest[$ticker['exchange']] = 0;
            }
            $latest[$ticker['exchange']] = max($latest[$ticker['exchange']], strtotime($ticker[$source['key']]));
        }
    }
    // get rid of any exchange summaries that had zero data
    foreach ($maximum_balances as $key => $balance) {
        if ($balance == 0) {
            foreach ($data_temp as $dt_key => $values) {
                unset($data_temp[$dt_key][$key]);
            }
            unset($exchanges_found[$key]);
        }
    }
    // sort by date so we can get previous dates if necessary for missing data
    ksort($data_temp);
    $data = array();
    // add headings after we know how many exchanges we've found
    $first_heading = array('title' => t("Date"));
    if ($make_proportional) {
        $first_heading['min'] = 0;
        $first_heading['max'] = 100;
    }
    $headings = array($first_heading);
    $i = 0;
    // sort them so they're always in the same order
    ksort($exchanges_found);
    foreach ($exchanges_found as $key => $ignored) {
        $headings[$key] = array('title' => $get_heading_title($key, $args), 'line_width' => 2, 'color' => default_chart_color(in_array(strtolower($key), get_all_currencies()) ? array_search(strtolower($key), get_all_currencies()) : $i++));
    }
    $data[0] = $headings;
    // add '0' for exchanges that we've found at one point, but don't have a data point
    // but reset to '0' for exchanges that are no longer present (i.e. from graph_data_balances archives)
    // this fixes a bug where old securities data is still displayed as present in long historical graphs
    $previous_row = array();
    foreach ($data_temp as $date => $values) {
        $row = array('new Date(' . date('Y, n-1, j', strtotime($date)) . ')');
        foreach ($exchanges_found as $key => $ignored) {
            if (!$hide_missing_data || strtotime($date) <= $latest[$key]) {
                if (!isset($values[$key])) {
                    $row[$key] = graph_number_format(isset($previous_row[$key]) ? $previous_row[$key] : 0);
                } else {
                    $row[$key] = graph_number_format(demo_scale($values[$key]));
                }
            } else {
                $row[$key] = graph_number_format(0);
            }
        }
        if (count($row) > 1) {
            // don't add empty rows
            $data[$date] = $row;
            $previous_row = $row;
        }
    }
    // make proportional?
    if ($make_proportional) {
        $data_temp = array();
        foreach ($data as $row => $columns) {
            $row_temp = array();
            if ($row == 0) {
                foreach ($columns as $key => $value) {
                    if ($key !== 0) {
                        $value['title'] .= " %";
                    }
                    $row_temp[$key] = $value;
                }
            } else {
                $total = 0;
                foreach ($columns as $key => $value) {
                    $total += $key === 0 ? 0 : $value;
                }
                foreach ($columns as $key => $value) {
                    $row_temp[$key] = $key === 0 ? $value : graph_number_format($total == 0 ? 0 : $value / $total * 100);
                }
            }
            $data_temp[$row] = $row_temp;
        }
        $data = $data_temp;
    }
    // sort each row by the biggest value in the most recent data
    // so e.g. BTC comes first, LTC comes second, regardless of order of summary_instances, balances etc
    $keys = array_keys($data);
    $last_row = $data[$keys[count($keys) - 1]];
    arsort($last_row);
    $data_temp = array();
    foreach ($data as $row => $columns) {
        $temp = array();
        $temp[0] = $columns[0];
        // keep row 0 the same
        foreach ($last_row as $key => $ignored) {
            if ($key !== 0) {
                $temp[$key] = $columns[$key];
            }
        }
        $data_temp[$row] = $temp;
    }
    $data = $data_temp;
    if (count($data) > 1) {
        // calculate deltas if necessary
        $data = calculate_graph_deltas($graph, $data);
        // calculate technicals
        // (only if there is at least one point of data, otherwise calculate_technicals() will throw an error)
        $data = calculate_technicals($graph, $data);
        // discard early data
        $data = discard_early_data($data, $days);
        // sort by key, but we only want values
        // we also need to sort by time *before* calculating subheadings
        uksort($data, 'cmp_time');
        if ($has_subheadings) {
            if ($has_subheadings == 'last_total') {
                $graph['subheading'] = format_subheading_values_subtotal($graph, $data);
            } else {
                $graph['subheading'] = format_subheading_values($graph, $data);
            }
        }
        $graph['last_updated'] = $last_updated;
        render_linegraph_date($graph, array_values($data), $stacked);
    } else {
        if ($user_id == get_site_config('system_user_id')) {
            render_text($graph, t("No data to display."));
            // or Invalid balance type.
        } else {
            render_text($graph, t("Either you have not enabled this balance, or your summaries for this balance have not yet been updated by :site_name.") . "<br><a class=\"add_accounts\" href=\"" . htmlspecialchars(url_for('wizard_currencies')) . "\">" . ht("Configure currencies") . "</a>");
        }
    }
}