2014-04-23 Alejandro Gallardo <alejandro.gallardo@artica.es>
* godmode/setup/setup_netflow.php: Added a prompt dialog to warning about the time increase caused by the address resolution. * include/functions_graph.php: Added the functions "graph_netflow_circular_mesh" and "graph_netflow_host_traffic". * include/functions_netflow.php: Added the function "netflow_get_record". Added IP address resolution. Added two new netflow live view items: "host detailed traffic" and "circular mesh". Error fixes on several functions. * include/graphs/functions_d3.php: Added the function "d3_tree_map_graph". Improved the function "d3_relationship_graph". * include/graphs/pandora.d3.js: Added the function "treeMap". Improved the function "chordDiagram". git-svn-id: https://svn.code.sf.net/p/pandora/code/trunk@9801 c3f86ba8-e40f-0410-aaad-9ba5e7f4b01f
This commit is contained in:
parent
b7a208da82
commit
d5d3b1286f
|
@ -1,3 +1,24 @@
|
|||
2014-04-23 Alejandro Gallardo <alejandro.gallardo@artica.es>
|
||||
|
||||
* godmode/setup/setup_netflow.php: Added a prompt dialog
|
||||
to warning about the time increase caused by the address
|
||||
resolution.
|
||||
|
||||
* include/functions_graph.php: Added the functions
|
||||
"graph_netflow_circular_mesh" and "graph_netflow_host_traffic".
|
||||
|
||||
* include/functions_netflow.php: Added the function
|
||||
"netflow_get_record". Added IP address resolution. Added
|
||||
two new netflow live view items: "host detailed traffic" and
|
||||
"circular mesh". Error fixes on several functions.
|
||||
|
||||
* include/graphs/functions_d3.php: Added the function
|
||||
"d3_tree_map_graph".
|
||||
Improved the function "d3_relationship_graph".
|
||||
|
||||
* include/graphs/pandora.d3.js: Added the function "treeMap".
|
||||
Improved the function "chordDiagram".
|
||||
|
||||
2014-04-23 Vanessa Gil <vanessa.gil@artica.es>
|
||||
|
||||
* include/functions_ui.php
|
||||
|
|
|
@ -41,27 +41,35 @@ $table->data = array ();
|
|||
$table->data[0][0] = '<b>' . __('Data storage path') . '</b>' .
|
||||
ui_print_help_tip (__("Directory where netflow data will be stored."), true);
|
||||
$table->data[0][1] = html_print_input_text ('netflow_path', $config['netflow_path'], false, 50, 200, true);
|
||||
|
||||
$table->data[1][0] = '<b>' . __('Daemon interval') . '</b>' .
|
||||
ui_print_help_tip (__("Specifies the time interval in seconds to rotate netflow data files."), true);
|
||||
$table->data[1][1] = html_print_input_text ('netflow_interval', $config['netflow_interval'], false, 50, 200, true);
|
||||
|
||||
$table->data[2][0] = '<b>' . __('Daemon binary path') . '</b>';
|
||||
$table->data[2][1] = html_print_input_text ('netflow_daemon', $config['netflow_daemon'], false, 50, 200, true);
|
||||
|
||||
$table->data[3][0] = '<b>' . __('Nfdump binary path') . '</b>';
|
||||
$table->data[3][1] = html_print_input_text ('netflow_nfdump', $config['netflow_nfdump'], false, 50, 200, true);
|
||||
|
||||
$table->data[4][0] = '<b>' . __('Nfexpire binary path') . '</b>';
|
||||
$table->data[4][1] = html_print_input_text ('netflow_nfexpire', $config['netflow_nfexpire'], false, 50, 200, true);
|
||||
|
||||
$table->data[5][0] = '<b>' . __('Maximum chart resolution') . '</b>' . ui_print_help_tip (__("Maximum number of points that a netflow area chart will display. The higher the resolution the performance. Values between 50 and 100 are recommended."), true);
|
||||
$table->data[5][1] = html_print_input_text ('netflow_max_resolution', $config['netflow_max_resolution'], false, 50, 200, true);
|
||||
|
||||
$table->data[6][0] = '<b>' . __('Disable custom live view filters') . '</b>' .
|
||||
ui_print_help_tip (__("Disable the definition of custom filters in the live view. Only existing filters can be used."), true);
|
||||
$table->data[6][1] = __('Yes').' '.html_print_radio_button ('netflow_disable_custom_lvfilters', 1, '', $config["netflow_disable_custom_lvfilters"], true).' ';
|
||||
$table->data[6][1] .= __('No').' '.html_print_radio_button ('netflow_disable_custom_lvfilters', 0, '', $config["netflow_disable_custom_lvfilters"], true).' ';
|
||||
$table->data[6][1] = __('Yes').' '.html_print_radio_button ('netflow_disable_custom_lvfilters', 1, '', $config["netflow_disable_custom_lvfilters"], true).' ';
|
||||
$table->data[6][1] .= __('No').' '.html_print_radio_button ('netflow_disable_custom_lvfilters', 0, '', $config["netflow_disable_custom_lvfilters"], true);
|
||||
$table->data[7][0] = '<b>' . __('Netflow max lifetime') . '</b>'.ui_print_help_tip (__("Sets the maximum lifetime for netflow data in days."), true);
|
||||
$table->data[7][1] = html_print_input_text ('netflow_max_lifetime', $config['netflow_max_lifetime'], false, 50, 200, true);
|
||||
|
||||
$table->data[8][0] = '<b>' . __('Name resolution for IP address') . '</b>' .
|
||||
ui_print_help_tip (__("Resolve the IP addresses to get their hostnames."), true);
|
||||
$table->data[8][1] = __('Yes').' '.html_print_radio_button ('netflow_get_ip_hostname', 1, '', $config["netflow_get_ip_hostname"], true).' ';
|
||||
$table->data[8][1] .= __('No').' '.html_print_radio_button ('netflow_get_ip_hostname', 0, '', $config["netflow_get_ip_hostname"], true).' ';
|
||||
$onclick = "if (!confirm('".__('Warning').". ".__('IP address resolution can take a lot of time').".')) return false;";
|
||||
$table->data[8][1] = __('Yes').' '.html_print_radio_button_extended ('netflow_get_ip_hostname', 1, '', $config["netflow_get_ip_hostname"], false, $onclick, '', true).' ';
|
||||
$table->data[8][1] .= __('No').' '.html_print_radio_button ('netflow_get_ip_hostname', 0, '', $config["netflow_get_ip_hostname"], true);
|
||||
|
||||
echo '<form id="netflow_setup" method="post">';
|
||||
|
||||
|
|
|
@ -3194,6 +3194,35 @@ function graph_netflow_aggregate_pie ($data, $aggregate, $ttl = 1, $only_image =
|
|||
$config['fontpath'], $config['font_size'], $ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a circular graph with the data transmitted between IPs
|
||||
*/
|
||||
function graph_netflow_circular_mesh ($data, $unit, $radius = 700) {
|
||||
global $config;
|
||||
|
||||
if (empty($data) || empty($data['elements']) || empty($data['matrix'])) {
|
||||
return fs_error_image ();
|
||||
}
|
||||
|
||||
include_once($config['homedir'] . "/include/graphs/functions_d3.php");
|
||||
|
||||
return d3_relationship_graph ($data['elements'], $data['matrix'], $unit, $radius, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a rescangular graph with the traffic of the ports for each IP
|
||||
*/
|
||||
function graph_netflow_host_traffic ($data, $unit, $width = 700, $height = 700) {
|
||||
global $config;
|
||||
|
||||
if (empty ($data)) {
|
||||
return fs_error_image ();
|
||||
}
|
||||
|
||||
include_once($config['homedir'] . "/include/graphs/functions_d3.php");
|
||||
|
||||
return d3_tree_map_graph ($data, $width, $height, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a graph of Module string data of agent
|
||||
|
|
|
@ -25,6 +25,9 @@ enterprise_include_once ($config['homedir'] . '/enterprise/include/functions_met
|
|||
global $nfdump_date_format;
|
||||
$nfdump_date_format = 'Y/m/d.H:i:s';
|
||||
|
||||
// Array to hold the hostnames
|
||||
$hostnames = array();
|
||||
|
||||
/**
|
||||
* Selects all netflow filters (array (id_name => id_name)) or filters filtered
|
||||
*
|
||||
|
@ -354,35 +357,35 @@ function netflow_summary_table ($data) {
|
|||
$table->style[0] = 'font-weight: bold; padding: 6px';
|
||||
$table->style[1] = 'padding: 6px';
|
||||
|
||||
$data = array();
|
||||
$data[] = __('Total flows');
|
||||
$data[] = format_numeric ($data['totalflows']);
|
||||
$table->data[] = $data;
|
||||
$row = array();
|
||||
$row[] = __('Total flows');
|
||||
$row[] = format_numeric ($data['totalflows']);
|
||||
$table->data[] = $row;
|
||||
|
||||
$data = array();
|
||||
$data[] = __('Total bytes');
|
||||
$data[] = format_numeric ($data['totalbytes']);
|
||||
$table->data[] = $data;
|
||||
$row = array();
|
||||
$row[] = __('Total bytes');
|
||||
$row[] = format_numeric ($data['totalbytes']);
|
||||
$table->data[] = $row;
|
||||
|
||||
$data = array();
|
||||
$data[] = __('Total packets');
|
||||
$data[] = format_numeric ($data['totalpackets']);
|
||||
$table->data[] = $data;
|
||||
$row = array();
|
||||
$row[] = __('Total packets');
|
||||
$row[] = format_numeric ($data['totalpackets']);
|
||||
$table->data[] = $row;
|
||||
|
||||
$data = array();
|
||||
$data[] = __('Average bits per second');
|
||||
$data[] = format_numeric ($data['avgbps']);
|
||||
$table->data[] = $data;
|
||||
$row = array();
|
||||
$row[] = __('Average bits per second');
|
||||
$row[] = format_numeric ($data['avgbps']);
|
||||
$table->data[] = $row;
|
||||
|
||||
$data = array();
|
||||
$data[] = __('Average packets per second');
|
||||
$data[] = format_numeric ($data['avgpps']);
|
||||
$table->data[] = $data;
|
||||
$row = array();
|
||||
$row[] = __('Average packets per second');
|
||||
$row[] = format_numeric ($data['avgpps']);
|
||||
$table->data[] = $row;
|
||||
|
||||
$data = array();
|
||||
$data[] = __('Average bytes per packet');
|
||||
$data[] = format_numeric ($data['avgbpp']);
|
||||
$table->data[] = $data;
|
||||
$row = array();
|
||||
$row[] = __('Average bytes per packet');
|
||||
$row[] = format_numeric ($data['avgbpp']);
|
||||
$table->data[] = $row;
|
||||
|
||||
$html = html_print_table ($table, true);
|
||||
|
||||
|
@ -457,10 +460,10 @@ function netflow_get_data ($start_date, $end_date, $interval_length, $filter, $a
|
|||
$command .= ' -q -o csv';
|
||||
|
||||
// Call nfdump
|
||||
$agg_command = $command . " -s $aggregate/bytes -n $max -t ".date($nfdump_date_format, $start_date).'-'.date($nfdump_date_format, $end_date);
|
||||
$agg_command = $command . " -n $max -s $aggregate/bytes -t ".date($nfdump_date_format, $start_date).'-'.date($nfdump_date_format, $end_date);
|
||||
exec($agg_command, $string);
|
||||
|
||||
// Reamove the first line
|
||||
// Remove the first line
|
||||
$string[0] = '';
|
||||
|
||||
// Parse aggregates
|
||||
|
@ -505,6 +508,29 @@ function netflow_get_data ($start_date, $end_date, $interval_length, $filter, $a
|
|||
$values = array ();
|
||||
}
|
||||
|
||||
// Address resolution start
|
||||
if ($config['netflow_get_ip_hostname'] && ($aggregate == "srcip" || $aggregate == "dstip")) {
|
||||
$get_hostnames = true;
|
||||
global $hostnames;
|
||||
|
||||
$sources = array();
|
||||
foreach ($values['sources'] as $source => $value) {
|
||||
if (!isset($hostnames[$source])) {
|
||||
$hostname = gethostbyaddr($source);
|
||||
if ($hostname !== false) {
|
||||
$hostnames[$source] = $hostname;
|
||||
$source = $hostname;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$source = $hostnames[$source];
|
||||
}
|
||||
$sources[$source] = $value;
|
||||
}
|
||||
$values['sources'] = $sources;
|
||||
}
|
||||
// Address resolution end
|
||||
|
||||
$interval_start = $start_date;
|
||||
for ($i = 0; $i < $num_intervals; $i++, $interval_start+=$interval_length+1) {
|
||||
$interval_end = $interval_start + $interval_length;
|
||||
|
@ -543,12 +569,29 @@ function netflow_get_data ($start_date, $end_date, $interval_length, $filter, $a
|
|||
foreach ($values['sources'] as $source => $discard) {
|
||||
$values['data'][$interval_start][$source] = 0;
|
||||
}
|
||||
|
||||
$data = netflow_get_stats ($interval_start, $interval_end, $filter, $aggregate, $max, $unit, $connection_name);
|
||||
foreach ($data as $line) {
|
||||
|
||||
// Address resolution start
|
||||
if ($get_hostnames) {
|
||||
if (!isset($hostnames[$line['agg']])) {
|
||||
$hostname = gethostbyaddr($line['agg']);
|
||||
if ($hostname !== false) {
|
||||
$hostnames[$line['agg']] = $hostname;
|
||||
$line['agg'] = $hostname;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$line['agg'] = $hostnames[$line['agg']];
|
||||
}
|
||||
}
|
||||
// Address resolution end
|
||||
|
||||
if (! isset ($values['sources'][$line['agg']])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
$values['data'][$interval_start][$line['agg']] = $line['data'];
|
||||
}
|
||||
}
|
||||
|
@ -574,7 +617,7 @@ function netflow_get_data ($start_date, $end_date, $interval_length, $filter, $a
|
|||
* @return An array with netflow stats.
|
||||
*/
|
||||
function netflow_get_stats ($start_date, $end_date, $filter, $aggregate, $max, $unit, $connection_name = '') {
|
||||
global $nfdump_date_format;
|
||||
global $config, $nfdump_date_format;
|
||||
|
||||
// Requesting remote data
|
||||
if (defined ('METACONSOLE') && $connection_name != '') {
|
||||
|
@ -586,7 +629,7 @@ function netflow_get_stats ($start_date, $end_date, $filter, $aggregate, $max, $
|
|||
$command = netflow_get_command ($filter);
|
||||
|
||||
// Execute nfdump
|
||||
$command .= " -o csv -q -s $aggregate/bytes -n $max -t " .date($nfdump_date_format, $start_date).'-'.date($nfdump_date_format, $end_date);
|
||||
$command .= " -o csv -q -n $max -s $aggregate/bytes -t " .date($nfdump_date_format, $start_date).'-'.date($nfdump_date_format, $end_date);
|
||||
exec($command, $string);
|
||||
|
||||
if (! is_array($string)) {
|
||||
|
@ -617,6 +660,24 @@ function netflow_get_stats ($start_date, $end_date, $filter, $aggregate, $max, $
|
|||
$values[$i]['agg'] = $val[3];
|
||||
}
|
||||
else {
|
||||
|
||||
// Address resolution start
|
||||
if ($config['netflow_get_ip_hostname'] && ($aggregate == "srcip" || $aggregate == "dstip")) {
|
||||
global $hostnames;
|
||||
|
||||
if (!isset($hostnames[$val[4]])) {
|
||||
$hostname = gethostbyaddr($val[4]);
|
||||
if ($hostname !== false) {
|
||||
$hostnames[$val[4]] = $hostname;
|
||||
$val[4] = $hostname;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$val[4] = $hostnames[$val[4]];
|
||||
}
|
||||
}
|
||||
// Address resolution end
|
||||
|
||||
$values[$i]['agg'] = $val[4];
|
||||
}
|
||||
if (! isset ($val[9])) {
|
||||
|
@ -675,7 +736,7 @@ function netflow_get_summary ($start_date, $end_date, $filter, $connection_name
|
|||
$command = netflow_get_command ($filter);
|
||||
|
||||
// Execute nfdump
|
||||
$command .= " -o csv -s srcip/bytes -n 1 -t " .date($nfdump_date_format, $start_date).'-'.date($nfdump_date_format, $end_date);
|
||||
$command .= " -o csv -n 1 -s srcip/bytes -t " .date($nfdump_date_format, $start_date).'-'.date($nfdump_date_format, $end_date);
|
||||
exec($command, $string);
|
||||
|
||||
if (! is_array($string) || ! isset ($string[5])) {
|
||||
|
@ -698,6 +759,113 @@ function netflow_get_summary ($start_date, $end_date, $filter, $connection_name
|
|||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a traffic record for the given period in an array.
|
||||
*
|
||||
* @param string start_date Period start date.
|
||||
* @param string end_date Period end date.
|
||||
* @param string filter Netflow filter.
|
||||
* @param int max Maximum number of elements.
|
||||
* @param string unit to show.
|
||||
*
|
||||
* @return An array with netflow stats.
|
||||
*/
|
||||
function netflow_get_record ($start_date, $end_date, $filter, $max, $unit) {
|
||||
global $nfdump_date_format;
|
||||
global $config;
|
||||
|
||||
// TIME_START = 0;
|
||||
// TIME_END = 1;
|
||||
// DURATION = 2;
|
||||
// SOURCE_ADDRESS = 3;
|
||||
// DESTINATION_ADDRESS = 4;
|
||||
// SOURCE_PORT = 5;
|
||||
// DESTINATION_PORT = 6;
|
||||
// PROTOCOL = 7;
|
||||
// INPUT_BYTES = 12;
|
||||
|
||||
// Get the command to call nfdump
|
||||
$command = netflow_get_command($filter);
|
||||
|
||||
// Execute nfdump
|
||||
$command .= " -q -o csv -n $max -s record/bytes -t " .date($nfdump_date_format, $start_date).'-'.date($nfdump_date_format, $end_date);
|
||||
exec($command, $result);
|
||||
|
||||
if (! is_array($result)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$values = array();
|
||||
foreach ($result as $key => $line) {
|
||||
$data = array();
|
||||
|
||||
$items = explode (',', $line);
|
||||
|
||||
$data['time_start'] = $items[0];
|
||||
$data['time_end'] = $items[1];
|
||||
$data['duration'] = $items[2] / 1000;
|
||||
$data['source_address'] = $items[3];
|
||||
$data['destination_address'] = $items[4];
|
||||
$data['source_port'] = $items[5];
|
||||
$data['destination_port'] = $items[6];
|
||||
$data['protocol'] = $items[7];
|
||||
|
||||
switch ($unit){
|
||||
case "megabytes":
|
||||
$data['data'] = $items[12] / 1048576;
|
||||
break;
|
||||
case "megabytespersecond":
|
||||
$data['data'] = $items[12] / 1048576 / $data['duration'];
|
||||
break;
|
||||
case "kilobytes":
|
||||
$data['data'] = $items[12] / 1024;
|
||||
break;
|
||||
case "kilobytespersecond":
|
||||
$data['data'] = $items[12] / 1024 / $data['duration'];
|
||||
break;
|
||||
default:
|
||||
case "bytes":
|
||||
$data['data'] = $items[12];
|
||||
break;
|
||||
case "bytespersecond":
|
||||
$data['data'] = $items[12] / $data['duration'];
|
||||
break;
|
||||
}
|
||||
$values[] = $data;
|
||||
}
|
||||
|
||||
// Address resolution start
|
||||
if ($config['netflow_get_ip_hostname']) {
|
||||
global $hostnames;
|
||||
|
||||
for ($i = 0; $i < count($values); $i++) {
|
||||
if (!isset($hostnames[$values[$i]['source_address']])) {
|
||||
$hostname = gethostbyaddr($values[$i]['source_address']);
|
||||
if ($hostname !== false) {
|
||||
$hostnames[$values[$i]['source_address']] = $hostname;
|
||||
$values[$i]['source_address'] = $hostname;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$values[$i]['source_address'] = $hostnames[$values[$i]['source_address']];
|
||||
}
|
||||
if (!isset($hostnames[$values[$i]['destination_address']])) {
|
||||
$hostname = gethostbyaddr($values[$i]['destination_address']);
|
||||
if ($hostname !== false) {
|
||||
$hostnames[$values[$i]['destination_address']] = $hostname;
|
||||
$values[$i]['destination_address'] = $hostname;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$values[$i]['destination_address'] = $hostnames[$values[$i]['destination_address']];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Address resolution end
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the command needed to run nfdump for the given filter.
|
||||
*
|
||||
|
@ -710,7 +878,7 @@ function netflow_get_command ($filter) {
|
|||
global $config;
|
||||
|
||||
// Build command
|
||||
$command = io_safe_output ($config['netflow_nfdump']) . ' -N -Otstart';
|
||||
$command = io_safe_output ($config['netflow_nfdump']) . ' -N';
|
||||
|
||||
// Netflow data path
|
||||
if (isset($config['netflow_path']) && $config['netflow_path'] != '') {
|
||||
|
@ -846,7 +1014,9 @@ function netflow_get_chart_types () {
|
|||
'netflow_area' => __('Area graph'),
|
||||
'netflow_pie_summatory' => __('Pie graph and Summary table'),
|
||||
'netflow_statistics' => __('Statistics table'),
|
||||
'netflow_data' => __('Data table'));
|
||||
'netflow_data' => __('Data table'),
|
||||
'netflow_mesh' => __('Circular mesh'),
|
||||
'netflow_host_treemap' => __('Host detailed traffic'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1105,6 +1275,111 @@ function netflow_draw_item ($start_date, $end_date, $interval_length, $type, $fi
|
|||
break;
|
||||
}
|
||||
break;
|
||||
case 'netflow_mesh':
|
||||
$netflow_data = netflow_get_record($start_date, $end_date, $filter, $max_aggregates, $unit);
|
||||
|
||||
switch ($aggregate) {
|
||||
case "srcport":
|
||||
case "dstport":
|
||||
$source_type = "source_port";
|
||||
$destination_type = "destination_port";
|
||||
break;
|
||||
default:
|
||||
case "dstip":
|
||||
case "srcip":
|
||||
$source_type = "source_address";
|
||||
$destination_type = "destination_address";
|
||||
break;
|
||||
}
|
||||
|
||||
$data = array();
|
||||
$data['elements'] = array();
|
||||
$data['matrix'] = array();
|
||||
foreach ($netflow_data as $record) {
|
||||
if (!in_array($record[$source_type], $data['elements'])) {
|
||||
$data['elements'][] = $record[$source_type];
|
||||
$data['matrix'][] = array();
|
||||
}
|
||||
if (!in_array($record[$destination_type], $data['elements'])) {
|
||||
$data['elements'][] = $record[$destination_type];
|
||||
$data['matrix'][] = array();
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0; $i < count($data['matrix']); $i++) {
|
||||
$data['matrix'][$i] = array_fill(0, count($data['matrix']), 0);
|
||||
}
|
||||
|
||||
foreach ($netflow_data as $record) {
|
||||
$source_key = array_search($record[$source_type], $data['elements']);
|
||||
$destination_key = array_search($record[$destination_type], $data['elements']);
|
||||
if ($source_key !== false && $destination_key !== false) {
|
||||
$data['matrix'][$source_key][$destination_key] += $record['data'];
|
||||
}
|
||||
}
|
||||
|
||||
$html = "<div style=\"text-align:center;\">";
|
||||
$html .= graph_netflow_circular_mesh ($data, netflow_format_unit($unit), 700);
|
||||
$html .= "</div>";
|
||||
|
||||
return $html;
|
||||
break;
|
||||
case 'netflow_host_treemap':
|
||||
$netflow_data = netflow_get_record($start_date, $end_date, $filter, $max_aggregates, $unit);
|
||||
|
||||
switch ($aggregate) {
|
||||
case "srcip":
|
||||
case "srcport":
|
||||
$address_type = "source_address";
|
||||
$port_type = "source_port";
|
||||
$type = __("Sent");
|
||||
break;
|
||||
default:
|
||||
case "dstip":
|
||||
case "dstport":
|
||||
$address_type = "destination_address";
|
||||
$port_type = "destination_port";
|
||||
$type = __("Received");
|
||||
break;
|
||||
}
|
||||
$data_aux = array();
|
||||
foreach ($netflow_data as $record) {
|
||||
$address = $record[$address_type];
|
||||
$port = $record[$port_type];
|
||||
|
||||
if (!isset($data_aux[$address]))
|
||||
$data_aux[$address] = array();
|
||||
|
||||
if (!isset($data_aux[$address][$port]))
|
||||
$data_aux[$address][$port] = 0;
|
||||
|
||||
$data_aux[$address][$port] += $record['data'];
|
||||
}
|
||||
|
||||
$id = -1;
|
||||
|
||||
$data = array();
|
||||
$data['name'] = __("Host detailed traffic") . ": " . $type;
|
||||
$data['children'] = array();
|
||||
|
||||
foreach ($data_aux as $address => $ports) {
|
||||
$children = array();
|
||||
$children['id'] = $id++;
|
||||
$children['name'] = $address;
|
||||
$children['children'] = array();
|
||||
foreach ($ports as $port => $value) {
|
||||
$children_data = array();
|
||||
$children_data['id'] = $id++;
|
||||
$children_data['name'] = $port;
|
||||
$children_data['value'] = $value;
|
||||
$children_data['tooltip_content'] = "$port: <b>" . format_numeric($value) . " " . netflow_format_unit($unit) . "</b>";
|
||||
$children['children'][] = $children_data;
|
||||
}
|
||||
$data['children'][] = $children;
|
||||
}
|
||||
|
||||
return graph_netflow_host_traffic ($data, netflow_format_unit($unit), 'auto', 400);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,11 @@ function include_javascript_d3 ($return = false) {
|
|||
function d3_relationship_graph ($elements, $matrix, $unit, $width = 700, $return = false) {
|
||||
global $config;
|
||||
|
||||
if (is_array($elements))
|
||||
$elements = json_encode($elements);
|
||||
if (is_array($matrix))
|
||||
$matrix = json_encode($matrix);
|
||||
|
||||
$output = "<div id=\"chord_diagram\"></div>";
|
||||
$output .= include_javascript_d3(true);
|
||||
$output .= "<script language=\"javascript\" type=\"text/javascript\">
|
||||
|
@ -49,5 +54,67 @@ function d3_relationship_graph ($elements, $matrix, $unit, $width = 700, $return
|
|||
return $output;
|
||||
}
|
||||
|
||||
function d3_tree_map_graph ($data, $width = 700, $height = 700, $return = false) {
|
||||
global $config;
|
||||
|
||||
if (is_array($data))
|
||||
$data = json_encode($data);
|
||||
|
||||
$output = "<div id=\"tree_map\"></div>";
|
||||
$output .= include_javascript_d3(true);
|
||||
$output .= "<style type=\"text/css\">
|
||||
.cell>rect {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
stroke: #EEEEEE;
|
||||
}
|
||||
|
||||
.chart {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.parent .label {
|
||||
color: #FFFFFF;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||
-webkit-text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||
-moz-text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.labelbody {
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 2px;
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||
-webkit-text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||
-moz-text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.child .label {
|
||||
white-space: pre-wrap;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.cell {
|
||||
font-size: 11px;
|
||||
cursor: pointer
|
||||
}
|
||||
</style>";
|
||||
$output .= "<script language=\"javascript\" type=\"text/javascript\">
|
||||
treeMap('#tree_map', $data, '$width', '$height');
|
||||
</script>";
|
||||
|
||||
if (!$return)
|
||||
echo $output;
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
?>
|
|
@ -13,7 +13,14 @@
|
|||
// GNU General Public License for more details.
|
||||
|
||||
|
||||
// https://github.com/fzaninotto/DependencyWheel
|
||||
// The recipient is the selector of the html element
|
||||
// The elements is an array with the names of the wheel elements
|
||||
// The matrix must be a 2 dimensional array with a row and a column for each element
|
||||
// Ex:
|
||||
// elements = ["a", "b", "c"];
|
||||
// matrix = [[0, 0, 2], // a[a => a, a => b, a => c]
|
||||
// [5, 0, 1], // b[b => a, b => b, b => c]
|
||||
// [2, 3, 0]]; // c[c => a, c => b, c => c]
|
||||
function chordDiagram (recipient, elements, matrix, unit, width) {
|
||||
|
||||
d3.chart = d3.chart || {};
|
||||
|
@ -52,8 +59,7 @@ function chordDiagram (recipient, elements, matrix, unit, width) {
|
|||
.outerRadius(radius + 20);
|
||||
|
||||
var fill = function(d) {
|
||||
if (d.index === 0) return '#ccc';
|
||||
return "hsl(" + parseInt(((elements[d.index][0].charCodeAt() - 97) / 26) * 360, 10) + ",90%,70%)";
|
||||
return "hsl(" + parseInt((d.index / 26) * 360, 10) + ",80%,70%)";
|
||||
};
|
||||
|
||||
// Returns an event handler for fading a given chord group.
|
||||
|
@ -133,14 +139,93 @@ function chordDiagram (recipient, elements, matrix, unit, width) {
|
|||
.style("opacity", 1);
|
||||
|
||||
// Add an elaborate mouseover title for each chord.
|
||||
gEnter.selectAll("path.chord").append("title").text(function(d) {
|
||||
return elements[d.source.index]
|
||||
+ " → " + elements[d.target.index]
|
||||
+ ": " + d.source.value + " " + unit
|
||||
+ "\n" + elements[d.target.index]
|
||||
+ " → " + elements[d.source.index]
|
||||
+ ": " + d.target.value + " " + unit;
|
||||
});
|
||||
gEnter.selectAll("path.chord")
|
||||
.on("mouseover", over_user)
|
||||
.on("mouseout", out_user)
|
||||
.on("mousemove", move_tooltip);
|
||||
|
||||
function move_tooltip(d) {
|
||||
x = d3.event.pageX + 10;
|
||||
y = d3.event.pageY + 10;
|
||||
|
||||
$("#tooltip").css('left', x + 'px');
|
||||
$("#tooltip").css('top', y + 'px');
|
||||
}
|
||||
|
||||
function over_user(d) {
|
||||
id = d.id;
|
||||
|
||||
$("#" + id).css('border', '1px solid black');
|
||||
$("#" + id).css('z-index', '1');
|
||||
|
||||
show_tooltip(d);
|
||||
}
|
||||
|
||||
function out_user(d) {
|
||||
id = d.id;
|
||||
|
||||
$("#" + id).css('border', '');
|
||||
$("#" + id).css('z-index', '');
|
||||
|
||||
hide_tooltip();
|
||||
}
|
||||
|
||||
function create_tooltip(d, x, y) {
|
||||
if ($("#tooltip").length == 0) {
|
||||
$(recipient)
|
||||
.append($("<div></div>")
|
||||
.attr('id', 'tooltip')
|
||||
.html(
|
||||
elements[d.source.index]
|
||||
+ " → "
|
||||
+ elements[d.target.index]
|
||||
+ ": <b>" + d.source.value.toFixed(2) + " " + unit + "</b>"
|
||||
+ "<br>"
|
||||
+ elements[d.target.index]
|
||||
+ " → "
|
||||
+ elements[d.source.index]
|
||||
+ ": <b>" + d.target.value.toFixed(2) + " " + unit + "</b>"
|
||||
));
|
||||
}
|
||||
else {
|
||||
$("#tooltip").html(
|
||||
elements[d.source.index]
|
||||
+ " → "
|
||||
+ elements[d.target.index]
|
||||
+ ": <b>" + d.source.value.toFixed(2) + " " + unit + "</b>"
|
||||
+ "<br>"
|
||||
+ elements[d.target.index]
|
||||
+ " → "
|
||||
+ elements[d.source.index]
|
||||
+ ": <b>" + d.target.value.toFixed(2) + " " + unit + "</b>"
|
||||
);
|
||||
}
|
||||
|
||||
$("#tooltip").attr('style', 'background: #fff;' +
|
||||
'position: absolute;' +
|
||||
'display: inline-block;' +
|
||||
'width: auto;' +
|
||||
'max-width: 500px;' +
|
||||
'text-align: left;' +
|
||||
'padding: 10px 10px 10px 10px;' +
|
||||
'z-index: 2;' +
|
||||
"-webkit-box-shadow: 7px 7px 5px rgba(50, 50, 50, 0.75);" +
|
||||
"-moz-box-shadow: 7px 7px 5px rgba(50, 50, 50, 0.75);" +
|
||||
"box-shadow: 7px 7px 5px rgba(50, 50, 50, 0.75);" +
|
||||
'left: ' + x + 'px;' +
|
||||
'top: ' + y + 'px;');
|
||||
}
|
||||
|
||||
function show_tooltip(d) {
|
||||
x = d3.event.pageX + 10;
|
||||
y = d3.event.pageY + 10;
|
||||
|
||||
create_tooltip(d, x, y);
|
||||
}
|
||||
|
||||
function hide_tooltip() {
|
||||
$("#tooltip").hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -176,4 +261,397 @@ function chordDiagram (recipient, elements, matrix, unit, width) {
|
|||
matrix: matrix
|
||||
})
|
||||
.call(chart);
|
||||
}
|
||||
|
||||
// The recipient is the selector of the html element
|
||||
// The data must be a bunch of associative arrays like this
|
||||
// data = {
|
||||
// "name": "IP Traffic",
|
||||
// "id": 0,
|
||||
// "children": [
|
||||
// {
|
||||
// "name": "192.168.1.1",
|
||||
// "id": 1,
|
||||
// "children": [
|
||||
// {
|
||||
// "name": "HTTP",
|
||||
// "id": 2,
|
||||
// "value": 33938
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// "name": "192.168.1.2",
|
||||
// "id": 3,
|
||||
// "children": [
|
||||
// {
|
||||
// "name": "HTTP",
|
||||
// "id": 4,
|
||||
// "value": 3938
|
||||
// },
|
||||
// {
|
||||
// "name": "FTP",
|
||||
// "id": 5,
|
||||
// "value": 1312
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// };
|
||||
function treeMap(recipient, data, width, height) {
|
||||
|
||||
//var isIE = BrowserDetect.browser == 'Explorer';
|
||||
var isIE = true;
|
||||
var chartWidth = width;
|
||||
var chartHeight = height;
|
||||
if (width === 'auto') {
|
||||
chartWidth = $(recipient).innerWidth();
|
||||
}
|
||||
if (height === 'auto') {
|
||||
chartHeight = $(recipient).innerHeight();
|
||||
}
|
||||
var xscale = d3.scale.linear().range([0, chartWidth]);
|
||||
var yscale = d3.scale.linear().range([0, chartHeight]);
|
||||
var color = d3.scale.category10();
|
||||
var headerHeight = 20;
|
||||
var headerColor = "#555555";
|
||||
var transitionDuration = 500;
|
||||
var root;
|
||||
var node;
|
||||
|
||||
var treemap = d3.layout.treemap()
|
||||
.round(false)
|
||||
.size([chartWidth, chartHeight])
|
||||
.sticky(true)
|
||||
.value(function(d) {
|
||||
return d.value;
|
||||
});
|
||||
|
||||
var chart = d3.select(recipient)
|
||||
.append("svg:svg")
|
||||
.attr("width", chartWidth)
|
||||
.attr("height", chartHeight)
|
||||
.append("svg:g");
|
||||
|
||||
node = root = data;
|
||||
var nodes = treemap.nodes(root);
|
||||
|
||||
var children = nodes.filter(function(d) {
|
||||
return !d.children;
|
||||
});
|
||||
var parents = nodes.filter(function(d) {
|
||||
return d.children;
|
||||
});
|
||||
|
||||
// create parent cells
|
||||
var parentCells = chart.selectAll("g.cell.parent")
|
||||
.data(parents, function(d) {
|
||||
return d.id;
|
||||
});
|
||||
var parentEnterTransition = parentCells.enter()
|
||||
.append("g")
|
||||
.attr("class", "cell parent")
|
||||
.on("click", function(d) {
|
||||
zoom(node === d ? root : d);
|
||||
});
|
||||
parentEnterTransition.append("rect")
|
||||
.attr("width", function(d) {
|
||||
return Math.max(0.01, d.dx);
|
||||
})
|
||||
.attr("height", headerHeight)
|
||||
.style("fill", headerColor);
|
||||
parentEnterTransition.append('foreignObject')
|
||||
.attr("class", "foreignObject")
|
||||
.append("xhtml:div")
|
||||
.attr("class", "labelbody")
|
||||
.append("div")
|
||||
.attr("class", "label");
|
||||
// update transition
|
||||
var parentUpdateTransition = parentCells.transition().duration(transitionDuration);
|
||||
parentUpdateTransition.select(".cell")
|
||||
.attr("transform", function(d) {
|
||||
return "translate(" + d.dx + "," + d.y + ")";
|
||||
});
|
||||
parentUpdateTransition.select("rect")
|
||||
.attr("width", function(d) {
|
||||
return Math.max(0.01, d.dx);
|
||||
})
|
||||
.attr("height", headerHeight)
|
||||
.style("fill", headerColor);
|
||||
parentUpdateTransition.select(".foreignObject")
|
||||
.attr("width", function(d) {
|
||||
return Math.max(0.01, d.dx);
|
||||
})
|
||||
.attr("height", headerHeight)
|
||||
.select(".labelbody .label")
|
||||
.text(function(d) {
|
||||
return d.name;
|
||||
});
|
||||
// remove transition
|
||||
parentCells.exit()
|
||||
.remove();
|
||||
|
||||
// create children cells
|
||||
var childrenCells = chart.selectAll("g.cell.child")
|
||||
.data(children, function(d) {
|
||||
return d.id;
|
||||
});
|
||||
// enter transition
|
||||
var childEnterTransition = childrenCells.enter()
|
||||
.append("g")
|
||||
.attr("class", "cell child")
|
||||
.on("mouseover", over_user)
|
||||
.on("mouseout", out_user)
|
||||
.on("mousemove", move_tooltip)
|
||||
.on("click", function(d) {
|
||||
zoom(node === d.parent ? root : d.parent);
|
||||
});
|
||||
childEnterTransition.append("rect")
|
||||
.classed("background", true)
|
||||
.style("fill", function(d) {
|
||||
return color(d.name);
|
||||
});
|
||||
childEnterTransition.append('foreignObject')
|
||||
.attr("class", "foreignObject")
|
||||
.attr("width", function(d) {
|
||||
return Math.max(0.01, d.dx);
|
||||
})
|
||||
.attr("height", function(d) {
|
||||
return Math.max(0.01, d.dy);
|
||||
})
|
||||
.append("xhtml:div")
|
||||
.attr("class", "labelbody")
|
||||
.append("div")
|
||||
.attr("class", "label")
|
||||
.text(function(d) {
|
||||
return d.name;
|
||||
});
|
||||
|
||||
if (isIE) {
|
||||
childEnterTransition.selectAll(".foreignObject .labelbody .label")
|
||||
.style("display", "none");
|
||||
} else {
|
||||
childEnterTransition.selectAll(".foreignObject")
|
||||
.style("display", "none");
|
||||
}
|
||||
|
||||
// update transition
|
||||
var childUpdateTransition = childrenCells.transition().duration(transitionDuration);
|
||||
childUpdateTransition.select(".cell")
|
||||
.attr("transform", function(d) {
|
||||
return "translate(" + d.x + "," + d.y + ")";
|
||||
});
|
||||
childUpdateTransition.select("rect")
|
||||
.attr("width", function(d) {
|
||||
return Math.max(0.01, d.dx);
|
||||
})
|
||||
.attr("height", function(d) {
|
||||
return d.dy;
|
||||
});
|
||||
childUpdateTransition.select(".foreignObject")
|
||||
.attr("width", function(d) {
|
||||
return Math.max(0.01, d.dx);
|
||||
})
|
||||
.attr("height", function(d) {
|
||||
return Math.max(0.01, d.dy);
|
||||
})
|
||||
.select(".labelbody .label")
|
||||
.text(function(d) {
|
||||
return d.name;
|
||||
});
|
||||
// exit transition
|
||||
childrenCells.exit()
|
||||
.remove();
|
||||
|
||||
d3.select("select").on("change", function() {
|
||||
treemap.value(this.value == "size" ? size : count)
|
||||
.nodes(root);
|
||||
zoom(node);
|
||||
});
|
||||
|
||||
zoom(node);
|
||||
|
||||
function size(d) {
|
||||
return d.size;
|
||||
}
|
||||
|
||||
function count(d) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
//and another one
|
||||
function textHeight(d) {
|
||||
var ky = chartHeight / d.dy;
|
||||
yscale.domain([d.y, d.y + d.dy]);
|
||||
return (ky * d.dy) / headerHeight;
|
||||
}
|
||||
|
||||
function getRGBComponents (color) {
|
||||
var r = color.substring(1, 3);
|
||||
var g = color.substring(3, 5);
|
||||
var b = color.substring(5, 7);
|
||||
return {
|
||||
R: parseInt(r, 16),
|
||||
G: parseInt(g, 16),
|
||||
B: parseInt(b, 16)
|
||||
};
|
||||
}
|
||||
|
||||
function idealTextColor (bgColor) {
|
||||
var nThreshold = 105;
|
||||
var components = getRGBComponents(bgColor);
|
||||
var bgDelta = (components.R * 0.299) + (components.G * 0.587) + (components.B * 0.114);
|
||||
return ((255 - bgDelta) < nThreshold) ? "#000000" : "#ffffff";
|
||||
}
|
||||
|
||||
function zoom(d) {
|
||||
treemap.padding([headerHeight/(chartHeight/d.dy), 0, 0, 0]).nodes(d);
|
||||
|
||||
// moving the next two lines above treemap layout messes up padding of zoom result
|
||||
var kx = chartWidth / d.dx;
|
||||
var ky = chartHeight / d.dy;
|
||||
var level = d;
|
||||
|
||||
xscale.domain([d.x, d.x + d.dx]);
|
||||
yscale.domain([d.y, d.y + d.dy]);
|
||||
|
||||
if (node != level) {
|
||||
if (isIE) {
|
||||
chart.selectAll(".cell.child .foreignObject .labelbody .label")
|
||||
.style("display", "none");
|
||||
} else {
|
||||
chart.selectAll(".cell.child .foreignObject")
|
||||
.style("display", "none");
|
||||
}
|
||||
}
|
||||
|
||||
var zoomTransition = chart.selectAll("g.cell").transition().duration(transitionDuration)
|
||||
.attr("transform", function(d) {
|
||||
return "translate(" + xscale(d.x) + "," + yscale(d.y) + ")";
|
||||
})
|
||||
.each("end", function(d, i) {
|
||||
if (!i && (level !== self.root)) {
|
||||
chart.selectAll(".cell.child")
|
||||
.filter(function(d) {
|
||||
return d.parent === self.node; // only get the children for selected group
|
||||
})
|
||||
.select(".foreignObject .labelbody .label")
|
||||
.style("color", function(d) {
|
||||
return idealTextColor(color(d.parent.name));
|
||||
});
|
||||
|
||||
if (isIE) {
|
||||
chart.selectAll(".cell.child")
|
||||
.filter(function(d) {
|
||||
return d.parent === self.node; // only get the children for selected group
|
||||
})
|
||||
.select(".foreignObject .labelbody .label")
|
||||
.style("display", "")
|
||||
} else {
|
||||
chart.selectAll(".cell.child")
|
||||
.filter(function(d) {
|
||||
return d.parent === self.node; // only get the children for selected group
|
||||
})
|
||||
.select(".foreignObject")
|
||||
.style("display", "")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
zoomTransition.select(".foreignObject")
|
||||
.attr("width", function(d) {
|
||||
return Math.max(0.01, kx * d.dx);
|
||||
})
|
||||
.attr("height", function(d) {
|
||||
return d.children ? headerHeight: Math.max(0.01, ky * d.dy);
|
||||
})
|
||||
.select(".labelbody .label")
|
||||
.text(function(d) {
|
||||
return d.name;
|
||||
});
|
||||
|
||||
// update the width/height of the rects
|
||||
zoomTransition.select("rect")
|
||||
.attr("width", function(d) {
|
||||
return Math.max(0.01, kx * d.dx);
|
||||
})
|
||||
.attr("height", function(d) {
|
||||
return d.children ? headerHeight : Math.max(0.01, ky * d.dy);
|
||||
});
|
||||
|
||||
node = d;
|
||||
|
||||
if (d3.event) {
|
||||
d3.event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function position() {
|
||||
this.style("left", function(d) { return d.x + "px"; })
|
||||
.style("top", function(d) { return d.y + "px"; })
|
||||
.style("width", function(d) { return Math.max(0, d.dx - 1) + "px"; })
|
||||
.style("height", function(d) { return Math.max(0, d.dy - 1) + "px"; });
|
||||
}
|
||||
|
||||
function move_tooltip(d) {
|
||||
x = d3.event.pageX + 10;
|
||||
y = d3.event.pageY + 10;
|
||||
|
||||
$("#tooltip").css('left', x + 'px');
|
||||
$("#tooltip").css('top', y + 'px');
|
||||
}
|
||||
|
||||
function over_user(d) {
|
||||
id = d.id;
|
||||
|
||||
$("#" + id).css('border', '1px solid black');
|
||||
$("#" + id).css('z-index', '1');
|
||||
|
||||
show_tooltip(d);
|
||||
}
|
||||
|
||||
function out_user(d) {
|
||||
id = d.id;
|
||||
|
||||
$("#" + id).css('border', '');
|
||||
$("#" + id).css('z-index', '');
|
||||
|
||||
hide_tooltip();
|
||||
}
|
||||
|
||||
function create_tooltip(d, x, y) {
|
||||
if ($("#tooltip").length == 0) {
|
||||
$(recipient)
|
||||
.append($("<div></div>")
|
||||
.attr('id', 'tooltip')
|
||||
.html(d.tooltip_content));
|
||||
}
|
||||
else {
|
||||
$("#tooltip").html(d.tooltip_content);
|
||||
}
|
||||
|
||||
$("#tooltip").attr('style', 'background: #fff;' +
|
||||
'position: absolute;' +
|
||||
'display: block;' +
|
||||
'width: 200px;' +
|
||||
'text-align: left;' +
|
||||
'padding: 10px 10px 10px 10px;' +
|
||||
'z-index: 2;' +
|
||||
"-webkit-box-shadow: 7px 7px 5px rgba(50, 50, 50, 0.75);" +
|
||||
"-moz-box-shadow: 7px 7px 5px rgba(50, 50, 50, 0.75);" +
|
||||
"box-shadow: 7px 7px 5px rgba(50, 50, 50, 0.75);" +
|
||||
'left: ' + x + 'px;' +
|
||||
'top: ' + y + 'px;');
|
||||
}
|
||||
|
||||
function show_tooltip(d) {
|
||||
x = d3.event.pageX + 10;
|
||||
y = d3.event.pageY + 10;
|
||||
|
||||
create_tooltip(d, x, y);
|
||||
}
|
||||
|
||||
function hide_tooltip() {
|
||||
$("#tooltip").hide();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue