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_held_down, #handle_key_press, #handle_key_up, #handle_right_mouse, #info, #intercept_widget_event, #left_edge, #move_recursive_absolute, #move_recursive_delta, #overlaps_with, #pad, #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.



2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
# File 'lib/wads/widgets.rb', line 2989

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.



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

def graph
  @graph
end

#is_explorerObject

Returns the value of attribute is_explorer.



2987
2988
2989
# File 'lib/wads/widgets.rb', line 2987

def is_explorer
  @is_explorer
end

#selected_nodeObject

Returns the value of attribute selected_node.



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

def selected_node
  @selected_node
end

#selected_node_x_offsetObject

Returns the value of attribute selected_node_x_offset.



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

def selected_node_x_offset
  @selected_node_x_offset
end

#selected_node_y_offsetObject

Returns the value of attribute selected_node_y_offset.



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

def selected_node_y_offset
  @selected_node_y_offset
end

#size_by_connectionsObject

Returns the value of attribute size_by_connections.



2986
2987
2988
# File 'lib/wads/widgets.rb', line 2986

def size_by_connections
  @size_by_connections
end

Instance Method Details

#get_node_color(node) ⇒ Object



3240
3241
3242
3243
3244
3245
3246
# File 'lib/wads/widgets.rb', line 3240

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



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

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



3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
# File 'lib/wads/widgets.rb', line 3035

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



3013
3014
3015
3016
3017
3018
# File 'lib/wads/widgets.rb', line 3013

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



3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
# File 'lib/wads/widgets.rb', line 3141

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



3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
# File 'lib/wads/widgets.rb', line 3179

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



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
3351
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
# File 'lib/wads/widgets.rb', line 3272

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



3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
# File 'lib/wads/widgets.rb', line 3118

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



3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
# File 'lib/wads/widgets.rb', line 3383

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



3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
# File 'lib/wads/widgets.rb', line 3098

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



3230
3231
3232
3233
3234
3235
3236
3237
3238
# File 'lib/wads/widgets.rb', line 3230

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



3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
# File 'lib/wads/widgets.rb', line 3248

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



3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
# File 'lib/wads/widgets.rb', line 3049

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



3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
# File 'lib/wads/widgets.rb', line 3070

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



3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
# File 'lib/wads/widgets.rb', line 3192

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