diff --git a/src/marks/text.js b/src/marks/text.js index 7ea075bce2..17649bef30 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -13,6 +13,8 @@ export class Text extends Mark { title, fill, fillOpacity, + stroke, + strokeOpacity, textAnchor, fontFamily, fontSize, @@ -25,6 +27,8 @@ export class Text extends Mark { ...options } = {} ) { + const [vstroke, cstroke] = maybeColor(stroke, "none"); + const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity); const [vfill, cfill] = maybeColor(fill, "currentColor"); const [vfillOpacity, cfillOpacity] = maybeNumber(fillOpacity); const [vrotate, crotate] = maybeNumber(rotate, 0); @@ -39,11 +43,19 @@ export class Text extends Mark { {name: "text", value: text}, {name: "title", value: title, optional: true}, {name: "fill", value: vfill, scale: "color", optional: true}, - {name: "fillOpacity", value: vfillOpacity, scale: "opacity", optional: true} + {name: "fillOpacity", value: vfillOpacity, scale: "opacity", optional: true}, + {name: "stroke", value: vstroke, scale: "color", optional: true}, + {name: "strokeOpacity", value: vstrokeOpacity, scale: "opacity", optional: true} ], options ); - Style(this, {fill: cfill, fillOpacity: cfillOpacity, ...options}); + Style(this, { + fill: cfill, + fillOpacity: cfillOpacity, + stroke: cstroke, + strokeOpacity: cstrokeOpacity, + ...options + }); this.rotate = crotate; this.textAnchor = string(textAnchor); this.fontFamily = string(fontFamily); @@ -57,7 +69,7 @@ export class Text extends Mark { render( I, {x, y}, - {x: X, y: Y, rotate: R, text: T, title: L, fill: F, fillOpacity: FO, fontSize: FS}, + {x: X, y: Y, rotate: R, text: T, title: L, fill: F, fillOpacity: FO, fontSize: FS, stroke: S, strokeOpacity: SO}, {width, height, marginTop, marginRight, marginBottom, marginLeft} ) { const {rotate} = this; @@ -83,6 +95,8 @@ export class Text extends Mark { .call(applyAttr, "fill", F && (i => F[i])) .call(applyAttr, "fill-opacity", FO && (i => FO[i])) .call(applyAttr, "font-size", FS && (i => FS[i])) + .call(applyAttr, "stroke", S && (i => S[i])) + .call(applyAttr, "stroke-opacity", SO && (i => SO[i])) .text(i => T[i]) .call(title(L))) .node(); diff --git a/test/output/wordCloud.svg b/test/output/wordCloud.svg new file mode 100644 index 0000000000..06894624eb --- /dev/null +++ b/test/output/wordCloud.svg @@ -0,0 +1,3 @@ + + a (69)about (7)account (2)ago (2)air (2)all (23)almost (3)aloft (2)always (2)am (4)among (2)an (4)and (73)any (2)are (5)as (26)at (5)be (9)because (4)been (2)before (3)begin (2)being (4)besides (2)better (2)between (2)broiled (3)but (15)by (8)can (6)cannot (2)captain (2)care (2)chief (2)city (2)come (3)commodore (2)content (2)cook (3)could (2)country (2)crowds (2)deck (2)deep (2)did (5)distant (2)do (8)does (2)down (6)each (2)else (2)ever (5)every (4)exactly (2)fates (2)find (2)first (4)fixed (2)for (16)forecastle (2)from (11)get (6)glory (2)go (12)going (4)grand (3)great (3)grow (2)hand (3)have (8)having (2)he (10)head (4)healthy (2)here (5)high (4)hill (2)him (3)himself (2)his (10)how (3)however (2)hunks (2)i (43)if (9)image (3)in (48)into (9)is (34)ishmael (2)it (33)its (2)just (2)land (6)lead (2)leaders (2)leaves (2)let (2)like (6)little (4)long (2)look (2)magic (2)make (2)man (3)mast (2)may (3)me (25)meadow (2)mean (2)meaning (2)men (4)metaphysical (2)miles (3)money (4)more (6)most (5)motives (2)much (4)must (4)my (14)myself (3)never (5)no (6)not (11)nothing (3)now (5)ocean (2)of (81)off (3)officer (2)old (6)on (12)once (2)one (10)or (10)order (2)other (5)others (2)ourselves (2)out (3)over (2)own (2)paid (2)part (7)particular (2)parts (3)passenger (4)passengers (3)pay (2)paying (3)perhaps (2)phantom (2)plunged (2)point (2)previous (2)purse (3)requires (2)respectfully (2)reveries (2)right (3)robust (2)round (2)sail (2)sailor (5)same (5)say (3)schoolmaster (2)scores (2)sea (13)seas (2)see (6)set (3)shepherds (2)ship (3)ships (3)should (3)sight (2)sleep (2)so (4)some (11)something (3)sort (3)soul (3)spar (2)stand (5)still (3)stream (2)streets (2)strong (2)such (5)take (6)tell (4)than (4)that (31)the (124)their (4)them (5)themselves (2)then (5)there (16)these (4)they (12)thing (2)things (4)think (2)thinks (2)this (17)those (4)though (7)thousand (2)thousands (2)thump (2)time (6)to (53)two (4)under (2)unless (2)up (4)upon (9)voyage (6)warehouses (2)was (8)water (8)way (6)we (3)well (2)were (7)whale (3)whaling (5)what (9)when (5)whenever (5)where (2)which (4)who (5)why (7)wild (2)will (6)winds (3)with (13)without (3)world (4)would (4)yet (4)yonder (2)you (23)your (6) + \ No newline at end of file diff --git a/test/plots/index.js b/test/plots/index.js index e4d486a3dd..52bf730874 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -98,4 +98,5 @@ export {default as usPopulationStateAgeDots} from "./us-population-state-age-dot export {default as usPresidentialElection2020} from "./us-presidential-election-2020.js"; export {default as usRetailSales} from "./us-retail-sales.js"; export {default as usStatePopulationChange} from "./us-state-population-change.js"; +export {default as wordCloud} from "./word-cloud.js"; export {default as wordLengthMobyDick} from "./word-length-moby-dick.js"; diff --git a/test/plots/word-cloud.js b/test/plots/word-cloud.js new file mode 100644 index 0000000000..9ba9d3f0df --- /dev/null +++ b/test/plots/word-cloud.js @@ -0,0 +1,31 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const random = d3.randomLcg(32); + + // Compute a set of “words” from the text. As with any natural language task, + // this is messy and approximate. + const words = (await d3.text("data/moby-dick-chapter-1.txt")) + .replace(/’/g, "") // remove apostrophes + .split(/\b/g) // split at word boundaries + .map(word => word.replace(/[^a-z]+/ig, "")) // strip non-letters + .filter(word => word) // ignore non-letter words + .map(word => word.toLowerCase()); // normalize to lowercase + + return Plot.plot({ + inset: 20, + x: {axis: null}, + y: {axis: null}, + marks: [ + Plot.text(words, Plot.groupZ({ + text: d => d.length > 1 ? `${d[0]} (${d.length})` : "", + fontSize: d => 4 * Math.sqrt(d.length) + }, { + x: random, + y: random, + z: d => d + })) + ] + }); +}