19

I have seen the answer to this question:

Wordle-like word clouds

So yes it can be done by generating an image and inserting it, but that's kind of cheating. I'd like to be able to generate a word cloud that is made of searchable text, rather than a .png file. Does anyone know if that is possible?

Edit I'm interested in any style, it doesn't have to be a complex cloud with words in different directions, it could be a "horizontal" cloud like some of those here.

1
  • What part are you stuck at? That WP style just involves finding the relative frequency of the words, and then typesetting all the words scaled according to their weight (and disabling hyphenation).
    – Aditya
    Sep 19, 2010 at 19:23

2 Answers 2

19

Edit: Changed to code to make it work with newer versions of expl3. Also added colors.

If you simply want the words in linear order, TH's simpler answer should work nicely. My code typesets the words from the most frequent to the least frequent, as close as possible to the center of the cloud, avoiding overlaps of course. This is rather slow. For instance, from the lipsum text I get (in a minute or so on my laptop)

Word cloud produced from the lipsum text

You should to update the l3kernel and l3experimental bundles to the latest version (may 2014 at the time of writing): some of the functions I use (in particular sorting) are still evolving.

% Word cloud.
% At the end of the day, the user command is \wordcloud.
% A typical usage would be
%
% \wordcloud[
%   number-of-words = 3,
%   style = \color{blue}{#1}]
%  {list of various tags or words, possibly repeated
%    to show significance}
%
% This makes a wordcloud where tags appear with a size
% proportional to the number of times they appear in the
% braced argument.  Only the 3 (or more generally
% |number-of-words|) words which appear the most often
% will be displayed.

\RequirePackage{xparse,graphicx,l3sort}
\ExplSyntaxOn
\DeclareExpandableDocumentCommand { \wordcloudMaxValue } { }
  { \int_use:N \l_wordcloud_max_count_int }
\int_new:N \l_wordcloud_max_count_int
\prop_new:N \l_wordcloud_count_prop
\seq_new:N \l_wordcloud_words_seq
\tl_new:N \l_wordcloud_word_tl
\tl_new:N \l_wordcloud_tl
\tl_new:N \l_wordcloud_b_tl
\int_new:N \l_wordcloud_word_number_int
\dim_new:N \l_wordcloud_fboxrule_dim
\dim_new:N \l_wordcloud_fboxsep_dim
\dim_new:N \l_wordcloud_surround_fboxrule_dim
\dim_new:N \l_wordcloud_surround_fboxsep_dim
\fp_new:N \l_wordcloud_angle_increment_fp
\fp_new:N \l_wordcloud_angle_fp
\dim_new:N \l_wordcloud_x_dim
\dim_new:N \l_wordcloud_y_dim
\fp_new:N \l_wordcloud_best_radius_fp
\fp_new:N \l_wordcloud_best_angle_fp
\dim_new:N \g_wordcloud_xmin_dim
\dim_new:N \g_wordcloud_xmax_dim
\dim_new:N \g_wordcloud_ymin_dim
\dim_new:N \g_wordcloud_ymax_dim

\cs_generate_variant:Nn \prop_put:Nnn { Nno }
\keys_define:nn { wordcloud }
  {
    number-of-words   .int_set:N = \l_wordcloud_word_number_int        ,
    style             .code:n    =
      { \cs_set_protected:Npn \wordcloud_word_style:nn ##1 ##2 {#1} }  ,
    surround-fboxrule .dim_set:N = \l_wordcloud_surround_fboxrule_dim  ,
    surround-fboxsep  .dim_set:N = \l_wordcloud_surround_fboxsep_dim   ,
    angle-step        .fp_set:N  = \l_wordcloud_angle_increment_fp     ,
  }
\DeclareDocumentCommand { \wordcloud } { O { } m }
  {
    \dim_set_eq:NN \l_wordcloud_fboxrule_dim \fboxrule
    \dim_set_eq:NN \l_wordcloud_fboxsep_dim  \fboxsep
    \dim_set_eq:NN \l_wordcloud_surround_fboxrule_dim \fboxrule
    \dim_set_eq:NN \l_wordcloud_surround_fboxsep_dim  \fboxsep
    \int_set_eq:NN \l_wordcloud_word_number_int \c_max_int
    \fp_set:Nn \l_wordcloud_angle_increment_fp { 10 }
    \keys_set:nn { wordcloud } {#1}
    \msg_info:nn { wordcloud } { listing-words }
    \wordcloud_find_words:n {#2}
    \wordcloud_tally_words:
    \wordcloud_sort_words_using_tally:
    \msg_info:nn { wordcloud } { typesetting-words }
    \wordcloud_typeset_words:n { \l_wordcloud_word_number_int }
  }
\cs_generate_variant:Nn \seq_set_split:Nnn { Nnx }
\cs_new_protected:Npn \wordcloud_find_words:n #1
  {
    \seq_clear:N \l_wordcloud_words_seq
    \tl_clear:N \l_wordcloud_word_tl
    \seq_set_split:Nnx \l_wordcloud_words_seq
      { ~ } { \tl_to_str:n {#1} }
    \seq_set_map:NNn \l_wordcloud_words_seq
      \l_wordcloud_words_seq
      { \tl_map_function:nN {##1} \__wordcloud_filter_letters:n }
    \seq_remove_all:Nn \l_wordcloud_words_seq { }
  }
\cs_new:Npn \__wordcloud_filter_letters:n #1
  {
    \int_compare:nTF { `a <= `#1 <= `z } {#1}
      { \int_compare:nT { `A <= `#1 <= `Z } {#1} }
  }
\cs_new_protected_nopar:Npn \wordcloud_tally_words:
  {
    % Count occurrences, keeping track of repetition number in a prop.
    \prop_clear:N \l_wordcloud_count_prop
    \seq_map_inline:Nn \l_wordcloud_words_seq
      {
        \prop_get:NnNTF \l_wordcloud_count_prop {##1} \l_wordcloud_tl
          {
            % If the word is not new, add 1 to its count.
            \tl_set:Nx \l_wordcloud_tl
              { \int_eval:n { \l_wordcloud_tl + 1 } }
          }
          {
            % Otherwise, initialize the count at 1.
            \tl_set:Nx \l_wordcloud_tl { 1 }
          }
        \prop_put:Nno \l_wordcloud_count_prop {##1} \l_wordcloud_tl
      }
    % Then make a seq with items of the form {word}{count}
    % It would be better to use a different seq.
    \cs_set:Npn \__wordcloud_tmp:w ##1 ##2 { { {##1} {##2} } }
    \seq_set_split:Nnx \l_wordcloud_words_seq { }
      { \prop_map_function:NN \l_wordcloud_count_prop \__wordcloud_tmp:w }
  }
\cs_new_protected:Npn \wordcloud_sort_words_using_tally:
  {
    \seq_sort:Nn \l_wordcloud_words_seq
      {
        % If the second word count is > to the first, then the two words
        % were in the wrong order (reversed), otherwise, the order is
        % kept (ordered).
        \int_compare:nNnTF { \use_ii:nn ##1 } < { \use_ii:nn ##2 }
          { \sort_return_swapped: } { \sort_return_same: }
      }
    % we also set the max_count, used in the public \wordcloudMaxValue
    \int_set:Nn \l_wordcloud_max_count_int
      {
        \exp_last_unbraced:Nf \use_ii:nn
          { \seq_item:Nn \l_wordcloud_words_seq { 1 } }
      }
  }
\int_new:N \l_wordcloud_int
\box_new:N \l_wordcloud_result_box
\cs_new_protected:Npn \wordcloud_typeset_words:n #1
  {
    \dim_gzero:N \g_wordcloud_xmin_dim
    \dim_gzero:N \g_wordcloud_xmax_dim
    \dim_gzero:N \g_wordcloud_ymin_dim
    \dim_gzero:N \g_wordcloud_ymax_dim
    \hbox_set:Nn \l_wordcloud_result_box
      {
        % \rlap{*}
        \seq_clear:N \l_wordcloud_boxes_seq
        \int_zero:N \l_wordcloud_int
        \seq_clear:N \l_wordcloud_typeset_words_seq
        \seq_map_inline:Nn \l_wordcloud_words_seq
          {
            \int_compare:nF { \l_wordcloud_int < #1 } { \seq_map_break: }
            \int_incr:N \l_wordcloud_int
            \msg_info:nnxx { wordcloud } { one-word }
              { \use_i:nn ##1 } { \use_ii:nn ##1 }
            \wordcloud_typeset_one_word:Nnn \l_wordcloud_word_box ##1
            \wordcloud_locate_one_word:Nnn \l_wordcloud_word_box ##1
            \wordcloud_place_one_word:Nnn \l_wordcloud_word_box ##1
            \seq_put_right:Nx \l_wordcloud_typeset_words_seq
              { \use_i:nn ##1 }
          }
      }
    \msg_info:nn { wordcloud } { result-dimensions }
    \box_set_wd:Nn \l_wordcloud_result_box { 0pt }
    \box_set_ht:Nn \l_wordcloud_result_box { 0pt }
    \box_set_dp:Nn \l_wordcloud_result_box { 0pt }
    \group_begin:
      \dim_set_eq:NN \fboxsep  \l_wordcloud_surround_fboxsep_dim
      \dim_set_eq:NN \fboxrule \l_wordcloud_surround_fboxrule_dim
      \fbox
        {
          \vbox:n
            {
              \skip_vertical:N \g_wordcloud_ymax_dim
              \hbox:n
                {
                  \skip_horizontal:n { - \g_wordcloud_xmin_dim }
                  \box_use:N \l_wordcloud_result_box
                  \skip_horizontal:N \g_wordcloud_xmax_dim
                }
              \skip_vertical:n { - \g_wordcloud_ymin_dim }
            }
        }
    \group_end:
  }
\box_new:N \l_wordcloud_word_box
\cs_new_protected:Npn \wordcloud_typeset_one_word:Nnn #1#2#3
  {
    \hbox_set:Nn #1
      {
        \color_group_begin: \color_ensure_current:
          \wordcloud_word_style:nn {#2} {#3}
        \color_group_end:
      }
    % make our box depth-less (bug in box_scale) before scaling it
    \hbox_set:Nn #1
      { \box_move_up:nn { \box_dp:N #1 } { \box_use_drop:N #1 } }
    \box_scale:Nnn #1 {#3} {#3}
    \msg_info:nnxxx { wordcloud } { word-dimensions }
      {#2} { \box_wd:N #1 } { \box_ht:N #1 + \box_dp:N #1 }
  }
\cs_new_protected:Npn \wordcloud_locate_one_word:Nnn #1#2#3
  {
    \fp_set:Nn \l_wordcloud_best_radius_fp { inf }
    \fp_zero:N \l_wordcloud_best_angle_fp
    \fp_zero:N \l_wordcloud_angle_fp
    \fp_while_do:nn { \l_wordcloud_angle_fp < 360 }
      {
        \wordcloud_one_angle:
        \fp_add:Nn \l_wordcloud_angle_fp
          { \l_wordcloud_angle_increment_fp }
      }
  }
\cs_new_protected:Npn \wordcloud_place_one_word:Nnn #1#2#3
  {
    \dim_set:Nn \l_wordcloud_x_dim
      {
        \fp_to_dim:n
          { \l_wordcloud_best_radius_fp * cosd (\l_wordcloud_best_angle_fp) }
      }
    \dim_set:Nn \l_wordcloud_y_dim
      {
        \fp_to_dim:n
          { \l_wordcloud_best_radius_fp * sind (\l_wordcloud_best_angle_fp) }
      }
    \dim_new:c { l_wordcloud_word_#2_xmin_dim }
    \dim_set:cn { l_wordcloud_word_#2_xmin_dim }
      { \l_wordcloud_x_dim - .5 \box_wd:N #1 }
    \dim_new:c { l_wordcloud_word_#2_xmax_dim }
    \dim_set:cn { l_wordcloud_word_#2_xmax_dim }
      { \l_wordcloud_x_dim + .5 \box_wd:N #1 }
    \dim_new:c { l_wordcloud_word_#2_ymin_dim }
    \dim_set:cn { l_wordcloud_word_#2_ymin_dim }
      { \l_wordcloud_y_dim - .5 \box_ht:N #1 - .5 \box_dp:N #1 }
    \dim_new:c { l_wordcloud_word_#2_ymax_dim }
    \dim_set:cn { l_wordcloud_word_#2_ymax_dim }
      { \l_wordcloud_y_dim + .5 \box_ht:N #1 + .5 \box_dp:N #1 }
    \__wordcloud_update_dim:nnn { xmin } { min } {#2}
    \__wordcloud_update_dim:nnn { xmax } { max } {#2}
    \__wordcloud_update_dim:nnn { ymin } { min } {#2}
    \__wordcloud_update_dim:nnn { ymax } { max } {#2}
    \msg_info:nnn { wordcloud } { word-position } {#2}
    \box_move_up:nn
      { \use:c { l_wordcloud_word_#2_ymin_dim } + \box_dp:N #1 }
      {
        \hbox_to_wd:nn { 0pt }
          {
            \skip_horizontal:c { l_wordcloud_word_#2_xmin_dim }
            \box_use_drop:N #1
            \tex_hss:D
          }
      }
  }
\cs_new_protected:Npn \__wordcloud_update_dim:nnn #1#2#3
  {
    \dim_gset:cn { g_wordcloud_#1_dim }
      {
        \use:c { dim_#2:nn }
          { \use:c { g_wordcloud_#1_dim } }
          { \use:c { l_wordcloud_word_#3_#1_dim } }
      }
  }
\cs_new_protected:Npn \wordcloud_one_angle:
  {
    \fp_set:Nn \l_wordcloud_cos_fp { cosd ( \l_wordcloud_angle_fp ) }
    \fp_set:Nn \l_wordcloud_sin_fp { sind ( \l_wordcloud_angle_fp ) }
    % Next, we set the radius_fp to the distance between the origin
    % and the intersection point (between the ray and the rectangles)
    % furthest from the origin.
    \fp_zero:N \l_wordcloud_radius_fp
    \seq_map_inline:Nn \l_wordcloud_typeset_words_seq
      {
        \wordcloud_get_radius:ccccN
          { l_wordcloud_word_##1_xmin_dim }
          { l_wordcloud_word_##1_xmax_dim }
          { l_wordcloud_word_##1_ymin_dim }
          { l_wordcloud_word_##1_ymax_dim }
          \l_wordcloud_word_box
      }
    % We now know that we could place a word at a distance radius_fp
    % and an angle angle_fp.  If that is closer than what was found so
    % far, used those values as the best_radius and best_angle.
    \fp_compare:nT
      { \l_wordcloud_radius_fp < \l_wordcloud_best_radius_fp }
      {
        \fp_set_eq:NN \l_wordcloud_best_radius_fp \l_wordcloud_radius_fp
        \fp_set_eq:NN \l_wordcloud_best_angle_fp  \l_wordcloud_angle_fp
      }
  }
\cs_new_protected:Npn \wordcloud_get_radius:NNNNN #1#2#3#4#5
  {
    \use:x
      {
        \wordcloud_get_radius:nnnn
          { \dim_eval:n { #1 - .5 \box_wd:N #5 } }
          { \dim_eval:n { #2 + .5 \box_wd:N #5 } }
          { \dim_eval:n { #3 - .5 \box_ht:N #5 - .5 \box_dp:N #5 } }
          { \dim_eval:n { #4 + .5 \box_ht:N #5 + .5 \box_dp:N #5 } }
      }
  }
\cs_generate_variant:Nn \wordcloud_get_radius:NNNNN { cccc }
\cs_new_protected:Npn \wordcloud_get_radius:nnnn #1#2#3#4
  {
    % Finds where a half-line starting at the origin with angle
    % \l_wordcloud_angle_fp intersects the rectangle with
    % xmin=#1, xmax=#2, ymin=#3, ymax=#4.  For each intersection
    % that is further than the current radius_fp, increase radius_fp.
    % The rectangle has four sides, so four calls to the auxiliary.
    \wordcloud_get_radius_aux:NnNnn
      \l_wordcloud_cos_fp {#1}
      \l_wordcloud_sin_fp {#3} {#4}
    \wordcloud_get_radius_aux:NnNnn
      \l_wordcloud_cos_fp {#2}
      \l_wordcloud_sin_fp {#3} {#4}
    \wordcloud_get_radius_aux:NnNnn
      \l_wordcloud_sin_fp {#3}
      \l_wordcloud_cos_fp {#1} {#2}
    \wordcloud_get_radius_aux:NnNnn
      \l_wordcloud_sin_fp {#4}
      \l_wordcloud_cos_fp {#1} {#2}
  }
\cs_new_protected:Npn \wordcloud_get_radius_aux:NnNnn #1#2#3#4#5
  {
    \fp_compare:nF { #1 = 0 } % omit cases where ray // side.
      {
        \fp_set:Nn \l_wordcloud_r_fp { \dim_to_fp:n {#2} / #1 }
        \dim_compare:nT
          { #4 <= \fp_to_dim:n { #3 * \l_wordcloud_r_fp } <= #5 }
          {
            \fp_set:Nn \l_wordcloud_radius_fp
              { max( \l_wordcloud_radius_fp , \l_wordcloud_r_fp ) }
          }
      }
  }
\cs_new_protected:Npn \wordcloud_word_style:nn #1#2 {#1}
\msg_new:nnn { wordcloud } { listing-words }
  { Finding,~counting,~and~sorting~the~words }
\msg_new:nnn { wordcloud } { typesetting-words }
  { Typesetting~the~words }
\msg_new:nnn { wordcloud } { one-word }
  { '#1' ~with~size~#2 }
\msg_new:nnn { wordcloud } { word-dimensions }
  { Dimensions~of~the~word~'#1': wd = #2 , ~ ht+dp = #3. }
\msg_new:nnn { wordcloud } { word-position }
  {
    Word~'#1'~at~
    x = \dim_use:c { l_wordcloud_word_#1_xmin_dim }
     .. \dim_use:c { l_wordcloud_word_#1_xmax_dim } , ~
    y = \dim_use:c { l_wordcloud_word_#1_ymin_dim }
     .. \dim_use:c { l_wordcloud_word_#1_ymax_dim }
  }
\msg_new:nnn { wordcloud } { result-dimensions }
  {
    Result~box~dimensions:~
    x = \dim_use:N \g_wordcloud_xmin_dim
     .. \dim_use:N \g_wordcloud_xmax_dim , ~
    y = \dim_use:N \g_wordcloud_ymin_dim
     .. \dim_use:N \g_wordcloud_ymax_dim
  }
\ExplSyntaxOff

\documentclass{article}
\usepackage{xcolor} % for \textcolor
\usepackage{xparse} % for \DeclareExpandableDocumentCommand
\ExplSyntaxOn
\DeclareExpandableDocumentCommand { \fpeval } { m } { \fp_eval:n {#1} }
\ExplSyntaxOff
\begin{document}
\pagestyle{empty}
\wordcloud
  [
    number-of-words = 30 ,
    style = {\fboxsep=1pt\fboxrule=0pt%
      \textcolor[rgb]{\fpeval{1-#2/\wordcloudMaxValue} .5 .5}{#1}} ,
    angle-step = 15
  ]
  {
    Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Ut purus
    elit, vest ibulum ut, placerat ac, adipiscing vitae,
    felis. Curabitur dictum gravida mauris. Nam arcu libero, nonummy
    eget, consectetuer id, vulputate a, magna. Donec ve hicula augue eu
    neque. Pellentesque habitant morbi tristique senectus et netus et
    malesuada fames ac turpis egestas. Mauris ut leo. Cras viverra metus
    rhoncus sem. Nulla et lectus vestibulum urna fringilla
    ultrices. Phasellus eu tellus sit amet tortor gravida
    placerat. Integer sapien est, iaculis in, pretium quis, viverra ac,
    nunc. Praesent eget sem vel leo ultrices bibendum. Aenean faucibus.
    Morbi dolor nulla, malesuada eu, pulvinar at, mollis ac,
    nulla. Curabitur auct or semper nulla. Donec varius orci eget
    risus. Duis nibh mi, congue eu, accumsan eleifend, sagittis quis,
    diam. Duis eget orci sit amet orci dignissim rutrum.
  }
\end{document}
3
  • If I ever get time, I might try to implement the algorithm described here. One day... May 29, 2014 at 0:04
  • Thanks you for this package in full latex. How to change the rule color around the cloud ? I would like to remove it or set it to white Sep 9, 2015 at 12:13
  • @bioinformatics I am a bit confused by my code now, and I should probably revise it at some point. Remove the command \fbox (it appears once) from the code. This is not a nice fix, ideally I should put an option to decide whether to include a box or not, but right now I don't have much time. Sep 12, 2015 at 3:09
11

Here's a first stab at it.

\usepackage{mathpazo}
\usepackage{etoolbox}
\newcount\wcmaxfreq
\newdimen\wcsmall
\newdimen\wcscale
\newcommand\wordcloud[3]{%
        \begingroup
                \wcsmall#1
                \wcmaxfreq0
                \def\wordlist{}%
                \def\temp{ }%
                \edef\temp{\noexpand\parsewords#3\temp\relax\temp}%
                \temp
                \wcscale \dimexpr (#2 - \wcsmall)/(\wcmaxfreq - 1)\relax
                \forlistloop\printword\wordlist
        \endgroup
}

\long\def\parsewords#1 {%
        \ifx\relax#1
        \else\ifx\par#1
        \else
                \ifcsdef{wc@#1}{%
                        \count255 \numexpr \csuse{wc@#1} + 1 \relax
                        \csedef{wc@#1}{\numexpr \csname wc@#1\endcsname + 1\relax}
                }{%
                        \count255 1
                        \csdef{wc@#1}{1}
                        \listadd\wordlist{#1}%
                }%
                \csedef{wc@#1}{\the\count255 }%
                \ifnum\count255>\wcmaxfreq
                        \wcmaxfreq\count255
                \fi
                \expandafter\expandafter\expandafter\parsewords
        \fi\fi
}

\def\printword#1{%
        \count255 \numexpr \csuse{wc@#1} - 1 \relax
        \dimen0 \dimexpr \the\count255 \wcscale + \wcsmall \relax
        \fontsize{\dimen0}{1.2\dimen0}\selectfont #1 % intentional space
}

And then you can use it like this.

\wordcloud{8pt}{36pt}{foo bar baz foo foo foo foo foo baz}

\wordcloud{8pt}{36pt}{Lorem ipsum dolor sit amet, consectetur
adipiscing elit.  Praesent laoreet sem a metus dignissim iaculis. Cum
sociis natoque penatibus et magnis dis parturient montes, nascetur
ridiculus mus.  Cras quis orci sit amet lectus fermentum mollis. Donec
sagittis diam id magna consectetur eget sagittis nulla sodales. Aenean
vel dapibus sem. Aliquam semper commodo elit vitae dictum. Donec vitae
adipiscing elit. Donec non turpis urna, dapibus commodo turpis.
Aliquam dapibus est nec ante pellentesque vitae sodales enim laoreet.
Mauris molestie dui porttitor lacus adipiscing vitae iaculis ipsum
pretium.  Suspendisse et nulla ante. Integer mauris neque, dictum quis
lacinia non, iaculis ut nulla. Nam rhoncus dignissim ipsum ut
adipiscing.  Aliquam dapibus viverra justo id pharetra. In nec mauris
nisl.}

It doesn't seem to work correctly with paragraphs, but I didn't look into it.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .