Class: Wads::GraphWidget

Inherits:
Widget
  • Object
show all
Defined in:
lib/wads/widgets.rb

Overview

Given a single node or a graph data structure, this widget displays a visualization of the graph using one of the available node widget classes. There are different display modes that control what nodes within the graph are shown. The default display mode, GRAPH_DISPLAY_ALL, shows all nodes as the name implies. GRAPH_DISPLAY_TREE assumes an acyclic graph and renders the graph in a tree-like structure. GRAPH_DISPLAY_EXPLORER has a chosen center focus node with connected nodes circled around it based on the depth or distance from that node. This mode also allows the user to click on different nodes to navigate the graph and change focus nodes.

Instance Attribute Summary collapse

Attributes inherited from Widget

#base_z, #children, #gui_theme, #height, #is_selected, #layout, #overlay_widget, #override_color, #text_input_fields, #visible, #width, #x, #y

Instance Method Summary collapse

Methods inherited from Widget

#add, #add_axis_lines, #add_button, #add_child, #add_delete_button, #add_document, #add_graph_display, #add_image, #add_multi_select_table, #add_overlay, #add_panel, #add_plot, #add_single_select_table, #add_table, #add_text, #border_color, #bottom_edge, #button_down, #button_up, #center_children, #center_x, #center_y, #clear_children, #contains_click, #debug, #disable_background, #disable_border, #draw, #draw_background, #draw_border, #enable_background, #enable_border, #error, #get_layout, #get_theme, #graphics_color, #handle_key_press, #handle_right_mouse, #info, #intercept_widget_event, #left_edge, #move_recursive_absolute, #move_recursive_delta, #overlaps_with, #relative_x, #relative_y, #relative_z_order, #remove_child, #remove_children, #remove_children_by_type, #right_edge, #selection_color, #set_absolute_position, #set_dimensions, #set_layout, #set_selected, #set_theme, #text_color, #top_edge, #unset_selected, #update, #uses_layout, #warn, #widget_z, #x_pixel_to_screen, #y_pixel_to_screen, #z_order

Constructor Details

#initialize(x, y, width, height, graph, display_mode = GRAPH_DISPLAY_ALL) ⇒ GraphWidget

Returns a new instance of GraphWidget.



2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
# File 'lib/wads/widgets.rb', line 2958

def initialize(x, y, width, height, graph, display_mode = GRAPH_DISPLAY_ALL) 
    super(x, y)
    set_dimensions(width, height)
    if graph.is_a? Node 
        @graph = Graph.new(graph)
    else
        @graph = graph 
    end
    @size_by_connections = false
    @is_explorer = false 
    if [GRAPH_DISPLAY_ALL, GRAPH_DISPLAY_TREE, GRAPH_DISPLAY_EXPLORER].include? display_mode 
        debug("Displaying graph in #{display_mode} mode")
    else 
        raise "#{display_mode} is not a valid display mode for Graph Widget"
    end
    if display_mode == GRAPH_DISPLAY_ALL
        set_all_nodes_for_display
    elsif display_mode == GRAPH_DISPLAY_TREE 
        set_tree_display
    else 
        set_explorer_display 
    end
end

Instance Attribute Details

#graphObject

Returns the value of attribute graph.



2951
2952
2953
# File 'lib/wads/widgets.rb', line 2951

def graph
  @graph
end

#is_explorerObject

Returns the value of attribute is_explorer.



2956
2957
2958
# File 'lib/wads/widgets.rb', line 2956

def is_explorer
  @is_explorer
end

#selected_nodeObject

Returns the value of attribute selected_node.



2952
2953
2954
# File 'lib/wads/widgets.rb', line 2952

def selected_node
  @selected_node
end

#selected_node_x_offsetObject

Returns the value of attribute selected_node_x_offset.



2953
2954
2955
# File 'lib/wads/widgets.rb', line 2953

def selected_node_x_offset
  @selected_node_x_offset
end

#selected_node_y_offsetObject

Returns the value of attribute selected_node_y_offset.



2954
2955
2956
# File 'lib/wads/widgets.rb', line 2954

def selected_node_y_offset
  @selected_node_y_offset
end

#size_by_connectionsObject

Returns the value of attribute size_by_connections.



2955
2956
2957
# File 'lib/wads/widgets.rb', line 2955

def size_by_connections
  @size_by_connections
end

Instance Method Details

#get_node_color(node) ⇒ Object



3209
3210
3211
3212
3213
3214
3215
# File 'lib/wads/widgets.rb', line 3209

def get_node_color(node)
    color_tag = node.get_tag(COLOR_TAG)
    if color_tag.nil? 
        return @color 
    end 
    color_tag
end

#handle_mouse_down(mouse_x, mouse_y) ⇒ Object



2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
# File 'lib/wads/widgets.rb', line 2989

def handle_mouse_down mouse_x, mouse_y
    # check to see if any node was selected
    if @rendered_nodes
        @rendered_nodes.values.each do |rn|
            if rn.contains_click(mouse_x, mouse_y)
                @selected_node = rn 
                @selected_node_x_offset = mouse_x - rn.x 
                @selected_node_y_offset = mouse_y - rn.y
                @click_timestamp = Time.now
            end
        end
    end
    WidgetResult.new(false)
end

#handle_mouse_up(mouse_x, mouse_y) ⇒ Object



3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
# File 'lib/wads/widgets.rb', line 3004

def handle_mouse_up mouse_x, mouse_y
    if @selected_node 
        if @is_explorer
            time_between_mouse_up_down = Time.now - @click_timestamp
            if time_between_mouse_up_down < 0.2
                # Treat this as a single click and make the selected
                # node the new center node of the graph
                set_explorer_display(@selected_node.data_node)
            end 
        end
        @selected_node = nil 
    end 
end

#handle_update(update_count, mouse_x, mouse_y) ⇒ Object



2982
2983
2984
2985
2986
2987
# File 'lib/wads/widgets.rb', line 2982

def handle_update update_count, mouse_x, mouse_y
    if contains_click(mouse_x, mouse_y) and @selected_node 
        @selected_node.move_recursive_absolute(mouse_x - @selected_node_x_offset,
                                               mouse_y - @selected_node_y_offset)
    end
end

#move_text_for_node(rendered_node) ⇒ Object



3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
# File 'lib/wads/widgets.rb', line 3110

def move_text_for_node(rendered_node)
    text = rendered_node.get_text_widget
    if text.nil? 
        return 
    end
    radians_between_attempts = DEG_360 / 24
    current_radians = 0.05
    done = false 
    while not done
        # Use radians to spread the other nodes around the center node
        # TODO base the distance off of scale
        text_x = rendered_node.center_x + ((rendered_node.width / 2) * Math.cos(current_radians))
        text_y = rendered_node.center_y - ((rendered_node.height / 2) * Math.sin(current_radians))
        if text_x < @x 
            text_x = @x + 1
        elsif text_x > right_edge - 20
            text_x = right_edge - 20
        end 
        if text_y < @y 
            text_y = @y + 1
        elsif text_y > bottom_edge - 26 
            text_y = bottom_edge - 26
        end
        text.x = text_x 
        text.y = text_y
        current_radians = current_radians + radians_between_attempts
        if overlaps_with_a_node(text)
            # check for done
            if current_radians > DEG_360
                done = true 
                error("ERROR: could not find a spot to put the text")
            end
        else 
            done = true
        end 
    end
end

#overlaps_with_a_node(text) ⇒ Object



3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
# File 'lib/wads/widgets.rb', line 3148

def overlaps_with_a_node(text)
    @rendered_nodes.values.each do |rn| 
        if text.label == rn.label 
            # don't compare to yourself 
        else 
            if rn.overlaps_with(text) 
                return true
            end
        end
    end
    false
end

#populate_rendered_nodes(center_node = nil) ⇒ Object



3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
# File 'lib/wads/widgets.rb', line 3241

def populate_rendered_nodes(center_node = nil)
    # Spread out the other nodes around the center node
    # going in a circle at each depth level
    stats = Stats.new("NodesPerDepth")
    @visible_data_nodes.values.each do |n|
        stats.increment(n.depth)
    end
    current_radians = []
    radians_increment = []
    (1..4).each do |n|
        number_of_nodes_at_depth = stats.count(n)
        radians_increment[n] = DEG_360 / number_of_nodes_at_depth.to_f
        current_radians[n] = 0.05
    end

    padding = 100
    size_of_x_band = (@width - padding) / 6
    size_of_y_band = (@height - padding) / 6
    random_x = size_of_x_band / 8
    random_y = size_of_y_band / 8
    half_random_x = random_x / 2
    half_random_y = random_y / 2

    # Precompute the band center points
    # then reference by the scale or depth values below
    band_center_x = padding + (size_of_x_band / 2) 
    band_center_y = padding + (size_of_y_band / 2) 
    # depth 1 [0] - center node, distance should be zero. Should be only one
    # depth 2 [1] - band one
    # depth 3 [2] - band two
    # depth 4 [3] - band three
    bands_x = [0, band_center_x]
    bands_x << band_center_x + size_of_x_band
    bands_x << band_center_x + size_of_x_band + size_of_x_band

    bands_y = [0, band_center_y]
    bands_y << band_center_y + size_of_y_band
    bands_y << band_center_y + size_of_y_band + size_of_y_band

    @visible_data_nodes.each do |node_name, data_node|
        process_this_node = true
        if center_node 
            if node_name == center_node.name 
                process_this_node = false 
            end 
        end
        if process_this_node 
            scale_to_use = 1
            if stats.count(1) > 0 and stats.count(2) == 0
                # if all nodes are depth 1, then size everything
                # as a small node
            elsif data_node.depth < 4
                scale_to_use = 5 - data_node.depth
            end
            if @is_explorer 
                # TODO Layer the nodes around the center
                # We need a better multiplier based on the height and width
                # max distance x would be (@width / 2) - padding
                # divide that into three regions, layer 2, 3, and 4
                # get the center point for each of these regions, and do a random from there
                # scale to use determines which of the regions
                band_index = 4 - scale_to_use
                distance_from_center_x = bands_x[band_index] + rand(random_x) - half_random_x
                distance_from_center_y = bands_y[band_index] + rand(random_y) - half_random_y
            else 
                distance_from_center_x = 80 + rand(200)
                distance_from_center_y = 40 + rand(100)
            end
            # Use radians to spread the other nodes around the center node
            radians_to_use = current_radians[data_node.depth]
            radians_to_use = radians_to_use + (rand(radians_increment[data_node.depth]) / 2)
            current_radians[data_node.depth] = current_radians[data_node.depth] + radians_increment[data_node.depth]
            node_x = center_x + (distance_from_center_x * Math.cos(radians_to_use))
            node_y = center_y - (distance_from_center_y * Math.sin(radians_to_use))
            if node_x < @x 
                node_x = @x + 1
            elsif node_x > right_edge - 20
                node_x = right_edge - 20
            end 
            if node_y < @y 
                node_y = @y + 1
            elsif node_y > bottom_edge - 26 
                node_y = bottom_edge - 26
            end

            # Note we can link between data nodes and rendered nodes using the node name
            # We have a map of each
            if @gui_theme.use_icons
                @rendered_nodes[data_node.name] = NodeIconWidget.new(
                                                node_x,
                                                node_y,
                                                data_node,
                                                get_node_color(data_node),
                                                scale_to_use,
                                                @is_explorer) 
            else
                @rendered_nodes[data_node.name] = NodeWidget.new(
                                                node_x,
                                                node_y,
                                                data_node,
                                                get_node_color(data_node),
                                                scale_to_use,
                                                @is_explorer)
            end
        end
    end
    @rendered_nodes.values.each do |rn|
        rn.base_z = @base_z
    end
end

#prevent_text_overlapObject



3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
# File 'lib/wads/widgets.rb', line 3087

def prevent_text_overlap 
    @rendered_nodes.values.each do |rn|
        text = rn.get_text_widget
        if text
            if overlaps_with_a_node(text)
                move_text_for_node(rn)
            else 
                move_in_bounds = false
                # We also check to see if the text is outside the edges of this widget
                if text.x < @x or text.right_edge > right_edge 
                    move_in_bounds = true 
                elsif text.y < @y or text.bottom_edge > bottom_edge 
                    move_in_bounds = true
                end
                if move_in_bounds 
                    debug("#{text.label} was out of bounds")
                    move_text_for_node(rn)
                end
            end
        end
    end
end

#renderObject



3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
# File 'lib/wads/widgets.rb', line 3352

def render 
    if @rendered_nodes
        @rendered_nodes.values.each do |vn|
            vn.draw 
        end 

        # Draw the connections between nodes 
        @visible_data_nodes.values.each do |data_node|
            data_node.outputs.each do |connected_data_node|
                if connected_data_node.is_a? Edge 
                    connected_data_node = connected_data_node.destination 
                end
                rendered_node = @rendered_nodes[data_node.name]
                connected_rendered_node = @rendered_nodes[connected_data_node.name]
                if connected_rendered_node.nil?
                    # Don't draw if it is not currently visible
                else
                    if @is_explorer and (rendered_node.is_background or connected_rendered_node.is_background)
                        # Use a dull gray color for the line
                        Gosu::draw_line rendered_node.center_x, rendered_node.center_y, COLOR_LIGHT_GRAY,
                            connected_rendered_node.center_x, connected_rendered_node.center_y, COLOR_LIGHT_GRAY,
                            relative_z_order(Z_ORDER_GRAPHIC_ELEMENTS)
                    else
                        Gosu::draw_line rendered_node.center_x, rendered_node.center_y, rendered_node.graphics_color,
                            connected_rendered_node.center_x, connected_rendered_node.center_y, connected_rendered_node.graphics_color,
                            relative_z_order(Z_ORDER_GRAPHIC_ELEMENTS)
                    end
                end
            end
        end 
    end
end

#scale_node_sizeObject



3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
# File 'lib/wads/widgets.rb', line 3067

def scale_node_size 
    range = @graph.get_number_of_connections_range
    # There are six colors. Any number of scale sizes
    # Lets try 4 first as a max size.
    bins = range.bin_max_values(4)  

    # Set the scale for each node
    @visible_data_nodes.values.each do |node|
        num_links = node.number_of_links
        index = 0
        while index < bins.size 
            if num_links <= bins[index]
                @rendered_nodes[node.name].set_scale(index + 1, @is_explorer)
                index = bins.size
            end 
            index = index + 1
        end
    end
end

#set_all_nodes_for_displayObject



3199
3200
3201
3202
3203
3204
3205
3206
3207
# File 'lib/wads/widgets.rb', line 3199

def set_all_nodes_for_display 
    @visible_data_nodes = @graph.node_map
    @rendered_nodes = {}
    populate_rendered_nodes
    if @size_by_connections
        scale_node_size
    end
    prevent_text_overlap 
end

#set_center_node(center_node, max_depth = -1)) ⇒ Object



3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
# File 'lib/wads/widgets.rb', line 3217

def set_center_node(center_node, max_depth = -1)
    # Determine the list of nodes to draw
    @graph.reset_visited 
    @visible_data_nodes = @graph.traverse_and_collect_nodes(center_node, max_depth)

    # Convert the data nodes to rendered nodes
    # Start by putting the center node in the center, then draw others around it
    @rendered_nodes = {}
    if @gui_theme.use_icons
        @rendered_nodes[center_node.name] = NodeIconWidget.new(
            center_x, center_y, center_node, get_node_color(center_node)) 
    else
        @rendered_nodes[center_node.name] = NodeWidget.new(center_x, center_y,
            center_node, get_node_color(center_node), get_node_color(center_node))
    end

    populate_rendered_nodes(center_node)

    if @size_by_connections
        scale_node_size
    end
    prevent_text_overlap 
end

#set_explorer_display(center_node = nil) ⇒ Object



3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
# File 'lib/wads/widgets.rb', line 3018

def set_explorer_display(center_node = nil)
    if center_node.nil? 
        # If not specified, pick a center node as the one with the most connections
        center_node = @graph.node_with_most_connections
    end

    @graph.reset_visited
    @visible_data_nodes = {}
    center_node.bfs(4) do |n|
        @visible_data_nodes[n.name] = n
    end

    @size_by_connections = false
    @is_explorer = true

    @rendered_nodes = {}
    populate_rendered_nodes

    prevent_text_overlap 
end

#set_tree_displayObject



3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
# File 'lib/wads/widgets.rb', line 3039

def set_tree_display
    @graph.reset_visited
    @visible_data_nodes = @graph.node_map
    @rendered_nodes = {}

    root_nodes = @graph.root_nodes
    number_of_root_nodes = root_nodes.size 
    width_for_each_root_tree = @width / number_of_root_nodes

    start_x = 0
    y_level = 20
    root_nodes.each do |root|
        set_tree_recursive(root, start_x, start_x + width_for_each_root_tree - 1, y_level)
        start_x = start_x + width_for_each_root_tree
        y_level = y_level + 40
    end

    @rendered_nodes.values.each do |rn|
        rn.base_z = @base_z
    end

    if @size_by_connections
        scale_node_size
    end

    prevent_text_overlap 
end

#set_tree_recursive(current_node, start_x, end_x, y_level) ⇒ Object



3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
# File 'lib/wads/widgets.rb', line 3161

def set_tree_recursive(current_node, start_x, end_x, y_level)
    # Draw the current node, and then recursively divide up
    # and call again for each of the children
    if current_node.visited 
        return 
    end 
    current_node.visited = true

    if @gui_theme.use_icons
        @rendered_nodes[current_node.name] = NodeIconWidget.new(
            x_pixel_to_screen(start_x + ((end_x - start_x) / 2)),
            y_pixel_to_screen(y_level),
            current_node,
            get_node_color(current_node))
    else
        @rendered_nodes[current_node.name] = NodeWidget.new(
            x_pixel_to_screen(start_x + ((end_x - start_x) / 2)),
            y_pixel_to_screen(y_level),
            current_node,
            get_node_color(current_node))
    end

    number_of_child_nodes = current_node.outputs.size 
    if number_of_child_nodes == 0
        return 
    end
    width_for_each_child_tree = (end_x - start_x) / number_of_child_nodes
    start_child_x = start_x + 5

    current_node.outputs.each do |child| 
        if child.is_a? Edge 
            child = child.destination 
        end
        set_tree_recursive(child, start_child_x, start_child_x + width_for_each_child_tree - 1, y_level + 40)
        start_child_x = start_child_x + width_for_each_child_tree
    end
end