diff --git a/lib/Biodiverse/GUI/Legend.pm b/lib/Biodiverse/GUI/Legend.pm index 09c595c0d..75f2691d5 100644 --- a/lib/Biodiverse/GUI/Legend.pm +++ b/lib/Biodiverse/GUI/Legend.pm @@ -1160,6 +1160,13 @@ sub set_text_marks_for_labels { return; } +sub set_log_mode { + croak "boolean arg not passed" + if @_ < 2; + my ($self, $bool) = @_; + + return $self->{log_mode} = $bool ? 1 : 0; +} sub set_log_mode_on { my ($self) = @_; @@ -1225,7 +1232,7 @@ sub set_colour_mode_from_list_and_index { $self->{categorical}{labels} = $labels; foreach my $key (keys %$colours) { my $colour = $colours->{$key}; - $colours->{$key} => Gtk2::Gdk::Color->parse($colour); + $colours->{$key} = Gtk2::Gdk::Color->parse($colour); } $self->{categorical}{colours} = $colours; } diff --git a/lib/Biodiverse/GUI/Tabs/Spatial.pm b/lib/Biodiverse/GUI/Tabs/Spatial.pm index 2c4f4e3ff..0b1b0f92c 100644 --- a/lib/Biodiverse/GUI/Tabs/Spatial.pm +++ b/lib/Biodiverse/GUI/Tabs/Spatial.pm @@ -1,5 +1,5 @@ package Biodiverse::GUI::Tabs::Spatial; -use 5.010; +use 5.014; use strict; use warnings; @@ -12,7 +12,7 @@ use Carp; use Scalar::Util qw /blessed looks_like_number refaddr weaken/; use List::Util qw /max/; use Time::HiRes; -use Sort::Key::Natural qw /natsort/; +use Sort::Key::Natural qw /natsort natkeysort/; use Ref::Util qw /is_ref is_hashref is_arrayref/; use Biodiverse::GUI::GUIManager; @@ -491,22 +491,19 @@ sub init_dendrogram { $self->{dendrogram}->set_num_clusters (1); $self->{no_dendro_legend_for} = { - 'Turnover' => 1, + 'Turnover' => 1, 'Branches in nbr set 1' => 1, + 'Turnover' => 1, + 'Branches in nbr set 1' => 1, }; - $self->init_branch_colouring_combo; + $self->init_branch_colouring_menu; $self->init_dendrogram_legend; return 1; } -sub update_branch_colouring_combo { - my $self = shift; - $self->init_branch_colouring_combo (refresh => 1); -} - -sub init_branch_colouring_combo { +sub init_branch_colouring_menu { my $self = shift; my %args = @_; @@ -515,93 +512,210 @@ sub init_branch_colouring_combo { my $xml_page = $self->{xmlPage}; my $bottom_hbox = $xml_page->get_object('hbox_spatial_tab_bottom'); - - my $combo = $self->{branch_colouring_combobox}; - my $have_combo = !!$combo; - - if ($args{refresh} || !$combo) { - # clean up pre-existing - if ($have_combo) { - foreach my $widget ( - $combo, - @{$self->{branch_colouring_extra_widgets} // []} - ) { - $widget->destroy; + + my $menubar = $self->{branch_colouring_menu}; + my $have_menu = !!$menubar; + + return 1 if !($args{refresh} || !$menubar); + + # clean up pre-existing + if ($have_menu) { + $_->destroy + foreach @{$self->{branch_colouring_extra_widgets} // []}; + $menubar->destroy if $menubar; + } + + my $label = Gtk2::Label->new('Branch colouring: '); + + my $checkbox_show_legend + = $self->{xmlPage}->get_object('menuitem_spatial_tree_show_legend'); + $menubar = Gtk2::MenuBar->new; + my $menu = Gtk2::Menu->new; + my $menuitem = Gtk2::MenuItem->new_with_label('Branch colouring: '); + $menuitem->set_submenu ($menu); + $menubar->append($menuitem); + my $menu_action = sub { + my $args = shift; + my ($self, $listname, $output_ref) = @$args; + if ($checkbox_show_legend->get_active) { + $self->{dendrogram}->update_legend; # need dendrogram to pass on coords + $self->{dendrogram}->get_legend->show; + } + $self->{current_branch_colouring_source} = [$output_ref, $listname]; + my $output_name = $output_ref->get_name; + $label->set_markup ("$listname (source: $output_name)"); + }; + + # need to keep in synch with $self->{no_dendro_legend_for} + my $default_text + = $self->{output_ref}->get_spatial_conditions_count > 1 + ? 'Turnover' + : 'Branches in nbr set 1'; + $label->set_markup ($default_text); + my $default_text_sans_markup = $default_text =~ s/<.?i>//gr; + my $sel_group = []; + + my $menu_item_label = Gtk2::Label->new($default_text); + my $menu_item + = Gtk2::RadioMenuItem->new_with_label($sel_group, $default_text_sans_markup); + push @$sel_group, $menu_item; + $menu_item->set_use_underline(0); + # $menu_item->set_label($menu_item_label); + $menu->append($menu_item); + $menu_item->signal_connect_swapped( + activate => sub { + $self->{dendrogram}->get_legend->hide; + $self->{current_branch_colouring_source} = undef; + $label->set_markup ($default_text); + }, + ); + + $menu->append(Gtk2::SeparatorMenuItem->new); + $menu->append(Gtk2::MenuItem->new_with_label('Lists in this output:')); + + state $re_skip_list = qr/(^RECYCLED_SET$)|(SPATIAL_RESULTS|CANAPE>>)$/; + + my $output_ref = $self->{output_ref}; + + my $list_names + = $output_ref->get_hash_lists_across_elements; + foreach my $list_name (natsort @$list_names) { + next if $list_name =~ /$re_skip_list/; + + my $menu_item = Gtk2::RadioMenuItem->new($sel_group, $list_name); + push @$sel_group, $menu_item; # first one is default + $menu_item->set_use_underline(0); + $menu->append($menu_item); + $menu_item->signal_connect_swapped( + activate => $menu_action, [$self, $list_name, $output_ref], + ); + } + + $menu->append(Gtk2::SeparatorMenuItem->new); + $menu->append(Gtk2::MenuItem->new_with_label('Lists across project basedatas:')); + + # now add the lists from other spatial outputs in the project, + # organised by their parent basedatas + my $own_bd = $output_ref->get_basedata_ref; + my @project_basedatas + = @{$self->{project}->get_base_data_list}; + foreach my $bd (@project_basedatas) { + my @output_refs + = grep {$_ ne $output_ref} + $bd->get_spatial_output_refs; + next if !@output_refs; + + my $bd_name = $bd->get_name; + my $bd_submenu = Gtk2::Menu->new; + my $bd_submenu_item = Gtk2::MenuItem->new_with_label($bd_name); + $bd_submenu_item->set_use_underline(0); + my $item_count; + + foreach my $ref (natkeysort {$_->get_name} @output_refs) { + my @list_names + = natsort + grep {$_ !~ /$re_skip_list/} + $ref->get_hash_lists_across_elements; + next if !@list_names; + + $item_count++; + + my $output_name = $ref->get_name; + my $sp_submenu = Gtk2::Menu->new; + my $sp_submenu_item = Gtk2::MenuItem->new_with_label($output_name); + $sp_submenu_item->set_use_underline(0); + $sp_submenu_item->set_submenu($sp_submenu); + foreach my $list_name (@list_names) { + my $menu_item = Gtk2::RadioMenuItem->new($sel_group, $list_name); + push @$sel_group, $menu_item; + $menu_item->set_use_underline(0); + $menu_item->signal_connect_swapped( + activate => $menu_action, [$self, $list_name, $ref], + ); + $sp_submenu->append($menu_item); } + + $bd_submenu->append($sp_submenu_item); } - my $model = Gtk2::ListStore->new('Glib::String'); - $combo = Gtk2::ComboBox->new_with_model ($model); - $self->{branch_colouring_combobox} = $combo; - - my $label = Gtk2::Label->new('Branch colouring: '); - - my $renderer = Gtk2::CellRendererText->new(); - $combo->pack_start($renderer, 1); - $combo->add_attribute($renderer, markup => 0); - - # need to keep in synch with $self->{no_dendro_legend_for} - my $combo_text - = $self->{output_ref}->get_spatial_conditions_count > 1 - ? 'Turnover' - : 'Branches in nbr set 1'; - my $iter = $model->append(); - $model->set ( $iter, 0, $combo_text ); - $combo->set_active(0); - - my $list_names - = $self->{output_ref}->get_hash_lists_across_elements; - foreach my $list_name (natsort @$list_names) { - next if $list_name =~ /SPATIAL_RESULTS$/; - next if $list_name =~ /CANAPE>>$/; - next if $list_name eq 'RECYCLED_SET'; - - my $iter = $model->append(); - $model->set ( $iter, 0, $list_name ); + if ($item_count) { + $bd_submenu_item->set_submenu($bd_submenu); + $menu->append($bd_submenu_item); } - - my $separator = Gtk2::SeparatorToolItem->new; - $bottom_hbox->pack_start ($separator, 0, 0, 0); - $bottom_hbox->pack_start ($label, 0, 0, 0); - $bottom_hbox->pack_start ($combo, 0, 0, 0); - $separator->show; - $label->show; - $self->{branch_colouring_extra_widgets} - = [$separator, $label]; - - # add callback - $combo->signal_connect_swapped( - changed => sub { - my $self = shift; - my $key = $self->{branch_colouring_combobox}->get_active_text; - $self->{dendrogram}->get_legend->hide - if $self->{no_dendro_legend_for}{$key}; - }, - $self, - ); } - $combo->show; + my $separator = Gtk2::SeparatorToolItem->new; + foreach my $widget ($separator, $menubar, $label) { + $bottom_hbox->pack_start ($widget, 0, 0, 0); + } + $bottom_hbox->show_all; + $menu->set_sensitive(1); + + $menubar->set_has_tooltip(1); + $menubar->set_tooltip_text ($self->_get_branch_colouring_menu_tooltip); + $label->set_has_tooltip(1); + $label->set_tooltip_text ($self->_get_branch_colouring_label_tooltip); + + $self->{branch_colouring_menu} = $menubar; + $self->{branch_colouring_extra_widgets} + = [$separator, $label]; return 1; } +sub _get_branch_colouring_label_tooltip { + state $text = <<'EOT' +The current list and source used to colour the tree branches. +This can be changed using the 'Branch colouring' menu to the +immediate left of this label. +EOT + ; + return $text; +} + +sub _get_branch_colouring_menu_tooltip { + state $text = <<'EOT' +Select the list to visualise as colours on the tree +when hovering over the grid. + +The first (default) option shows the paths connecting +the labels in the neighbour sets used for the analysis. +When there is one such set all branches are coloured blue. +When there are two such sets blue denotes branches only +in the first set, red denotes those only in the second set, +and black denotes those in both. From these one can see +the turnover of branches between the groups (cells) in +each neighbour set. + +The next set of menu options are list indices in the spatial +output that belongs to this tab. The remainder are lists +across other spatial outputs in the project, organised by their +basedata objects. These are in the same order as in the +Outputs tab. Basedatas and outputs with no list indices are +not shown. + +If a branch is not in the list then it is highlighted +using a default colour (usually black). If the selected +output has no labels that are also on the tree then no +highlighting is done (all branches remain black). + +Right clicking on a group (cell) fixes the highlighting +in place, stopping changes to the branch colouring as +the mouse is hovered over other groups. This allows +the tree to be exported with the current colouring. + +EOT + ; + return $text; +} + sub init_dendrogram_legend { my $self = shift; my $legend = $self->{dendrogram}->get_legend; return if !$legend; - my $combo = $self->{branch_colouring_combobox}; - return if !$combo; - - my $selected_text = $combo->get_active_text; - if (!$self->{no_dendro_legend_for}{$selected_text}) { - $legend->show; - } - else { - $legend->hide; - } - + # we used to do more here + return; } sub init_grid { @@ -682,7 +796,7 @@ sub update_display_list_combos { my @methods = qw / update_lists_combo update_output_indices_combo - update_branch_colouring_combo + init_branch_colouring_menu /; $self->SUPER::update_display_list_combos ( @@ -1381,7 +1495,7 @@ sub on_run { #$self->setup_dendrogram; # completely refresh the dendrogram $self->update_dendrogram_combo; $self->on_selected_phylogeny_changed; # update the tree plot - $self->update_branch_colouring_combo; + $self->init_branch_colouring_menu (refresh => 1); } # make sure the grid is sensitive again @@ -1506,15 +1620,14 @@ my @dendro_highlight_branch_colours sub highlight_paths_on_dendrogram { my ($self, $hashrefs, $group) = @_; - if (my $combo = $self->{branch_colouring_combobox}) { - my $selected_text = $combo->get_active_text; - if (!$self->{no_dendro_legend_for}{$selected_text}) { - $self->colour_branches_on_dendrogram ( - list_name => $selected_text, - group => $group, - ); - return; - } + if (my $sources = $self->{current_branch_colouring_source}) { + my ($ref, $listname) = @$sources; + $self->colour_branches_on_dendrogram ( + list_name => $listname, + output_ref => $ref, + group => $group, + ); + return; } $self->{dendrogram}->get_legend->hide; @@ -1565,13 +1678,15 @@ sub colour_branches_on_dendrogram { my $self = shift; my %args = @_; - my $tree = $self->get_current_tree; + my $tree = $self->get_current_tree; return if !$tree; my $list_name = $args{list_name}; my $dendrogram = $self->{dendrogram}; - my $output_ref = $self->{output_ref}; + + my $output_ref = $args{output_ref}; + $list_name =~ s{\s+.+$}{}; my $legend = $dendrogram->get_legend; $legend->set_colour_mode_from_list_and_index ( @@ -1580,12 +1695,7 @@ sub colour_branches_on_dendrogram { ); my $log_check_box = $self->{xmlPage}->get_object('menuitem_spatial_tree_log_scale'); - if ($log_check_box->get_active) { - $legend->set_log_mode_on; - } - else { - $legend->set_log_mode_off; - } + $legend->set_log_mode($log_check_box->get_active); my $flip_check_box = $self->{xmlPage}->get_object('menuitem_spatial_tree_colour_stretch_flip_mode'); $legend->set_invert_colours ($flip_check_box->get_active); @@ -1596,7 +1706,7 @@ sub colour_branches_on_dendrogram { ); my $minmax - = $self->get_index_min_max_values_across_full_list ($list_name); + = $self->get_index_min_max_values_across_full_list ($list_name, $output_ref); my ($min, $max) = @$minmax; # should not need to pass this $legend->set_min_max ($min, $max); @@ -1878,12 +1988,12 @@ sub on_active_index_changed { # bad name - we want all values across all lists of name $listname across all elements sub get_index_min_max_values_across_full_list { - my ($self, $list_name) = @_; + my ($self, $list_name, $output_ref) = @_; - my $output_ref = $self->{output_ref}; + $output_ref //= $self->{output_ref}; use List::MoreUtils qw /minmax/; - my $stats = $self->{list_minmax_across_all_elements}{$list_name}; + my $stats = $self->{list_minmax_across_all_elements}{$output_ref}{$list_name}; return $stats if $stats; @@ -1902,7 +2012,8 @@ sub get_index_min_max_values_across_full_list { $stats = \@minmax; - $self->{list_minmax_across_all_elements}{$list_name} = $stats; # store it + # store it + $self->{list_minmax_across_all_elements}{$output_ref}{$list_name} = $stats; return $stats; } @@ -2325,12 +2436,15 @@ sub on_show_tree_legend_changed { my $check = $menu_item->get_active; - my $combo = $self->{branch_colouring_combobox}; - return if !$combo; + my $menu = $self->{branch_colouring_menu}; + return if !$menu; - # no legend for turnover - my $selected_text = $combo->get_active_text; - $check &&= !$self->{no_dendro_legend_for}{$selected_text}; + # no legend for turnover + my $aref = $self->{current_branch_colouring_source}; + if (!defined $aref) { + $check = 0; + } + $check &&= !$self->{no_dendro_legend_for}{$aref->[0] // ''}; if ($check) { $legend->show; diff --git a/lib/Biodiverse/GUI/Tabs/Tab.pm b/lib/Biodiverse/GUI/Tabs/Tab.pm index 4b7deb280..f9387f0ac 100644 --- a/lib/Biodiverse/GUI/Tabs/Tab.pm +++ b/lib/Biodiverse/GUI/Tabs/Tab.pm @@ -1139,6 +1139,7 @@ sub update_display_list_combos { my $methods = $args{methods} // []; foreach my $method (@$methods) { + next if !$self->can($method); $self->$method; }