IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Interfaces Graphiques Perl Discussion :

Tk::Chart : Limite de la zone graphique pour la construction des diagrammes !


Sujet :

Interfaces Graphiques Perl

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre confirmé
    Profil pro
    Développeur Full Stack
    Inscrit en
    Novembre 2007
    Messages
    101
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations professionnelles :
    Activité : Développeur Full Stack

    Informations forums :
    Inscription : Novembre 2007
    Messages : 101
    Par défaut Tk::Chart : Limite de la zone graphique pour la construction des diagrammes !
    Bonjour,

    J'ai créé un script pour représenter sous forme de barres des données chronologiques. Comme le nombre de données est assez élevé, une fois le diagramme construit, j'utilise l'ascenseur horizontal pour accéder à l'ensemble des données. J'étais plutôt content du résultat obtenu, sauf que, le nombre de données augmentant, un problème est apparu.

    Lorsque le nombre de données atteint une certaine limite, le diagramme s'affiche mais avec des trainées noires, qui disparaissent si j'utilise par exemple l'ascenseur horizontal, ou toute autre action provoquant un rafraîchissement de la fenêtre du graphique.



    Lorsque j'utilise l'ascenseur pour visualer les données les plus à droite du diagramme en barres, je réalise qu'elle ne sont pas toutes construites, comme si la zone graphique ne pouvait pas dépasser une certaine largeur limite, que du reste je n'arrive pas à déterminer.



    J'aimerai savoir si ce problème est connu, si on peut y remédier, à moins qu'il serait lié aux limites de mon PC pas très puissant :

    Carte mère : Asus A7V8X-X

    Processeur : Athlon xp 1800+ 1533 Mhz

    Mémoire vive : 512 Mo

    Carte vidéo : NVIDIA GeForce4 MX 440 AGP8X (64 Mo)

    Autre question :

    Comme on peut le voir sur le graphique, en abscisse, en dessous de chaque barre, on peut lire le numéro de la donnée, mais à partir de 100 (3 chiffres), cette information n'est plus visible.

    - Peut-on remédier à ce problème ?

    - Ou bien est-il possible d'ajouter des informations supplémentaires concernant la donnée, visible uniquement lors du passage du curseur de la souris sur la barre correspondante ?

    Quelques explications sur le fonctionnement du script :
    Pour faciliter la représentation des données dans un fichier texte, celles-ci sont écrites ligne par ligne entre balises, de la manière suivante :

    <dataN>d1|d2|...|dn</dataN>

    Une partie du script (ci-dessous) à donc pour but la récupération de ces données, de manière à construire un tableau "@data" conforme au module Tk::Chart.
    D'autre part, pour faciliter ce test, j'ai juste mis un menu "Fichier" pour sélectionner le fichier txt à représenter. Dans le zip joint, j'ai mis plusieurs fichiers avec un nombre croissant de données, 200, 400, 600, 800, 1000, 1200,1300, 1400, 1500, 1650, pour faciliter les tests de ceux qui se pencheront sur ce problème.
    Chez moi le problème se produit entre 1400 et 1500.

    Merci d'avance à ceux qui pourront m'aider.

    Krys006

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    #!/usr/bin/perl
    use strict;
    use utf8;
    use warnings;
    use Tk;
    use Tk::LabFrame;
    use Tk::Chart::Bars;
    use Tk::Chart::Mixed;
    use Tk::Pane;
    use Cwd;
     
    my $dir_get_open_file = getcwd();
    my $dim_data_ligne=2; # Nombre de données par ligne dans le fichier
     
    my $main = MainWindow->new( -title  => "Graphe_data --> $dir_get_open_file");
    $main->minsize( 700, 500 );
     
    my $menu_bar = $main->Menu( -type => "menubar", );
    $main->configure( -menu => $menu_bar, );
     
    # menu Fichier avec sous menu
    my $menu_fichier = $menu_bar->cascade( -label => 'Fichier', -tearoff => 0, );
    $menu_fichier->command( -label => 'Sélectionner un fichier', -command => \&lecture_fichier, );
     
    my %param_graph = (); # Hash contenant les paramètres récupérés en début de fichier, pour la construction du graphique 
     
    sub lecture_fichier {
        my $fichier_SQL = $main->getOpenFile( -initialdir => $dir_get_open_file, ) ;
     
        if ($fichier_SQL) {
            if(-e $fichier_SQL) {
                $fichier_SQL =~ s/.*\/([^\/]*\.txt)/$1/;    # On extrait uniquement le nom du fichier sans le chemin
                print "\nTraitement du fichier : ".$fichier_SQL."\n";        
                open(FILE,'<', $fichier_SQL);
                while (<FILE>) {
                    if(/<Param>/ ... /<\/Param>/) {
                        my $L=$_;
                        if($L =~ /(X_Label)=(.*)/) { $param_graph{$1} = $2; }            
                        if($L =~ /(Y_Label)=(.*)/) { $param_graph{$1} = $2; }                            
                        if($L =~ /(Legend)=\[(.*)\]/) { $param_graph{$1} = $2; }
                        if($L =~ /(Title)=(.*)/) { $param_graph{$1} = $2; }
                    }    
                }
                close(FILE);
                const_graphique($fichier_SQL, \%param_graph); 
            }
        }
    }
    MainLoop;
     
    sub const_graphique {
        my ( $fichier_SQL, $ref_params ) = @_;
     
        my %params = %$ref_params; # On déréférence pour récupérer le contenu de la légende
     
        my @legends = split ',',$params{'Legend'};    
        my $X_label = $params{'X_Label'};
        my $Y_label = $params{'Y_Label'};
        my $title = $params{'Title'};
     
        my $mwg = MainWindow->new(
          -title      => $fichier_SQL,
          -background => 'white',
        );
     
        my $pane = $mwg->Scrolled(
          'Pane',
          -scrollbars => 'osoe',
          -sticky     => 'nswe',
          -width      => 1270,  # Largeur de la fenêtre par défaut
          -height     => 500,   # Hauteur de la fenêtre par défaut  
        );
        $pane->Frame;
        $pane->pack(qw / -fill both -expand 1 /);
     
        my @types = ( 'bars', 'bars' );
        my $chart = $pane->Mixed(
          -title      => $title,
          -titleposition => 'left',
          -xlabel     => $X_label,
          -ylabel     => $Y_label,
          -overwrite  => 1,
          -typemixed  => \@types,
          -background => 'snow',
          -width      => 3500,
          -longticks  => 1,         # Affiche un quadrillage gris
        )->pack( qw / -side left / );
        $chart->enabled_gradientcolor();
     
        # Add a legend to the graph    
        $chart->set_legend(      
          -title       => 'Title legend',
          -data        => \@legends,
          -titlecolors => 'blue',
        );
     
        # Tableaux pour la récupération des données depuis le fichier texte    
        my @data_fichier=();
        my @data=();    
     
        $mwg->configure( -title => $fichier_SQL );
     
        # Transformation du fichier en tableau de tableaux
        # Chaque ligne est stockée dans un tableau @tab, puis on ajoute la référence de @tab à @data_fichier
        my $cpt_lignes = 0;  # Compteur de lignes de données       
        open(DATA,'<', $fichier_SQL);
        while (my $L=<DATA>) {
            if ($L=~ /^<data[0-9]+>(.*)<\/data[0-9]+>/) { # Les données sont entre 2 balises, séparées par des '|'             
            my @tab=split('\|',$1);     # On transforme la liste en tableau                
            push @data_fichier,\@tab;   # On ajoute la référence du tableau au tableau de tableaux
            $cpt_lignes++;              # On incrémente le compteur de lignes
            }
        }
        close(DATA);
        print $cpt_lignes." lignes de donnees\n";
     
        # Redimensionnement du graphique en fonction du nombre de données à représenter
        # => ASTUCE EMPIRIQUE
        my $width = 100+$cpt_lignes*23;
        #my $width = 100+$cpt_lignes*30;
        print "Largeur de la zone graphique calculee = ".$width."\n";
     
        $chart->configure( -width => $width );
     
        # Réorganisation des données dans le tableau @data pour utilisation du module Tk::Chart
        for (my $k=0; $k<$dim_data_ligne; $k++) {
            my @tab_ligne=();
            # On extrait la k-ieme donnee de chaque petit tableau
            foreach my $ref (@data_fichier) { # On parcourt les ref de tous les tableaux trouvés
                my @tab = @$ref;              # On déréférence
                push @tab_ligne,$tab[$k];     # On stocke la k-ieme valeur de chaque tableau dans le tableau ligne
            }
            push @data,\@tab_ligne;
        }
     
        # Add help identification
        $chart->set_balloon();
     
        # Call this method to avoid resizing.
        $chart->disabled_automatic_redraw;
     
        # Create the graph
        $chart->plot( \@data );
     
    }
    Images attachées Images attachées   
    Fichiers attachés Fichiers attachés

  2. #2
    Responsable Perl et Outils

    Avatar de djibril
    Homme Profil pro
    Inscrit en
    Avril 2004
    Messages
    19 822
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : Avril 2004
    Messages : 19 822
    Par défaut
    J'ai modifié ton programme en rajoutant :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    print "\nTraitement du fichier : " . basename($fichier_SQL) . "\n";
    à la place de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
                $fichier_SQL =~ s/.*\/([^\/]*\.txt)/$1/;    # On extrait uniquement le nom du fichier sans le chemin
                print "\nTraitement du fichier : ".$fichier_SQL."\n";
    sans oublier Sinon, j'ai reproduit le bug chez moi, également à 1422 valeurs.

  3. #3
    Membre confirmé
    Profil pro
    Développeur Full Stack
    Inscrit en
    Novembre 2007
    Messages
    101
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations professionnelles :
    Activité : Développeur Full Stack

    Informations forums :
    Inscription : Novembre 2007
    Messages : 101
    Par défaut
    Merci djibril pour ta réactivité !

    Par curiosité, ta machine est-elle beaucoup plus puissante que la mienne ?

  4. #4
    Responsable Perl et Outils

    Avatar de djibril
    Homme Profil pro
    Inscrit en
    Avril 2004
    Messages
    19 822
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : Avril 2004
    Messages : 19 822
    Par défaut
    Oui, je suis sous Vista avec 2Go RAM.
    Je me demande si ce n'est pas une limite de Tk.
    J'ai essayé de reproduire le bug en réalisant d'autres programmes et lorsque la largeur excède 40000 pixels ( ce qui est quand même trop), l'image est tronquée.
    Crée une image avec photoshop ou paint.net puis essaye de l'afficher dans une fenêtre Tk, tu verras qu'elle est tronquée au même niveau que le graph. donc ce n'est pas lié au module mais à une limitation Tk. En même temps, c'est assez normal.
    Tu est en train d'essayer d'afficher une image de façon nette quelque soit la taille de celle-ci. A un moment donnée, la taille du graph devient irraisonnable, et je pense que Tk limite cette taille afin d'éviter des erreurs de pixelisation ou autre. L'idée serait de faire du croping afin de n'afficher que la proportion de l'image à un moment donnée.
    Exemple : Pour une taille de la fenêtre de 500 pixels, supposons que le graph affiche de façon nette 100 données, tu pourrais n'afficher que 100 valeurs, puis via une boite de liste, donner la possibilité d'afficher le graph de 100 à 200, 200 à 300, etc.
    Via Tk::Chart, tu pourrais recharger le contenu du tableau et faire un redraw.
    C'est ce que font tous les logiciels ou sites web pour éviter la génération de graphiques ou images de très grandes dimensions.

    Si j'ai un peu de temps, j'essayerais de modifier ton prog

  5. #5
    Membre confirmé
    Profil pro
    Développeur Full Stack
    Inscrit en
    Novembre 2007
    Messages
    101
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations professionnelles :
    Activité : Développeur Full Stack

    Informations forums :
    Inscription : Novembre 2007
    Messages : 101
    Par défaut
    Je pense que tu apportes là une réponse à mon problème.
    Je me doutais bien qu'en augmentant le nombre de données, j'allais un jour où l'autre avoir des ennuis. Cependant j'espérais pouvoir limiter le nombre de données à une année entière. Tanpis !

    Je vois assez bien ce que tu proposes comme solution, et l'idée de progresser dans les données par palier à l'aide de "controls" devrait convenir, en regrettant quand même le déplacement continu à l'aide du slider horizontal .

    Enfin, juste pour ma culture informatique, est-ce que le terme "croping" est analogue au streaming pour l'écoute de la radio en ligne par exemple ?

    De toute façon, je pense continuer dans cette voie, puisque j'avais fini par choisir Tk pour faire de la représentation statistique de données très personnalisée, les autres logiciels libres que j'ai pu trouvés ne répondant pas à mes exigences.

    Si d'aventure tu trouvais une solution, je suis bien sûr preneur .

    De mon côté je vais creuser le sujet et proposer ma solution si c'est satisfaisant. Je pense que ça pourrait intéresser d'autres perliens !

    Ah, j'oubliais ! Et ma petite question concernant la possibilité d'afficher des infos sur la donnée représentée, mais visibles uniquement avec un survol de la souris ?


    Merci djibril

    Krys006

  6. #6
    Responsable Perl et Outils

    Avatar de djibril
    Homme Profil pro
    Inscrit en
    Avril 2004
    Messages
    19 822
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : Avril 2004
    Messages : 19 822
    Par défaut
    Bon, j'ai modifié ton programme afin d'adopter la solution que je t'ai suggérée. J'affiche des portion de 50 valeurs que tu peux modifier. Ainsi, j'ai enlevé la cadre scrollable.
    J'essaye que ça te convient :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    #!/usr/bin/perl
    use strict;
    use utf8;
    use warnings;
    use Tk;
    use Tk::LabFrame;
    use Tk::Chart::Bars;
    use Tk::Chart::Mixed;
    use Tk::Pane;
    use Cwd;
    use File::Basename;
    use Tk::BrowseEntry;
     
    my $dir_get_open_file = getcwd;
    my $dim_data_ligne    = 2;        # Nombre de données par ligne dans le fichier
     
    my $main = MainWindow->new( -title => "Graphe_data --> $dir_get_open_file" );
    $main->minsize( 700, 500 );
     
    my $menu_bar = $main->Menu( -type => "menubar", );
    $main->configure( -menu => $menu_bar, );
     
    # menu Fichier avec sous menu
    my $menu_fichier = $menu_bar->cascade( -label => 'Fichier', -tearoff => 0, );
    $menu_fichier->command( -label => 'Sélectionner un fichier', -command => \&lecture_fichier, );
     
    my %param_graph
      = ();  # Hash contenant les paramètres récupérés en début de fichier, pour la construction du graphique
     
    sub lecture_fichier {
      my $fichier_SQL = $main->getOpenFile( -initialdir => $dir_get_open_file, );
     
      if ($fichier_SQL) {
        if ( -e $fichier_SQL ) {
     
          #$fichier_SQL =~ s/.*\/([^\/]*\.txt)/$1/;    # On extrait uniquement le nom du fichier sans le chemin
          print "\nTraitement du fichier : " . basename($fichier_SQL) . "\n";
          open( FILE, '<', $fichier_SQL );
          while (<FILE>) {
            if ( /<Param>/ ... /<\/Param>/ ) {
              my $L = $_;
              if ( $L =~ /(X_Label)=(.*)/ )    { $param_graph{$1} = $2; }
              if ( $L =~ /(Y_Label)=(.*)/ )    { $param_graph{$1} = $2; }
              if ( $L =~ /(Legend)=\[(.*)\]/ ) { $param_graph{$1} = $2; }
              if ( $L =~ /(Title)=(.*)/ )      { $param_graph{$1} = $2; }
            }
          }
          close(FILE);
          const_graphique( $fichier_SQL, \%param_graph );
        }
      }
    }
    MainLoop;
     
    sub const_graphique {
      my ( $fichier_SQL, $ref_params ) = @_;
     
      my %params = %$ref_params;    # On déréférence pour récupérer le contenu de la légende
     
      my @legends = split ',', $params{'Legend'};
      my $X_label = $params{'X_Label'};
      my $Y_label = $params{'Y_Label'};
      my $title   = $params{'Title'};
     
      my $mwg = MainWindow->new(
        -title      => $fichier_SQL,
        -background => 'white',
      );
     
      my $croplist = $mwg->BrowseEntry(
        -label      => 'Choisir portion : ',
        -background => 'white',
      )->pack();
     
      my @types = ( 'bars', 'bars' );
      my $chart = $mwg->Mixed(
        -title         => $title,
        -titleposition => 'left',
        -xlabel        => $X_label,
        -ylabel        => $Y_label,
        -overwrite     => 1,
        -typemixed     => \@types,
        -background    => 'snow',
        -longticks     => 1,          # Affiche un quadrillage gris
      )->pack(qw / -expand 1 -fill both /);
      $chart->enabled_gradientcolor();
     
      # Add a legend to the graph
      $chart->set_legend(
        -title       => 'Title legend',
        -data        => \@legends,
        -titlecolors => 'blue',
      );
     
      # Tableaux pour la récupération des données depuis le fichier texte
      my @data_fichier = ();
      my @data         = ();
     
      $mwg->configure( -title => $fichier_SQL );
     
      # Transformation du fichier en tableau de tableaux
      # Chaque ligne est stockée dans un tableau @tab, puis on ajoute la référence de @tab à @data_fichier
      my $cpt_lignes = 0;    # Compteur de lignes de données
      open( DATA, '<', $fichier_SQL );
      while ( my $L = <DATA> ) {
        if ( $L =~ /^<data[0-9]+>(.*)<\/data[0-9]+>/ )
        {                    # Les données sont entre 2 balises, séparées par des '|'
          my @tab = split( '\|', $1 );    # On transforme la liste en tableau
          push @data_fichier, \@tab;      # On ajoute la référence du tableau au tableau de tableaux
          $cpt_lignes++;                  # On incrémente le compteur de lignes
        }
      }
      close(DATA);
      print $cpt_lignes. " lignes de donnees\n";
     
      # Réorganisation des données dans le tableau @data pour utilisation du module Tk::Chart
      for ( my $k = 0; $k < $dim_data_ligne; $k++ ) {
        my @tab_ligne = ();
     
        # On extrait la k-ieme donnee de chaque petit tableau
        foreach my $ref (@data_fichier) {    # On parcourt les ref de tous les tableaux trouvés
          my @tab = @$ref;                   # On déréférence
          push @tab_ligne, $tab[$k];         # On stocke la k-ieme valeur de chaque tableau dans le tableau ligne
        }
        push @data, \@tab_ligne;
      }
     
      # Add help identification
      $chart->set_balloon();
     
      # Call this method to avoid resizing.
      $chart->enabled_automatic_redraw;
     
      my $nombre_valeurs = scalar @{ $data[0] };
      my $interval       = "0-$nombre_valeurs";
     
      # Portion de 100
      my @choices;
      my $debut   = 0;
      my $portion = 50;
      for ( my $i = $debut; $i <= $nombre_valeurs; $i += $portion ) {
        next if ( $i == 0 );
        push @choices, "$debut-$i";
        $debut = $i;
      }
      if ( $debut < $nombre_valeurs ) {
        push @choices, "$debut-$nombre_valeurs";
      }
     
      $croplist->configure(
        -choices   => \@choices,
        -variable  => \$interval,
        -browsecmd => sub {
          my ( $debut, $fin ) = split /-/, $interval;
          my ( @new_data, @new_x, @new_value ) = ();
     
          # Regénération du graph
          for ( $debut .. $fin - 1 ) {
            push @new_x,     $data[0][$_];
            push @new_value, $data[1][$_];
          }
          push @new_data, \@new_x;
          push @new_data, \@new_value;
          $chart->plot( \@new_data );
          $chart->redraw;
        }
      );
     
      # Create the graph
      $chart->plot( \@data );
     
    }
    cropping signifie recadrage. En d'autre terme, c'est récupérer une portion de notre image. ça n'a rien avoir avec streaming. Dans notre cas, nous affichons une portion de notre graphique, donc j'ai utilisé le terme cropping par abus de langage.
    En ce qui concerne ta question, les trois petits points sont présents automatiquement lorsque le texte dépasse la largeur de la barre. Cela évite le chevauchement de texte. Par contre, tu ne peux pas modifier le contenu de la bulle. C'est une aide apporté par le module pour savoir où tu te situe, c'est tout. S'il fallait gérer toutes les barres, cela deviendrait ingérable.

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Editeur graphique pour la gestion des graphes
    Par nasro21 dans le forum Débuter
    Réponses: 2
    Dernier message: 11/05/2014, 13h19
  2. [Administration] Mode graphique pour la gestion des droits d'accès en SVN
    Par amalamal dans le forum Subversion
    Réponses: 2
    Dernier message: 07/07/2008, 15h27
  3. un outil graphique pour la presentation des pages HTML
    Par hichem_enis dans le forum Struts 1
    Réponses: 2
    Dernier message: 08/03/2008, 10h45
  4. Un calendrier graphique pour la saisie des dates
    Par kam81 dans le forum AWT/Swing
    Réponses: 1
    Dernier message: 29/12/2007, 11h30

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo