Take the 2-minute tour ×
TeX - LaTeX Stack Exchange is a question and answer site for users of TeX, LaTeX, ConTeXt, and related typesetting systems. It's 100% free, no registration required.

I have seen the answer to this question:

http://tex.stackexchange.com/questions/917/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.

share|improve this question
    
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 '10 at 19:23
add comment

2 Answers

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 (a minute or so on my laptop).

Word cloud produced from the lipsum text

You need to update the l3kernel and l3experimental bundles to the latest version (end of February 2012): some of the functions I use (in particular sorting) are very recent --- and still unstable.

% Word cloud.
\RequirePackage{xparse,graphicx,l3regex,l3sort}
\ExplSyntaxOn
\prop_new:N \l_wordcloud_count_prop
\seq_new:N \l_wordcloud_words_seq
\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
\fp_set_eq:NN \l_wordcloud_angle_fp \c_pi_fp
\fp_mul:Nn \l_wordcloud_angle_fp { 2 }
\fp_const:Nn \c_wordcloud_two_pi_fp { \l_wordcloud_angle_fp }
\cs_generate_variant:Nn \prop_put:Nnn { Nno }
\tl_new:N \l_wordcloud_regex_tl
\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:n ##1 {#1} }        ,
    surround-fboxrule .dim_set:N = \l_wordcloud_surround_fboxrule_dim   ,
    surround-fboxset  .dim_set:N = \l_wordcloud_surround_fboxsep_dim    ,
    regex             .tl_set:N  = \l_wordcloud_regex_tl                ,
    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 }
    \tl_set:Nn \l_wordcloud_regex_tl { [[:alpha:]]+ }
    \keys_set:nn { wordcloud } {#1}
    \fp_mul:Nn \l_wordcloud_angle_increment_fp { \c_pi_fp }
    \fp_div:Nn \l_wordcloud_angle_increment_fp { 180 }
    \iow_term:x { Finding,~counting,~and~sorting~the~words }
    \wordcloud_find_words:n {#2}
    \wordcloud_tally_words:
    \wordcloud_sort_words_using_tally:
    \iow_term:x { Typesetting~the~words }
    \wordcloud_typeset_words:n { \l_wordcloud_word_number_int }
  }
\cs_new_protected:Npn \wordcloud_find_words:n #1
  {
    \exp_args:No \regex_extract_all:nnN
      \l_wordcloud_regex_tl
      {#1}
      \l_wordcloud_words_seq
  }
\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
      }
  }
\cs_new_protected:Npn \wordcloud_sort_words_using_tally:
  {
    % Remove duplicate words from the seq
    % (better done by extracting keys from the prop instead??)
    \seq_remove_duplicates:N \l_wordcloud_words_seq
    \seq_sort:Nn \l_wordcloud_words_seq
      {
        % Find word count for the two words "##1" and "##2".
        \prop_get:NnN \l_wordcloud_count_prop {##1} \l_wordcloud_tl
        \prop_get:NnN \l_wordcloud_count_prop {##2} \l_wordcloud_b_tl
        % 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 { \l_wordcloud_tl } < { \l_wordcloud_b_tl }
          { \sort_reversed: } { \sort_ordered: }
      }
  }
\int_new:N \l_wordcloud_int
\box_new:N \l_wordcloud_result_box
\cs_new_protected:Npn \wordcloud_typeset_words:n #1
  {
    \dim_gzero_new:N \g_wordcloud_xmin_dim
    \dim_gzero_new:N \g_wordcloud_xmax_dim
    \dim_gzero_new:N \g_wordcloud_ymin_dim
    \dim_gzero_new: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
            \iow_term:x { ##1 }
            \wordcloud_typeset_one_word:n {##1}
            \seq_put_right:Nn \l_wordcloud_typeset_words_seq {##1}
          }
      }
    % \iow_term:x
    %   {
    %     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
    %   }
    \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:n #1
  {
    \hbox_set:Nn \l_wordcloud_word_box
      {
        \group_begin:
          \wordcloud_word_style:n {#1}
        \group_end:
      }
    \prop_get:NnN \l_wordcloud_count_prop {#1} \l_wordcloud_tl
    \box_scale:Nnn \l_wordcloud_word_box
      { \l_wordcloud_tl } { \l_wordcloud_tl }
    % \iow_term:x
    %   {
    %     Word~'#1'~dimensions:~
    %     wd = \dim_eval:n { \box_wd:N \l_wordcloud_word_box } ,~
    %     ht+dp =
    %       \dim_eval:n
    %         {
    %           \box_ht:N \l_wordcloud_word_box
    %           + \box_dp:N \l_wordcloud_word_box
    %         }
    %   }
    \fp_set:Nn \l_wordcloud_min_radius_fp { 1e9 }
    \fp_zero:N \l_wordcloud_min_angle_fp
    \fp_zero:N \l_wordcloud_angle_fp
    \wordcloud_one_angle:
    \fp_cos:Nn \l_wordcloud_cos_fp { \l_wordcloud_min_angle_fp }
    \fp_mul:Nn \l_wordcloud_cos_fp { \l_wordcloud_min_radius_fp }
    \fp_sin:Nn \l_wordcloud_sin_fp { \l_wordcloud_min_angle_fp }
    \fp_mul:Nn \l_wordcloud_sin_fp { \l_wordcloud_min_radius_fp }
    \dim_zero_new:c { l_wordcloud_word_#1_xmin_dim }
    \dim_set:cn { l_wordcloud_word_#1_xmin_dim }
      {
        \fp_to_dim:N \l_wordcloud_cos_fp
        - .5 \box_wd:N \l_wordcloud_word_box
      }
    \dim_zero_new:c { l_wordcloud_word_#1_xmax_dim }
    \dim_set:cn { l_wordcloud_word_#1_xmax_dim }
      {
        \fp_to_dim:N \l_wordcloud_cos_fp
        + .5 \box_wd:N \l_wordcloud_word_box
      }
    \dim_zero_new:c { l_wordcloud_word_#1_ymin_dim }
    \dim_set:cn { l_wordcloud_word_#1_ymin_dim }
      {
        \fp_to_dim:N \l_wordcloud_sin_fp
        - .5 \box_ht:N \l_wordcloud_word_box
        - .5 \box_dp:N \l_wordcloud_word_box
      }
    \dim_zero_new:c { l_wordcloud_word_#1_ymax_dim }
    \dim_set:cn { l_wordcloud_word_#1_ymax_dim }
      {
        \fp_to_dim:N \l_wordcloud_sin_fp
        + .5 \box_ht:N \l_wordcloud_word_box
        + .5 \box_dp:N \l_wordcloud_word_box
      }
    \dim_gset_min:Nn \g_wordcloud_xmin_dim
      { \dim_use:c { l_wordcloud_word_#1_xmin_dim } }
    \dim_gset_max:Nn \g_wordcloud_xmax_dim
      { \dim_use:c { l_wordcloud_word_#1_xmax_dim } }
    \dim_gset_min:Nn \g_wordcloud_ymin_dim
      { \dim_use:c { l_wordcloud_word_#1_ymin_dim } }
    \dim_gset_max:Nn \g_wordcloud_ymax_dim
      { \dim_use:c { l_wordcloud_word_#1_ymax_dim } }
    % \iow_term:x
    %   {
    %     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 }
    %   }
    \box_move_up:nn
      {
        \use:c { l_wordcloud_word_#1_ymin_dim }
        + \box_dp:N \l_wordcloud_word_box
      }
      {
        \hbox_to_wd:nn { 0pt }
          {
            \skip_horizontal:c { l_wordcloud_word_#1_xmin_dim }
            \box_use_clear:N \l_wordcloud_word_box
            \tex_hss:D
          }
      }
  }
\cs_new_protected_nopar:Npn \wordcloud_one_angle:
  {
    \fp_cos:Nn \l_wordcloud_cos_fp { \l_wordcloud_angle_fp }
    \fp_sin:Nn \l_wordcloud_sin_fp { \l_wordcloud_angle_fp }
    \fp_zero:N \l_wordcloud_radius_fp
    \seq_map_inline:Nn \l_wordcloud_typeset_words_seq
      {
        \use:x
          {
            \wordcloud_get_intersection:nnnn
              {
                \dim_eval:n
                  {
                    \dim_use:c { l_wordcloud_word_##1_xmin_dim }
                    - .5 \box_wd:N \l_wordcloud_word_box
                  }
              }
              {
                \dim_eval:n
                  {
                    \dim_use:c { l_wordcloud_word_##1_xmax_dim }
                    + .5 \box_wd:N \l_wordcloud_word_box
                  }
              }
              {
                \dim_eval:n
                  {
                    \dim_use:c { l_wordcloud_word_##1_ymin_dim }
                    - .5 \box_ht:N \l_wordcloud_word_box
                    - .5 \box_dp:N \l_wordcloud_word_box
                  }
              }
              {
                \dim_eval:n
                  {
                    \dim_use:c { l_wordcloud_word_##1_ymax_dim }
                    + .5 \box_ht:N \l_wordcloud_word_box
                    + .5 \box_dp:N \l_wordcloud_word_box
                  }
              }
          }
      }
    \fp_compare:NNNT \l_wordcloud_radius_fp < \l_wordcloud_min_radius_fp
      {
        \fp_set_eq:NN \l_wordcloud_min_radius_fp \l_wordcloud_radius_fp
        \fp_set_eq:NN \l_wordcloud_min_angle_fp  \l_wordcloud_angle_fp
        % \iow_term:x
        %   {
        %     At~##1~degrees:~
        %     r = \fp_use:N \l_wordcloud_min_radius_fp ,~
        %     a = \fp_use:N \l_wordcloud_min_angle_fp
        %   }
      }
    \fp_add:Nn \l_wordcloud_angle_fp \l_wordcloud_angle_increment_fp
    \fp_compare:NNNT \l_wordcloud_angle_fp < \c_wordcloud_two_pi_fp
      { \wordcloud_one_angle: }
  }
\cs_new_protected:Npn \wordcloud_get_intersection:nnnn #1#2#3#4
  {
    \wordcloud_get_intersection_aux:NnNnn
      \l_wordcloud_cos_fp {#1}
      \l_wordcloud_sin_fp {#3} {#4}
    \wordcloud_get_intersection_aux:NnNnn
      \l_wordcloud_cos_fp {#2}
      \l_wordcloud_sin_fp {#3} {#4}
    \wordcloud_get_intersection_aux:NnNnn
      \l_wordcloud_sin_fp {#3}
      \l_wordcloud_cos_fp {#1} {#2}
    \wordcloud_get_intersection_aux:NnNnn
      \l_wordcloud_sin_fp {#4}
      \l_wordcloud_cos_fp {#1} {#2}
  }
\cs_new_protected:Npn \wordcloud_get_intersection_aux:NnNnn #1#2#3#4#5
  {
    \fp_set_from_dim:Nn \l_wordcloud_r_fp {#2}
    \fp_div:Nn \l_wordcloud_r_fp { #1 }
    \fp_set_eq:NN \l_wordcloud_b_fp \l_wordcloud_r_fp
    \fp_mul:Nn \l_wordcloud_b_fp { #3 }
    \fp_set_from_dim:Nn \l_wordcloud_bound_min_fp {#4}
    \fp_set_from_dim:Nn \l_wordcloud_bound_max_fp {#5}
    \fp_compare:NNNF \l_wordcloud_b_fp < \l_wordcloud_bound_min_fp
      {
        \fp_compare:NNNF \l_wordcloud_b_fp > \l_wordcloud_bound_max_fp
          {
            \fp_compare:NNNT \l_wordcloud_r_fp > \l_wordcloud_radius_fp
              { \fp_set_eq:NN \l_wordcloud_radius_fp \l_wordcloud_r_fp }
          }
      }
  }
\cs_new_protected:Npn \wordcloud_word_style:n #1 {#1}
\ExplSyntaxOff

\documentclass{article}
\begin{document}
\pagestyle{empty}
\wordcloud
  [
    number-of-words = 20 ,
    style = {\fboxsep=1pt\fboxrule=0pt\fbox{#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}
share|improve this answer
add comment

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.

share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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