Word clouds
Word clouds as SVG: a text's words, or your own counts, sized by how often they appear and laid out on a spiral without overlapping, some turned upright, in your colours; the same layout each time.
Version 1.0.0 · 2 functions · 2 crates
Functions
What use word_cloud.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use word_cloud_words, word_cloud_svg from word_cloud (Quillo 0.6).
word_cloud_words(text: str, limit: int, stop_words: bool) → listor an errorThe most frequent words of text, most first: [{ word, count }], lower case, common English words left out when stop_words.
word_cloud_svg(words: any, options: any) → stror an errorA word cloud as SVG markup: words is a text (counted, common English words left out) or a map of word to count ({ quillo: 12, fast: 5 }). options: width (600), height (400), max_words (80), min_size and max_size (font pixels, 12 and 64), colors (a list of #rrggbb), background ("" for none), font (a CSS font family), vertical (a share of words turned upright, 0.2). The layout is the same each time for the same words.
Get it
$ quillo install word_cloud # word_cloud.qwer into this folder$ quillo install word_cloud --example # and its example, word_cloud.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/word_cloud.qwer$ curl -O https://quillolang.org/plugins/files/word_cloud.qweThen, in your program, all of it or only what you call:
use word_cloud.qweruse word_cloud_words, word_cloud_svg from word_cloud // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
unicode-segmentation = "1"serde_json = { version = "1", features = ["preserve_order"] }Example
// The answers to a customer survey ("What do you like about us?") as a word cloud on the team's dashboard.//// quillo run word_cloud.qwe then open http://localhost:8080/cloud.svguse "word_cloud.qwer"let ANSWERS = "Fast delivery and friendly staff. The coffee is great, delivery was fast. Great coffee, great prices. Friendly staff, clean shop, great coffee! Delivery fast and the packaging was great. Fresh beans, fast delivery, fair prices. The staff are friendly and the coffee is fresh. Good coffee and fast service. Love the fresh beans and the friendly service."route GET /cloud.svg = respond(200, word_cloud_svg(ANSWERS, { width: 500, height: 300, background: "#fdfaf5" })?, { "content-type": "image/svg+xml" })route GET /words = word_cloud_words(ANSWERS, 6, true)?// Tags with counts from the database, in the brand's colours.route GET /tags.svg = respond(200, word_cloud_svg({ quillo: 40, rust: 25, web: 22, sqlite: 15, plugins: 12, routes: 9, views: 7, maps: 4, email: 3 }, { width: 400, height: 240, colors: ["#5b3fc4", "#e0533d"], vertical: 0.0 })?, { "content-type": "image/svg+xml" })$ quillo run word_cloud.qwe$ curl 'localhost:8080/words'$ curl 'localhost:8080/cloud.svg'$ curl 'localhost:8080/tags.svg'Source
//! Word clouds as SVG: the words of a text (or your own counts) sized by how often they appear, laid out on a spiral without overlapping, some turned upright, in your colours; for survey answers, reviews, tags and talks.//! version: 1.0.0//!//! [dependencies]//! unicode-segmentation = "1"//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Value};use std::collections::HashMap;use unicode_segmentation::UnicodeSegmentation;const STOP: &str = "a about above after again against all also am an and any are as at be because been before being below between both but by can could did do does doing down during each few for from further had has have having he her here hers him his how i if in into is it its just me more most my no nor not now of off on once only or other our out over own same she should so some such than that the their them then there these they this those through to too under until up very was we were what when where which while who whom why will with would you your yours really get got one";fn counts_of(words: &Value, stop_words: bool) -> Result<Vec<(String, f64)>, String> { let stop: std::collections::HashSet<&str> = STOP.split_whitespace().collect(); let mut v: Vec<(String, f64)> = match words { Value::String(text) => { let mut m: HashMap<String, (f64, usize, String)> = HashMap::new(); for (i, w) in text.unicode_words().enumerate() { let low = w.to_lowercase(); if w.chars().count() < 2 || low.chars().all(|c| c.is_numeric()) || (stop_words && stop.contains(low.as_str())) { continue } let e = m.entry(low).or_insert((0.0, i, w.to_string())); e.0 += 1.0; } let mut v: Vec<(String, f64, usize)> = m.into_iter().map(|(_, (c, i, w))| (w.to_lowercase(), c, i)).collect(); v.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap().then(a.2.cmp(&b.2))); v.into_iter().map(|(w, c, _)| (w, c)).collect() } Value::Object(o) => { let mut v: Vec<(String, f64)> = o.iter().filter_map(|(k, c)| c.as_f64().filter(|c| *c > 0.0).map(|c| (k.clone(), c))).collect(); v.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap().then(a.0.cmp(&b.0))); v } _ => return Err("words are a text or a map of word to count".into()), }; v.retain(|(w, _)| !w.trim().is_empty()); Ok(v)}// About how wide a word is set at `size` in a sans-serif face.fn text_width(word: &str, size: f64) -> f64 { word.chars().map(|c| { let k = if "iljtfr.,;:!'|".contains(c) { 0.34 } else if "mwMW".contains(c) { 0.88 } else if c.is_uppercase() { 0.70 } else if c as u32 > 0x2E80 { 1.0 } else { 0.58 }; k * size }).sum()}fn esc(s: &str) -> String { s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """) }/// The most frequent words of `text`, most first: `[{ word, count }]`, lower case, common English words left out when `stop_words`.pub fn word_cloud_words(text: &str, limit: i64, stop_words: bool) -> Result<Vec<Value>, String> { Ok(counts_of(&Value::String(text.to_string()), stop_words)?.into_iter().take(limit.max(0) as usize).map(|(w, c)| json!({ "word": w, "count": c as i64 })).collect())}/// A word cloud as SVG markup: `words` is a text (counted, common English words left out) or a map of word to count (`{ quillo: 12, fast: 5 }`)./// `options`: `width` (600), `height` (400), `max_words` (80), `min_size` and `max_size` (font pixels, 12 and 64), `colors` (a list of `#rrggbb`), `background` (`""` for none), `font` (a CSS font family), `vertical` (a share of words turned upright, 0.2). The layout is the same each time for the same words.pub fn word_cloud_svg(words: serde_json::Value, options: serde_json::Value) -> Result<String, String> { let n = |k: &str, d: f64| options.get(k).and_then(|v| v.as_f64()).unwrap_or(d); let (w, h) = (n("width", 600.0), n("height", 400.0)); let (min_s, max_s) = (n("min_size", 12.0), n("max_size", 64.0)); let vertical = n("vertical", 0.2).clamp(0.0, 1.0); let max_words = n("max_words", 80.0) as usize; let colors: Vec<String> = options.get("colors").and_then(|c| c.as_array()).map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect()).filter(|v: &Vec<String>| !v.is_empty()) .unwrap_or_else(|| ["#1f4e79", "#2e75b6", "#c55a11", "#548235", "#7030a0", "#bf9000"].iter().map(|s| s.to_string()).collect()); let font = options.get("font").and_then(|f| f.as_str()).unwrap_or("Verdana, 'DejaVu Sans', sans-serif").to_string(); let bg = options.get("background").and_then(|f| f.as_str()).unwrap_or("").to_string(); let list: Vec<(String, f64)> = counts_of(&words, true)?.into_iter().take(max_words).collect(); if list.is_empty() { return Err("there are no words to draw".into()) } let (lo, hi) = (list.iter().map(|x| x.1).fold(f64::MAX, f64::min), list.iter().map(|x| x.1).fold(0.0, f64::max)); let mut placed: Vec<[f64; 4]> = Vec::new(); let mut out = format!("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\" font-family=\"{}\">", esc(&font)); if !bg.is_empty() { out.push_str(&format!("<rect width=\"100%\" height=\"100%\" fill=\"{}\"/>", esc(&bg))) } for (i, (word, c)) in list.iter().enumerate() { let t = if hi > lo { (c - lo) / (hi - lo) } else { 1.0 }; let mut size = (min_s + (max_s - min_s) * t.sqrt()).round(); // Every few words, in a fixed pattern, one turned upright. let upright = i > 0 && vertical > 0.0 && ((i as f64 * 0.618034) % 1.0) < vertical; let mut spot = None; // A word that finds no room is tried smaller, down to the smallest size. while spot.is_none() { let (tw, th) = (text_width(word, size) + 2.0, size * 1.05); let (bw, bh) = if upright { (th, tw) } else { (tw, th) }; let mut a: f64 = 0.0; while a < 400.0 { let r = 2.2 * a; let (cx, cy) = (w / 2.0 + r * a.cos() * (w / h).sqrt(), h / 2.0 + r * a.sin() / (w / h).sqrt()); let b = [cx - bw / 2.0, cy - bh / 2.0, cx + bw / 2.0, cy + bh / 2.0]; if r > w.max(h) { break } if b[0] >= 0.0 && b[1] >= 0.0 && b[2] <= w && b[3] <= h && !placed.iter().any(|p| b[0] < p[2] && p[0] < b[2] && b[1] < p[3] && p[1] < b[3]) { spot = Some((cx, cy, b)); break; } a += 0.1; } if spot.is_none() { if size <= min_s { break } size = (size * 0.85).max(min_s).round() } } let Some((cx, cy, b)) = spot else { continue }; placed.push(b); let color = &colors[i % colors.len()]; let transform = if upright { format!(" transform=\"rotate(-90 {:.1} {:.1})\"", cx, cy) } else { String::new() }; out.push_str(&format!("<text x=\"{:.1}\" y=\"{:.1}\" font-size=\"{}\" fill=\"{}\" text-anchor=\"middle\" dominant-baseline=\"central\"{}>{}</text>", cx, cy, size, esc(color), transform, esc(word))); } out.push_str("</svg>"); Ok(out)}Related
sparkline.qwer · 1.0.0Tiny inline charts from a list of numbers: SVG line and bar sparklines for table cells and sentences, and the same in text (▁▃▆█).6 views · 8 downloadsSVG charts chart_svg.qwer · 1.0.0Line and bar charts as SVG from your data, with axes, a legend and labels, light or dark, for emails, PDFs and reports.6 views · 6 downloadsSVG to PNG svg_png.qwer · 1.0.0SVG drawn to PNG at the size you ask, text included with a built-in font: link previews, email images, icons and charts.7 views · 7 downloadsAudio waveform audio_wave.qwer · 1.0.0A WAV file's waveform for an audio player: the loudness of each slice from 0 to 1, or drawn as an SVG of bars.6 views · 7 downloadsColour blindness color_vision.qwer · 1.0.0Colours and pictures as people with colour blindness see them, at full or milder severity; chart palettes checked for colours that become alike; and text contrast (WCAG) as each of them sees it.8 views · 7 downloadsGradients gradient.qwer · 1.0.0Colour scales and gradients (viridis, magma, blues, red-blue and more, or your own stops) as hex lists, a colour per value, CSS or SVG.6 views · 6 downloadsThread 0 posts
On Word clouds: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.