34
\$\begingroup\$

Given an input sentence consisting of one or more words [a-z]+ and zero or more spaces , output an ASCII-art histogram (bar graph) of the letter distribution of the input sentence.

The histogram must be laid out horizontally, i.e. with the letter key along the bottom in alphabetical order from left to right, with a Y-axis labeled 1- and every 5 units. The Y-axis must be the smallest multiple of five that is at least as tall as the tallest bar, and must be right-aligned. The X-axis is labeled with the input letters, with no gaps between. For example, input a bb dd should have label abd and not ab d, skipping the c. The bars themselves can be made of any consistent ASCII character -- I'll be using X here in my examples.

test example

5-

   X
   X   X
1-XXXXXXXX
  aelmpstx

Since there are three e, two t, and one of almsx.

More examples:

the quick brown fox jumped over the lazy dogs

5-
      X         X
      X         X
     XX  X      X  X XX
1-XXXXXXXXXXXXXXXXXXXXXXXXXX
  abcdefghijklmnopqrstuvwxyz


now is the time for all good men to come to the aid of their country

10-
              X
              X
              X  X
      X       X  X
 5-   X       X  X
      X   X   X  X
      X  XX XXXX X
   XXXXX XXXXXXX X
 1-XXXXXXXXXXXXXXXXXX
   acdefghilmnorstuwy

a bb ccc dddddddddddd

15-


      X
      X
10-   X
      X
      X
      X
      X
 5-   X
      X
     XX
    XXX
 1-XXXX
   abcd

a bb ccccc

5-  X
    X
    X
   XX
1-XXX
  abc

I/O and Rules

  • Input can be taken in any reasonable format and by any convenient method. This also means you can take input in all-uppercase, if that makes more sense for your code.
  • Leading/trailing newlines or other whitespace are optional, provided that the characters line up appropriately.
  • Either a full program or a function are acceptable. If a function, you can return the output rather than printing it.
  • Output can be to the console, returned as a list of strings, returned as a single string, etc.
  • Standard loopholes are forbidden.
  • This is so all usual golfing rules apply, and the shortest code (in bytes) wins.
\$\endgroup\$
21
  • 3
    \$\begingroup\$ I think this would be a bar graph rather than a histogram, as it's categorical rather than numeric data, but I'm mostly being pedantic. \$\endgroup\$
    – Giuseppe
    Commented Apr 11, 2018 at 19:03
  • \$\begingroup\$ is the input guaranteed to be non-empty? \$\endgroup\$
    – dzaima
    Commented Apr 11, 2018 at 19:03
  • 2
    \$\begingroup\$ Just being a pendant, but this isn't a histogram, it's a bar chart. Still a nice challenge though! \$\endgroup\$ Commented Apr 11, 2018 at 19:36
  • 4
    \$\begingroup\$ A Tuftian approach would be to make the bars out of the characters represented and not have a separate label row. \$\endgroup\$ Commented Apr 11, 2018 at 21:43
  • 2
    \$\begingroup\$ The histogram character has to be consistent, but across cases or within each case? \$\endgroup\$
    – Adám
    Commented Apr 12, 2018 at 7:38

22 Answers 22

7
\$\begingroup\$

Stax, 37 bytes

ü╣úwóΓµ┐Wh0íJ▌Ñìs┤►ï╖öz<à↔/Ü@τ|:╢Ω$‼φ

Run and debug it

\$\endgroup\$
7
\$\begingroup\$

R, 239 230 bytes

K=table(el(strsplit(gsub(" ","",scan(,"")),"")));m=matrix(" ",L<-sum(K|1)+1,M<-(M=max(K))+-M%%5+1);m[2:L,M]=names(K);m[1,M-g]=paste0(g<-c(1,seq(5,M,5)),"-");m[1,]=format(m[1,],j="r");for(X in 2:L)m[X,M-1:K[X-1]]=0;write(m,1,L,,"")

Try it online!

table does the heavy lifting here, uniquifying the characters, sorting them, and returning their counts.

Everything else is just ensuring the offsets are right for printing, which is the "real" work of an ascii-art challenge.

Thanks to @dylnan for pointing out a bug.

Thanks to @rturnbull for the scan approach, dropping 2 bytes.

\$\endgroup\$
2
  • \$\begingroup\$ 237 bytes \$\endgroup\$
    – rturnbull
    Commented Apr 11, 2018 at 23:40
  • \$\begingroup\$ @rturnbull I managed to knock off a few more bytes after that, thanks! \$\endgroup\$
    – Giuseppe
    Commented Apr 11, 2018 at 23:54
6
\$\begingroup\$

Dyalog APL, 109 97 96 95 93 88 bytes

{⊖(⍉r),⍨⍕↑(⊂'')@{1@0~⍵∊1,5×⍵}⍳≢⍉↑r←↑r,⍨⊂' -','   - '⍴⍨5×⌈5÷⍨≢1↓⍉↑r←↓{⍺,∊⍕¨0×⍵}⌸⍵[⍋⍵]∩⎕A}

Try it online!

Requires ⎕IO←0

Way too many bytes saved thanks to Adám and Cows quack!

\$\endgroup\$
9
  • \$\begingroup\$ For the last bit, you can try ⍵[⍋⍵]~' ' (sorts and removes spaces before passing through ) \$\endgroup\$
    – user41805
    Commented Apr 11, 2018 at 19:27
  • \$\begingroup\$ 'X'/⍨≢∊⍕¨× \$\endgroup\$
    – Adám
    Commented Apr 11, 2018 at 19:31
  • \$\begingroup\$ and ⍵>0×⍵ \$\endgroup\$
    – user41805
    Commented Apr 11, 2018 at 19:32
  • \$\begingroup\$ Your TIO link has an unnecessary Header. \$\endgroup\$
    – Adám
    Commented Apr 11, 2018 at 19:32
  • \$\begingroup\$ 2⌷⍴≢⍉ twice \$\endgroup\$
    – Adám
    Commented Apr 11, 2018 at 19:33
6
\$\begingroup\$

05AB1E, 58 47 bytes

áê©S¢Z5‰`Ā+L5*1¸ì'-«ð5×ý#À¦Áí.Bís'X×ζ‚ζJR»,®3ú,

Try it online!

-11 bytes thanks to @Emigna

\$\endgroup\$
6
  • \$\begingroup\$ Maybe this could help? Don't have time to tie them together but maybe they can give some inspiration. \$\endgroup\$
    – Emigna
    Commented Apr 11, 2018 at 20:35
  • \$\begingroup\$ @Emigna I'll have a look, definitely different than mine :). \$\endgroup\$ Commented Apr 11, 2018 at 21:34
  • \$\begingroup\$ @Emigna 57 bytes after I stitched it... given I didn't try too hard to optimize. Try it online! \$\endgroup\$ Commented Apr 11, 2018 at 21:45
  • \$\begingroup\$ 47 bytes with some restructuring and optimization. \$\endgroup\$
    – Emigna
    Commented Apr 12, 2018 at 8:23
  • 1
    \$\begingroup\$ Your letters don't line up with the X's for certain inputs. tio.run/##AVUAqv8wNWFiMWX//… \$\endgroup\$
    – mbomb007
    Commented Apr 12, 2018 at 14:49
6
\$\begingroup\$

gnu sed -r, 516 490 278 249 + 1 bytes

s/$/:ABCDEFGHIJKLMNOPQRSTUVWXYZ /
:a
s/(.)(:.*\1)/\2\1/I
ta
s/[A-Z]+/ /g
h
z
:b
s/ \w/ /g
G
s/:/&I/g
/:II{5}+ *$/M!bb
s/[a-z]/X/g
G
s/:(I{5}+|I)\b/0\1-/g
s/:I*/  /g
s/ (\w)\1*/\1/g
s/$/; 10123456789I0/
:c
s/(.)I(.*\1(I?.))|;.*/\3\2/
/\nI/s/^/ /Mg
tc

Try it online!


I am sure this can be improved, but for now, this should be good considering it is made in sed, where you don't have native arithmetic or sorting. So I lied, this wasn't good enough, so I improved (rewrote) it by another 212 bytes, with a tip regarding the sorting algorithm from Cows quack, which gave me a idea to make the unary to decimal conversion shorter too.
Description of inner workings:

s/$/:ABCDEFGHIJKLMNOPQRSTUVWXYZ /
:a
s/(.)(:.*\1)/\2\1/I
ta
s/[A-Z]+/ /g
h
z

This sorts the input and separates the groups with spaces. This works by first appending an uppercase alphabet plus space separated by a colon to the end. Then it moves each character in front of the colon to a matching character behind the colon using a case-insensitive substitution in a loop. The uppercase letters are then replaced by spaces and the string is copied to the holding space.

:b
s/ \w/ /g
G
s/:/&I/g
/:II{5}+ *$/M!bb

This loop works by reducing each character group size by one, appending the sorted original line and incrementing unary counters after the colon that remained from the sorting. It loops until an empty line with a number of 5*n + 1 is reached (since the last line ultimately results in whitespace). The pattern space looks something like this after the loop:

:IIIIII           
:IIIII           
:IIII           
:III  e         
:II  ee     t    
:I a eee l m p s tt x   

Then the formatting follows:

s/[a-z]/X/g            # makes bars consistent
G                      # appends line that becomes x-axis
s/:(I{5}+|I)\b/0\1-/g  # moves zero in front of line 1 or 5-divisible
                       # lines for the decimal conversion and adds -
s/:I*/  /g             # removes numbers from other lines
s/ (\w)\1*/\1/g        # collapses groups of at least 1 into 1
                       # character, deleting the space before it
                       # so that only size-0-groups have spaces

And finally, the unary to decimal converter remains:

s/$/; 10123456789I0/
:c
s/(.)I(.*\1(I?.))|;.*/\3\2/
/\nI/s/^/ /Mg
tc

It basically appends a string where the knowledge of conversion is. You can interprete it as :space:->1 and 0->1->2->3->4->5->6->7->8->9->I0. The substitution expression s/(.)I(.*\1(I?.))|;.*/\3\2/ works similar to the sorting one, replacing the characters in front of I's [ (.)I ] by the character that is next to the one from the front of the I in the conversion string [ (.*\1(I?.)) ] and if there is no I left, it removes the appended string [ |;.* ]. The substitution [ /\nI/s/^/ /Mg ] adds padding if needed.

Thanks to Cows quack for reducing the size by 26 bytes and for the shorter sorting algorithm.

\$\endgroup\$
4
  • \$\begingroup\$ Welcome to PPCG, and nice first answer! \$\endgroup\$
    – user41805
    Commented Apr 12, 2018 at 4:55
  • \$\begingroup\$ You can use \w (matches word characters) in a number of places to save some bytes. Also :b ... tb can simply become s/\B\w/X/g. You can remove the line that follows it, s/:/:,/g, by modifying the earlier substitutions. You can look at goo.gl/JvD7Rs (shortened TIO link to sed program) to see what I mean. \$\endgroup\$
    – user41805
    Commented Apr 12, 2018 at 14:47
  • 1
    \$\begingroup\$ You can improve on the sorting algorithm, hint: try appending zyx...cba to the input. \$\endgroup\$
    – user41805
    Commented Apr 13, 2018 at 7:25
  • \$\begingroup\$ Brilliant unary to decimal converter! Yours is at least 30 bytes shorter than the one in Tips for golfing in sed \$\endgroup\$
    – user41805
    Commented Apr 14, 2018 at 9:11
3
\$\begingroup\$

Python 2, 192 bytes

s=input()
d={c:s.count(c)for c in s if' '<c}
h=-max(d.values())/5*-5
for y in range(h,-1,-1):print('%d-'%y*(y%5==2>>y)).rjust(len(`-h`))+''.join(('X '[y>v],k)[y<1]for k,v in sorted(d.items()))

Try it online!

Explanation

Line 2 computes the histogram values in a fairly straightforward way, discarding ' '.

Line 3 uses the trick of computing ceil(x/5) as -(-x/5): we round the maximal frequency up to the next multiple of 5 using the formula -x/5*-5. This is h.

Line 4 is a loop counting from h down to 0 inclusive, printing each row:

  • If y%5==2>>y we print a label. This is when y ∈ {1, 5, 10, 15, 20, …}

    (This formula could maybe be shorter. We just need something that's 1 or True for {1, 5, 10, …}, and 0 or False or even a negative integer for all other values of y.)

  • We right-justify the label (or empty space) into len(`-h`) spaces: this is a neat one-byte saving over len(`h`)+1!

  • Then, we print either X's and spaces for this row (if y ≥ 1) or the letters (if y = 0), running through key-value pairs in d in ascending order.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Nice tick creation with '%d-'%y*(y%5==2>>y). Do you mind if I use that in my answer? \$\endgroup\$
    – dylnan
    Commented Apr 12, 2018 at 14:55
  • \$\begingroup\$ -~-(y%5*~-y) works too but it's one byte longer unfortunately. \$\endgroup\$
    – dylnan
    Commented Apr 12, 2018 at 16:12
2
\$\begingroup\$

Pyth, 65 bytes

J.tm+ed*hd\Xr8S-Qd)=+J*]d%_tlJ5_.e+?q<k2%k5.F"{:{}d}-",klQ*dhlQbJ

Try it here

Explanation

J.tm+ed*hd\Xr8S-Qd)=+J*]d%_tlJ5_.e+?q<k2%k5.F"{:{}d}-",klQ*dhlQbJ
J.tm+ed*hd\Xr8S-Qd)
     Get the bars.
                   =+J*]d%_tlJ5
     Round up the height to the next number that's 1 mod 5.
                               _.e+?q<k2%k5.F"{:{}d}-",klQ*dhlQbJ
     Stick the axis labels on.
\$\endgroup\$
2
\$\begingroup\$

Charcoal, 62 bytes

≔E²⁷⟦⟧ηFθ⊞§η⌕βιι≔⊟ηθ≦LηP⭆β⎇§ηκιω↑↑ΦηιF÷⁺⁹⌈η⁵«≔∨×⁵ι¹ιJ±¹±ι←⮌⁺ι-

Try it online! Link is to verbose version of code. Explanation:

≔E²⁷⟦⟧η

Create a list of 27 lists.

Fθ⊞§η⌕βιι

Push each input character to the list corresponding to its position in the lowercase alphabet. Non-lowercase characters get pushed to the 27th list.

≔⊟ηθ

Discard the 27th element of the list.

≦Lη

Take the lengths of all the elements of the list.

P⭆β⎇§ηκιω

Print the lowercase letters corresponding to non-zero list elements.

↑↑Φηι

Print the non-zero list elements upwards. Since this is an array of integers, each integer prints as a (now vertical) line, each in a separate column.

F÷⁺⁹⌈η⁵«

Calculate the number of tick marks on the Y-axis and loop over them.

≔∨×⁵ι¹ι

Calculate the position of the next tick mark.

J±¹±ι

Jump to the next tickmark.

←⮌⁺ι-

Print the tickmark reversed and back-to-front, effectively right-aligning it.

\$\endgroup\$
2
\$\begingroup\$

Jelly, 48 bytes

What a mine-field to traverse!

J’⁶D;”-Ɗ%5^ỊƲ?€Uz⁶ZU
ḟ⁶ṢµĠ¬;⁶$L%5Ɗ¿€;"@Qz⁶Ç;"$ṚY

A full-program printing the result (as a monadic link it would return a list containing characters and integers from [0,9])

Try it online! Or see the test-suite

How?

J’⁶D;”-Ɗ%5^ỊƲ?€Uz⁶ZU - Link 1, get y-axis: list of columns (including x-axis & top-spaces)
J                    - range of length  [1,2,3,4,5,6,...,height+1] (+1 for x-axis)
 ’                   - decrement        [0,1,2,3,4,5,...] (line it up with the content)
             ?€      - if for €ach...
            Ʋ        - ...condition: last four links as a monad:
        %5           -   modulo by five
           Ị         -   insignificant? (1 for 0 and 1, else 0)
          ^          -   XOR (0 for 1 or multiples of 5 greater than 0, else 0)
  ⁶                  - ...then: literal space character
       Ɗ             - ...else: last three links as a monad:
   D                 -   decimal list of the number, e.g. 10 -> [1,0]
     ”-              -   literal '-' character
    ;                -   concatenate, e.g. [1,0,'-']
               U     - upend (reverse each)
                z⁶   - transpose with a filler of space characters
                  Z  - transpose
                   U - upend (i.e. Uz⁶ZU pads the left with spaces as needed)

ḟ⁶ṢµĠ¬;⁶$L%5Ɗ¿€;"@Qz⁶Ç;"$ṚY - Main link: list of characters
ḟ⁶                          - filter out space characters
  Ṣ                         - sort
   µ                        - start a new monadic chain, call that S
    Ġ                       - group indices of S by their values
     ¬                      - logical NOT (vectorises) (getting 0 for the X "characters")
             ¿€             - while for €ach...
            Ɗ               - ...condition: last three links as a monad:
         L                  -   length
          %5                -   modulo by five
        $                   - ...do: last two links as a monad:
      ;⁶                    -   concatenate a space character
                  Q         - deduplicate S (get the x-axis)
               ;"@          - zip with (") concatenation (;) with swapped arguments (@)
                   z⁶       - transpose a with filler of space characters
                        $   - last two links as a monad:
                     Ç      -   call last link (1) as a monad (get y-axis)
                      ;"    -   zip with concatenation (complete the layout)
                         Ṛ  - reverse (otherwise it'll be upside-down)
                          Y - join with newlines
                            - implicit print
\$\endgroup\$
2
\$\begingroup\$

APL (Dyalog Classic), 56 bytes

⊖h,⍨{∊⍺'-'/⍨~×5|⍵-⍵<2}⌸⍕⍪⍳≢h←(⊢↑⍨≢+5|1-≢)⍉(⊢,⌸⊣\)∊⍞∘∩¨⎕a

Try it online!

\$\endgroup\$
2
\$\begingroup\$

Ruby, 250 248 234 188 173 157 153 bytes

->s{a=s.scan(/\w/).sort|[]
m=-(c=a.map{|l|s.count l}).max/5*-5
m.downto(1).map{|i|(i%5<1||i<2?"#{i}-":'').rjust(m)+c.map{|l|l<i ?' ':?X}*''}<<' '*m+a*''}

Try it online!

Thanks to:

  • dylnan for -16 bytes with less strict padding
  • Lynn for -2 bytes by rounding up with -x/5*-5
  • Kirill L. for -2 bytes by getting unique array elements with |[]
\$\endgroup\$
0
2
\$\begingroup\$

Java (JDK), 295 bytes

s->{int d[]=new int[26],m=0;char a;for(int c:s.getBytes())m=c>32&&++d[c-=65]>m?(d[c]+4)/5*5:m;String r=m+"-",z=r.replaceAll("."," ");for(;m>0;r+="\n"+(--m%5>0&m!=1|m<1?z:z.format("%"+~-z.length()+"s-",m)))for(a=0;a<26;a++)r+=d[a]>0?m>d[a]?" ":"x":"";for(a=64;a++<90;)r+=d[a-65]>0?a:"";return r;}

Try it online!

Credits

\$\endgroup\$
4
  • \$\begingroup\$ @aoemica Correct. I fixed it. \$\endgroup\$ Commented Apr 12, 2018 at 7:40
  • 1
    \$\begingroup\$ It's not much, but you can save 2 bytes. --m%5==0 can be --m%5<1, because you also have the &m>0 check. And m<=d[a]?"x":" " can be m>d[a]?" ":"x". \$\endgroup\$ Commented Apr 12, 2018 at 8:23
  • \$\begingroup\$ @KevinCruijssen 2 bytes are 2 bytes! I don't think there is much to golf anymore, except for a different algorithm. \$\endgroup\$ Commented Apr 12, 2018 at 8:30
  • 1
    \$\begingroup\$ 1 more byte by changing (--m%5<1|m==1)&m>0 to --m%5<1|m==1&&m>0 \$\endgroup\$ Commented Apr 13, 2018 at 7:34
1
\$\begingroup\$

JavaScript (Node.js), 262 256 bytes

*Thanks to @Shaggy for reducing by 2 bytes

a=>[...a].map(x=>x>" "&&(d=c[x]=(c[x]||x)+"X")[m]?m=d.length-1:0,c={},m=i=0)&&Object.keys(c).sort().map(x=>[...c[x].padEnd(m)].map((l,j)=>A[m-j-1]+=l),A=[...Array(m+=6-m%5)].map(x=>(++i>=m||((D=m-i)%5&&m-i^1)?"":D+"-").padStart((m+" ").length)))&&A.join`
`

Try it online!

\$\endgroup\$
2
  • \$\begingroup\$ Couple of quick savings I can spot on my phone: 1. Take input as an array of individual characters, 2. Replace x!=" " with x>" ". \$\endgroup\$
    – Shaggy
    Commented Apr 11, 2018 at 20:20
  • \$\begingroup\$ 3. Replace m=0 with i=m=0 and map((x,i)=> with map(x=>. \$\endgroup\$
    – Shaggy
    Commented Apr 11, 2018 at 20:25
1
\$\begingroup\$

Python 2, 249 224 219 215 205 197 187 188 182 176 bytes

def f(s):S=sorted(set(s)^{' '});C=map(s.count,S);P=max(C)+4;return zip(*(zip(*[('%d-'%y*(y%5==2>>y)).rjust(P)for y in range(P,0,-1)])+[(n*'#').rjust(P)for n in C]))+[[' ']*P+S]

Try it online!

Returns a list of lists of characters representing lines.

  • Saved some bytes by including a lot of extra whitespace.
  • Had an unnecessary map(list,yticks) in there.
  • Changed space padding to save some bytes.
  • I thought I was sorting but I was not: +2 bytes. But I saved one independently at the same time. y==1 replaced by y<2.
  • -6 bytes thanks to Lynn by using '%d-'%y*(y%5==2>>y) instead of (`y`+'-')*(not y%5or y<2).

Slightly ungolfed:

def f(s):
	S=sorted(set(s)^{' '})  # sorted list of unique letters (without ' ')
	C=map(s.count,S)        # count of each unique letter in the input
	P=max(C)+4              # used for padding and getting highest y tick
	B=[(n*'#').rjust(P)for n in C]     # create bars
	yticks = [('%d-'%y*(y%5==2>>y)).rjust(P)for y in range(P,0,-1)]  # create y ticks at 1 and multiples of 5
	yticks = zip(*yticks)                      # need y ticks as columns before combining with bars
	return zip(*(yticks+B))+[[' ']*P+S]        # zip ticks+bars then add row of sorted unique letters.
\$\endgroup\$
1
\$\begingroup\$

C# (.NET Core), 344 340 338 + 18 bytes

Includes 18 bytes for using System.Linq;

Saved 6 bytes thanks to @KevinCruijssen.

n=>{var l=n.Where(c=>c!=32).GroupBy(c=>c).OrderBy(c=>c.Key).ToDictionary(k=>k.Key,c=>c.Count());int h=(l.Values.Max()/5+1)*5,o=(h+"").Length+1,m=l.Keys.Count+o,t=h+1,i=0,j;var a=new string[t];for(string p,q;i<t;a[i++]=q)for(q=(p=i>0&i%5<1|i==1?i+"-":"").PadLeft(j=o);j<m;){var c=l.ElementAt(j++-o).Key;q+=i<1?c:l[c]>=i?'X':' ';}return a;}

Try it online!

\$\endgroup\$
4
  • \$\begingroup\$ You have a space at j< m; that can be removed. And int i=0,j can be placed as ,i=0,j after the other ints for -4 bytes in total. You'll have to including the 18 bytes for the using System.Linq; however.. \$\endgroup\$ Commented Apr 13, 2018 at 8:30
  • \$\begingroup\$ @KevinCruijssen Thanks, I missed these. And I added the 18 bytes. \$\endgroup\$
    – Ian H.
    Commented Apr 13, 2018 at 8:46
  • \$\begingroup\$ +1 from me. Oh, and you can save 2 more bytes by changing for(;i<t;){string p=i>0&i%5<1|i==1?i+"-":"",q=p.PadLeft(o);for(j=o;j<m;){...}a[i++]=q;} to for(string p,q;i<t;)for(p=i>0&i%5<1|i==1?i+"-":"",q=p.PadLeft(j=o);j<m;a[i++]=q){...}. Try it online. \$\endgroup\$ Commented Apr 13, 2018 at 8:57
  • \$\begingroup\$ @KevinCruijssen Thats really clever, thanks! \$\endgroup\$
    – Ian H.
    Commented Apr 13, 2018 at 9:15
1
\$\begingroup\$

Bash + coreutils, 332 324 323 318 312 302 298 296 293 291 bytes

c()(cut -d\  -f$@)
p=printf
cd `mktemp -d`
grep -o [^\ ]<<<$@|sort|uniq -c|c 7->a
sort -k2<a>b
r=$[`c 1 <a|sort -n|tail -1`+5]
s=${#r}
t()($p \ ;((i++<s))&&t;i=)
for((;--r;));{
((r%5&&r>1))&&t||$p %${s}s- $r;IFS='
'
for l in `<b`;{ ((r<=`c 1 <<<$l`))&&$p X||$p \ ;}
echo
}
t
c 2 <b|tr -d \\n

Try it online!

Annotated:

c()(cut -d\  -f$@)
p=printf              # saving a few bytes

cd `mktemp -d`        # for temp files

grep -o [^\ ]<<<$@    # grabs all non-space characters
    |sort|uniq -c     # get character frequency
    |c 7->a           # slightly hacky way of stripping leading spaces;
                      #     uniq -c adds 6 spaces in front of each line

sort -k2<a>b          # store frequencies sorted alphabetically in b

r=$[`                 # r = highest frequency +5:
    c 1 <a            #     get frequencies
    |sort -n|tail -1  #     get maximum frequency
    `+5]              #     +4 so at least one multiple of 5 is
                      #     labeled, +1 because r gets pre-decremented

s=${#r}                    # s = length of r as a string
t()($p \ ;((i++<s))&&t;i=) # pad line with s+1 spaces

for((;--r;));{         # while (--r != 0)
    ((r%5&&r>1))&&     # if r isn't 1 or a multiple of 5
        t||            #     then indent line 
        $p %${s}s- $r; # otherwise print right-aligned "${r}-"
        IFS='
'                      # newline field separator
    for l in `<b`;{          # for all letters and their frequencies:
        ((r<=`c 1 <<<$l`))&& #     if frequency <= current height 
            $p X||           #         then print an X
            $p \ ;}          #     otherwise print a space
    echo
}
t # indent x-axis labels
c 2 <b|tr -d \\n # print alphabetically sorted characters

Thanks to @IanM_Matrix for saving 3 bytes.

\$\endgroup\$
1
  • \$\begingroup\$ cat b could be <b saving 3 characters \$\endgroup\$ Commented Apr 13, 2018 at 12:43
0
\$\begingroup\$

C, 201 bytes

char c[256],*p,d;main(int a,char **b){for(p=b[1];*p;p++)++c[*p|32]>d&*p>64?d++:0;for(a=(d+4)/5*5;a+1;a--){printf(!a||a%5&&a!=1?"    ":"%3i-",a);for(d=96;++d>0;c[d]?putchar(a?32|c[d]>=a:d):0);puts(p);}}

Input is taken from the command line (first argument). Uses exclamation marks instead of X's to further reduce code size. Counter on the left is always three characters long.

Tested with GCC and clang.

\$\endgroup\$
2
  • \$\begingroup\$ for(p=b[1];*p;p++) can most likely be for(p=b[1]-1;*++p;), main(int a,char **b) could probably be golfed to m(a,b)char**b;. \$\endgroup\$ Commented Apr 11, 2018 at 22:52
  • \$\begingroup\$ Since a!=1 will be boolean, a%5&&a!=1? should be equivalent to a%5&a!=1? or a%5&&~-a. \$\endgroup\$ Commented Apr 11, 2018 at 22:55
0
\$\begingroup\$

Excel VBA, 316 bytes

An Anonymous VBE immediate window function that takes input from cell [A1] and outputs to the VBE immediate window.

For i=1To 26:Cells(2,i)=Len(Replace([Upper(A1)],Chr(i+64),11))-[Len(A1)]:Next:m=-5*Int(-[Max(2:2)]/5):l=Len(m)+1:For i=-m To-1:?Right(Space(l) &IIf(i=-1Xor i Mod 5,"",-i &"-"),l);:For j=1To 26:?IIf(Cells(2,j),IIf(Cells(2, j) >= -i, "X", " "),"");:Next:?:Next:?Spc(l);:For i=1To 26:?IIf(Cells(2,i),Chr(i+96),"");:Next

Ungolfed Version

Public Sub bar_graph()
    For i = 1 To 26
        ''  gather the count of the letter into cells
        Cells(2, i) = Len(Replace([Upper(A1)], Chr(i + 64), 11)) - [Len(A1)]
    Next
    m = -5 * Int(-[Max(2:2)] / 5)   ''  get max bar height
    l = Len(m) + 1                  ''  length of `m` + 1
    For i = -m To -1
        ''  print either a label or free space (y-axis)
        Debug.Print Right(Space(l) & IIf((i = -1) Xor i Mod 5, "", -i & "-"), l);
        For j = 1 To 26
            ''  print 'X' or ' ' IFF the count of the letter is positive
            If Cells(2, j) Then Debug.Print IIf(Cells(2, j) >= -i, "X", " ");
        Next
        Debug.Print                 ''  print a newline
    Next
    Debug.Print Spc(l);             ''  print spaces
    For i = 1 To 26
        ''  print the letters that were used (x-axis)
        Debug.Print IIf(Cells(2, i), Chr(i + 96), "");
    Next
End Sub
\$\endgroup\$
0
\$\begingroup\$

Perl 5 -n, 198 168 bytes

s/[a-z]/$\<++${$&}?$\=${$&}:0/eg;$\++while$\%5;$;=1+length$\++;printf"%$;s".'%s'x26 .$/,$\%5&&$\-1?"":"$\-",map$$_>=$\?X:$$_&&$",a..z while--$\;say$"x$;,map$$_&&$_,a..z

Try it online!

\$\endgroup\$
0
\$\begingroup\$

Python 3, 177 bytes

lambda s:[[list(("%d-"%i*(i%5==2>>i)).rjust(len(q)))+["* "[s.count(c)<i]for c in q]for i in range(max(map(s.count,q))+4,0,-1)]+[[" "]*len(q)+q]for q in[sorted(set(s)-{' '})]][0]

Try it online!

This may not be the most byte-efficient approach in Python, but I really wanted to solve this with a "true one-liner" lambda.

Outputs a list of character lists. Abuses multiple leading newlines and spaces just like everybody else. It may actually be further reduced to 174 bytes if it is acceptable to wrap the result in another list, so that we could transfer the final [0] indexing to the footer.

\$\endgroup\$
0
\$\begingroup\$

JavaScript (ES8), 200 bytes

Takes input as an array of characters. Returns a string.

s=>(s.sort().join``.replace(/(\w)\1*/g,s=>a.push(s[0]+'X'.repeat(l=s.length,h=h<l?l:h)),h=a=[]),g=y=>y--?(y<2^y%5?'':y+'-').padStart(`${h}_`.length)+a.map(r=>r[y]||' ').join``+`
`+g(y):'')(h+=5-~-h%5)

Try it online!

Commented

s => (                    // s[] = input array of characters (e.g. ['a','b','a','c','a'])
  s.sort()                // sort it in lexicographical order (--> ['a','a','a','b','c'])
  .join``                 // join it (--> 'aaabc')
  .replace(/(\w)\1*/g,    // for each run s of consecutive identical letters (e.g. 'aaa'):
    s => a.push(          //   push in a[]:
      s[0] +              //     the letter, which will appear on the X-axis
      'X'.repeat(         //     followed by 'X' repeated L times
        L = s.length,     //     where L is the length of the run (--> 'aXXX')
        h = h < L ? L : h //     keep track of h = highest value of L
    )),                   //   initialization:
    h = a = []            //     h = a = empty array (h is coerced to 0)
  ),                      // end of replace() (--> a = ['aXXX','bX','cX'] and h = 3)
  g = y =>                // g = recursive function taking y
    y-- ?                 //   decrement y; if there's still a row to process:
      (                   //     build the label for the Y-axis:
        y < 2 ^ y % 5 ?   //       if y != 1 and (y mod 5 != 0 or y = 0):
          ''              //         use an empty label
        :                 //       else:
          y + '-'         //         use a mark
      ).padStart(         //     pad the label with leading spaces,
        `${h}_`.length    //     using the length of the highest possible value of y
      ) +                 //     (padStart() is defined in ECMAScript 2017, aka ES8)
      a.map(r => r[y]     //     append the row,
                 || ' ')  //     padded with spaces when needed
      .join`` + `\n` +    //     join it and append a linefeed
      g(y)                //     append the result of a recursive call
    :                     //   else:
      ''                  //     stop recursion
)(h += 5 - ~-h % 5)       // call g() with h adjusted to the next multiple of 5 + 1
\$\endgroup\$
0
\$\begingroup\$

PHP, 180 bytes

$c=count_chars($argn,1);unset($c[32]);for($s=max($c)+4;$s;){printf($s%5&&$s>1?$z='   ':'%2d-',$s--);foreach($c as$d)echo$d>$s?X:' ';echo"
";}foreach($c as$a=>$d)$z.=chr($a);echo$z;

Try it online!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.