From d5d3b1286f2bd9301aa4084843d106a78ae46ce1 Mon Sep 17 00:00:00 2001 From: alexhigh Date: Wed, 23 Apr 2014 09:34:00 +0000 Subject: [PATCH] 2014-04-23 Alejandro Gallardo * 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 --- pandora_console/ChangeLog | 21 + .../godmode/setup/setup_netflow.php | 16 +- pandora_console/include/functions_graph.php | 29 + pandora_console/include/functions_netflow.php | 339 ++++++++++-- .../include/graphs/functions_d3.php | 67 +++ pandora_console/include/graphs/pandora.d3.js | 500 +++++++++++++++++- 6 files changed, 925 insertions(+), 47 deletions(-) diff --git a/pandora_console/ChangeLog b/pandora_console/ChangeLog index 7ac864b6d1..c4cbb6cb6d 100644 --- a/pandora_console/ChangeLog +++ b/pandora_console/ChangeLog @@ -1,3 +1,24 @@ +2014-04-23 Alejandro Gallardo + + * 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 * include/functions_ui.php diff --git a/pandora_console/godmode/setup/setup_netflow.php b/pandora_console/godmode/setup/setup_netflow.php index abbaa3990b..b628428884 100644 --- a/pandora_console/godmode/setup/setup_netflow.php +++ b/pandora_console/godmode/setup/setup_netflow.php @@ -41,27 +41,35 @@ $table->data = array (); $table->data[0][0] = '' . __('Data storage path') . '' . 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] = '' . __('Daemon interval') . '' . 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] = '' . __('Daemon binary path') . ''; $table->data[2][1] = html_print_input_text ('netflow_daemon', $config['netflow_daemon'], false, 50, 200, true); + $table->data[3][0] = '' . __('Nfdump binary path') . ''; $table->data[3][1] = html_print_input_text ('netflow_nfdump', $config['netflow_nfdump'], false, 50, 200, true); + $table->data[4][0] = '' . __('Nfexpire binary path') . ''; $table->data[4][1] = html_print_input_text ('netflow_nfexpire', $config['netflow_nfexpire'], false, 50, 200, true); + $table->data[5][0] = '' . __('Maximum chart resolution') . '' . 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] = '' . __('Disable custom live view filters') . '' . 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] = '' . __('Netflow max lifetime') . ''.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] = '' . __('Name resolution for IP address') . '' . 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 '
'; diff --git a/pandora_console/include/functions_graph.php b/pandora_console/include/functions_graph.php index 138a440df7..e835628f68 100755 --- a/pandora_console/include/functions_graph.php +++ b/pandora_console/include/functions_graph.php @@ -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 diff --git a/pandora_console/include/functions_netflow.php b/pandora_console/include/functions_netflow.php index 187be59a74..4c3ad7d88f 100644 --- a/pandora_console/include/functions_netflow.php +++ b/pandora_console/include/functions_netflow.php @@ -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 = "
"; + $html .= graph_netflow_circular_mesh ($data, netflow_format_unit($unit), 700); + $html .= "
"; + + 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: " . format_numeric($value) . " " . netflow_format_unit($unit) . ""; + $children['children'][] = $children_data; + } + $data['children'][] = $children; + } + + return graph_netflow_host_traffic ($data, netflow_format_unit($unit), 'auto', 400); + break; default: break; } diff --git a/pandora_console/include/graphs/functions_d3.php b/pandora_console/include/graphs/functions_d3.php index 819d1dc808..b43158981d 100644 --- a/pandora_console/include/graphs/functions_d3.php +++ b/pandora_console/include/graphs/functions_d3.php @@ -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 = "
"; $output .= include_javascript_d3(true); $output .= ""; + + if (!$return) + echo $output; + + return $output; +} ?> \ No newline at end of file diff --git a/pandora_console/include/graphs/pandora.d3.js b/pandora_console/include/graphs/pandora.d3.js index 51cbd24c67..8671a387dd 100644 --- a/pandora_console/include/graphs/pandora.d3.js +++ b/pandora_console/include/graphs/pandora.d3.js @@ -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($("
") + .attr('id', 'tooltip') + .html( + elements[d.source.index] + + " → " + + elements[d.target.index] + + ": " + d.source.value.toFixed(2) + " " + unit + "" + + "
" + + elements[d.target.index] + + " → " + + elements[d.source.index] + + ": " + d.target.value.toFixed(2) + " " + unit + "" + )); + } + else { + $("#tooltip").html( + elements[d.source.index] + + " → " + + elements[d.target.index] + + ": " + d.source.value.toFixed(2) + " " + unit + "" + + "
" + + elements[d.target.index] + + " → " + + elements[d.source.index] + + ": " + d.target.value.toFixed(2) + " " + unit + "" + ); + } + + $("#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($("
") + .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(); + } } \ No newline at end of file