Beefy Boxes and Bandwidth Generously Provided by pair Networks
Your skill will accomplish
what the force of many cannot
 
PerlMonks  

Coloring IRC logs nicely

by iblech (Friar)
on Aug 19, 2004 at 16:15 UTC ( [id://384347]=perlquestion: print w/replies, xml ) Need Help??

iblech has asked for the wisdom of the Perl Monks concerning the following question:

Hello,

I want to color IRC logs nicely.

Each nick should have a different color. That's not a problem, calc_color (see beneath) expects two arguments, a color_id and the total number of people. It evenly distributes colors over the whole color space, e.g. #000000 to #ffffff.

But: This method doesn't work if there are many different nicks -- For example: nick1 will get color #aaaaaa and nick2 #ababab. Do you see the problem? The differences between the colors get smaller and smaller.

So, I came up with the following idea: Colors can be reused if the persons who "own" that colors don't interfere. Example:

  • A joins, color: 0
  • B joins, color: 1
  • C joins, color: 2
  • D joins, color: 3
  • ...Time passes...
  • E joins, color: 0
  • F joins, color: 1

My code can do this, but: To build the interference graph (look at the source) the whole log has to be known.

Example:
# 0=>A, B=>1, C=>2, D=>3, E=>4, F=>5 # A joins $chat->tick(0, $stamp+0); # B joins $chat->tick(1, $stamp+1); # C joins $chat->tick(2, $stamp+2); # D joins $chat->tick(3, $stamp+3); # ...Time passes... # E joins $chat->tick(4, $stamp+10000+0); # F joins $chat->tick(4, $stamp+10000+1); # Get the colors: local $_; print "$_: " . join ", ", $chat->color($_) for 0..5;

I'd like a solution where the colors can get calculated before all other users are known:

# 0=>A, B=>1, C=>2, D=>3, E=>4, F=>5 # A joins $chat->tick(0, $stamp+0); print "A: " . join ", ", $chat->color(0); # B joins $chat->tick(1, $stamp+1); print "B: " . join ", ", $chat->color(1); # etc.

The problem is: The color calculating sub I use, calc_color, needs the total number of persons as an argument. But, at the point in time I'd like to call $chat->color there's no possibility to know the total number of persons in advance.

So, my question is: Is there a nice way To Do What I Want? Maybe there isn't a 100% correct algorithm, but a 99%? I.e. The algorithm is allowed to make "small mistakes".

The following code does work, but it needs full knowledge of all events in order to build the interference graph.

Thanks!

# See http://m19s28.vlinux.de/iblech/Chat.pm.html for a syntax-highlighted version.
package Heinz::Colors; use warnings; use strict; # FADE_AWAY specifies the time in seconds after a nick is considered a +way/left. use constant FADE_AWAY => 3600 * 2; use constant DEBUG => 0; sub new { bless [ {}, # current [], # interference graph [], # total number of people "met" [], # color_id ] => shift } # $chat->tick(3, 1092927374) marks person id 3 as present at unix-time # 1092927374. sub tick { my ($self, $id, $time) = @_; # First, we fade out persons who haven't said anything for FADE_AWAY + seconds. $self->fade($time); # Mark $id as involved in a conversation. # If $self->cur->{$id} was false before the next line, the person ju +st # joined. $self->cur->{$id} = $time; # keys %{ $self->cur } is now a list of "active people". # Now we update our interference graph $self->i. local $_; # Make sure we don't get ...is not a hashref... errors. $self->i->[$id] ||= {}; # If $self->i->[$a]->{$b} is true, persons $a and $b have "met" each + other, # i.e. they said sth. in the same time window. # For everbody who's online ATM... for (keys %{ $self->cur }) { # ...mark $_ and $id as persons who have "met" each other. $self->i->[$id]->{$_} = 1; $self->i->[$_] ->{$id} = 1; } } # $chat->fade(1092927374) fades all people away which haven't said any +thing for # FADE_AWAY seconds. sub fade { my ($self, $time) = @_; local $_; # $self->cur is a hashref. If $self->cur->{$id} has a true value, pe +rson $id # is currently involved in a conversation. # We delete everybody who hasn't said anything for FADE_AWAY seconds +. for(keys %{ $self->cur }) { delete $self->cur->{$_} if $time - $self->cur->{$_} >= FADE_AWAY; } # delete()ing keys of a hash which is iterated over is safe as long +as the # key which is deleted is the current key. } # Calculate the "total number" of people $id met. # Example (left-to-right: time): # # AAAAAAAAAAAAAAAAAAA # BBB # CCCC +--- No connection between A..E and F,G! # DDDDD | # EEEEEEE v # FFFFFFF # GGGGGGG # # "Total number" count of A,B,C,D,E: 5 # "Total number" count of F,G: 2 # # The interference graph for this example is: # # 4/5 E 0/2 1/2 # \ F---G # \ # 0/5 A---B 1/5 # / \ # / \ # 3/5 D C 2/5 # # The "total number" of $id is the number of nodes in the graph $id is + in. # The color_id/total_number pair is what we have to calculate (calc_co +lor needs # color_id and total_number as arguments). sub collapse { my ($self, $id) = @_; # %visited is a hash of nodes we visited. If $visited{$a} is true, w +e visited # node $a. That's necessary to break the recursion. my %visited; # $recurse is a coderef. It marks $iid, its first argument, as visit +ed and # visits all nodes $iid is connected to. my $recurse; $recurse = sub { my $iid = shift; $visited{$iid} = 1; foreach my $iiid (keys %{ $self->i->[$iid] || {} }) { # Break the recursion: unless $visited{$iiid} -- Don't visit nod +es we # already visited. $recurse->($iiid) unless $visited{$iiid}; } }; # Start. $recurse->($id); # The "total number" is the number of people we visited. my $count = keys %visited; # Now, assign color_ids. We have to sort keys %visited so we get the + same # color_ids each time we execute da script. my $i = 0; foreach my $iid (sort { $a <=> $b } keys %visited) { # $self->met: "Total number" $self->met->[$iid] = $count; # $self->col: color_id $self->col->[$iid] = $i++; } } # Return the color (C< ("#foreground", "#background") >) of person $id +. sub color { my ($self, $id) = @_; # If we haven't calculated the total number of people $id met, do th +at now. $self->collapse($id) unless $self->met->[$id]; # Fetch the values $self->collapse calculated. my ($me, $total) = ($self->col->[$id], $self->met->[$id] - 1); # And execute calc_color. return $self->calc_color($me, $total); } # calc_color copied from irclog2html.pl # (http://freshmeat.net/projects/irclog2html.pl/), Copyleft (C) 2000-2 +002 Jeff # Waugh, licensed under the Terms of the GNU General Public License, v +ersion 2 # or higher. # calc_color expects the total number of colors to assign ($_[2]) and +the color # id ($_[1]) and returns a HTML-("#foreground", "#background")-pair wi +th nice # contrast etc. # Take calc_color as a sub w/o errors. sub calc_color { my ($self, $i, $ncolors) = @_; $ncolors = 1 if $ncolors == 0; # No division /0. my $a = 0.95; # tune these for the starting and ending concentra +tions of R,G,B my $b = 0.5; my $rgb = [ [$a,$b,$b], [$b,$a,$b], [$b,$b,$a], [$a,$a,$b], [$a,$b,$ +a], [$b,$a,$a] ]; my $rgbmax = 125; # tune these two for the outmost ranges of colou +r depth my $rgbmin = 240; my $n = $i % @$rgb; my $m = $rgbmin + ($rgbmax - $rgbmin) * ($ncolors - $i) / $ncolors; my @c = map { $rgb->[$n][$_] * $m } 0 .. 2; my $g = $c[0] * .3 + $c[1] * .59 + $c[2] * .11; my $f = $g > 127 ? "#000000" : "#ffffff"; my $h = sprintf "#%02x%02x%02x", @c; ($f, $h); } sub cur : lvalue { $_[0]->[0] } sub i : lvalue { $_[0]->[1] } sub met : lvalue { $_[0]->[2] } sub col : lvalue { $_[0]->[3] } 1;

Replies are listed 'Best First'.
Re: Coloring IRC logs nicely
by davido (Cardinal) on Aug 19, 2004 at 16:36 UTC

    Precalculate 64 colors based on your color distance formula. As nicks show up, draw from the pool. As nicks /part, recycle that color. If 64 colors result in some colors being indistinguishable, use other attributes such as italics or underline. For this, you'll need a viewer that can handle these other attributes. If 64 isn't a big enough number, stay out of #cybersex in the future (just kidding).

    This sounds like a job for object orientation too, where individuals are object instances, and the underlying class handles the details of keeping track of allocating display attributes to the individuals, as well as the details of outputting the proper sequences to display individual's attributes.


    Dave

      • My first algorithm: Uses the minimum number of colors necessary (= maximum contrast), but slow.
      • Your and Jaaps idea: Always uses a fixed number of colors (=contrast stays the same all the time), but fast.

      I hoped for a mix between those two solutions, but I think that's not possible: To build the interference graph, one has to know all future nicks. Your solution trades "perfection" for speed.

      Maybe I can use some heuristics:

      I read the first 100 (or so) lines. Based on the number of different nicks seen, I select the size of the pool. If many people (more than colors available) /JOIN after I set the size of the pool, that's "bad luck".

      Thanks Jaad and davido, I think I'll use your solutions.

        I agree that the precomputed method is faster, but the reason I proposed it wasn't because of its speed advantage. What I heard in your original post is that you really have no good way of knowing ahead of time how many nicks you'll be following, and that the color distance formula really needed to know that. So the color distance formula really only works if you decide ahead of time how many nicks you'll be following. Of course you don't have to use all of the ones you precompute, but you just hold them like cards in a deck, and hand them out as needed.


        Dave

        There's no reason why the two methods are mutually exclusive. You can retain contrast for small numbers of nicks by working out the fixed colors and then sorting them in an array so that the 3 primary colours appear first, then the 3 secondary colors, then the 6 tertiary colors, etc.
        @colors = qw(ff0000 00ff00 0000ff ffff00 ff00ff 00ffff);
        This will ensure that the first 3 nicks get a good contrast between the colors, then the next 3 get the second-best level of contrast ad infinitum.
Re: Coloring IRC logs nicely
by Jaap (Curate) on Aug 19, 2004 at 16:22 UTC
    It's easy. Use only 16 (or n) colours. Then the last 16 (or n) nicks get these colours. If you find a new nick, remove the nick that hasn't replied the longest and give the new nick that color.

      That's a nice idea, but consider:

      • A joins, color: 0/16
      • B joins, color: 1/16
      • (End)

      Only 2 of possible 16 colors are used.

      Another problem: If there's an active conversation with 16 or more nicks (though unlikely), then the colors of the nicks change unexpectedly.

      The question is, are this two disadvantages unavoidable?

        A joins, color: 0/16
        B joins, color: 1/16
        (End)
        Only 2 of possible 16 colors are used.

        How is that a problem? There are only two people in the channel, so why whould you want to use more than two colours? People already in the channel when you start would be considered as people joining when they talk and have no colour assigned to them.

        Another problem: If there's an active conversation with 16 or more nicks (though unlikely), then the colors of the nicks change unexpectedly.

        Then use 32 colours instead of 16. And if 32 people are talking at the same time, I don't think colours are going to help you much. (That might even be true of 16.)

Re: Coloring IRC logs nicely
by iblech (Friar) on Aug 19, 2004 at 18:43 UTC

    For anyone who's interested, here's the Jaad/davido pool-solution in Perl:

    package Chat::Pool; # 16 should suffice. use constant POOL_SIZE => 16; sub new { bless [ # $self->pool: Arrayref of ["#fg","#bg"]-pairs. # Fixed size. [ $_[0]->precalc_colors(POOL_SIZE) ], # $self->col->[$id] contains the ["#fg","#bg"]-pair of a user. [], ] => shift } sub tick { my ($self, $id, $time) = @_; # If we haven't allocated a color for $id... unless(defined $self->col->[$id]) { # Take one from the pool (pop), assign in to $id, and unshift it. # All in one line :) unshift @{ $self->pool }, $self->col->[$id] = pop @{ $self->pool } +; } } # Return the allocated color for $_[1]. sub color {@{ $_[0]->col->[$_[1]] }} # Precalculate the pool. sub precalc_colors { my ($self, $num) = @_; local $_; return map {[ $self->calc_color($_, $num) ]} 0..$num-1; } # calc_color as in OP sub pool : lvalue { $_[0]->[0] } sub col : lvalue { $_[0]->[1] }

    This solution is really easy :)

    Update: If you don't want HTML-colors, but ANSI (e.g. for STDOUT), you can use:

    package Chat::Pool::ANSI; use base "Chat::Pool"; sub precalc_colors {( ["\033[31;1m"], ["\033[32;1m"], ["\033[33;1m"], ["\033[34;1m"], ["\033[35;1m"], ["\033[36;1m"], ["\033[37;1m"], ["\033[31m"], ["\033[32m"], ["\033[33m"], ["\033[34m"], ["\033[35m"], ["\033[36m"], )} # later... printf "%s%s%s\n", ($chat->color($id))[0], $text, "\033[0m";
Re: Coloring IRC logs nicely
by Tuppence (Pilgrim) on Aug 20, 2004 at 02:41 UTC

    With the amount of people who lurk on IRC, I would think that you would want a uniform color for join / part messages, and only colorise what people are actually saying.

    Have a set number of colors, then as you need a color (someone that doesn't have a color assigned says something) grab the color that has the longest time since it was last used.

    Rather than trying to computer generate a list of colors, picking the colors by hand might be a better way to make sure that all the colors are visually distinct.

      With the amount of people who lurk on IRC, I would think that you would want a uniform color for join / part messages, and only colorise what people are actually saying.

      Yes, that's exactly what I do.

      See http://m19s28.vlinux.de/iblech/iblechbot.png for a screenshot.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://384347]
Approved by calin
Front-paged by roju
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others pondering the Monastery: (5)
As of 2024-04-26 07:54 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found