1 2const addYear = (date) => d3.utcParse("%Y")( "" + (date.getUTCFullYear() + 1) ) 3 4export default class Bubbles { 5 constructor({data}){ 6 7 const dataMap = d3.rollup(data, v => v[0], d => d.time, d => d.geo); 8 9 const xDomain = d3.extent(data, d => d.GDP) 10 const yDomain = d3.extent(data, d => d.LEX) 11 const sDomain = d3.extent(data, d => d.POP) 12 const tDomain = d3.extent([...dataMap.keys()]); 13 this.yearMax = tDomain[1].getUTCFullYear(); 14 this.yearMin = tDomain[0].getUTCFullYear(); 15 16 this.time = tDomain[0]; 17 this.dragging = false; 18 this.duration = 500; 19 20 this.chart = new Chart({dataMap, xDomain, yDomain, sDomain, parent: this}) 21 this.slider = new TimeSlider({domain: tDomain, parent: this}) 22 this.morphText = new MorphText({domain: tDomain}); 23 24 25 this.checkbox = d3.select(".ve-ts-play input").on("change", () => { 26 this.setPlaying(!this.playing); 27 }); 28 29 this.button = d3.select(".ve-hero button.ve-cta") 30 .on("mouseover", () => this.chart.updateSize({top: 0, left: 0, right: 0, bottom: -0.5})) 31 .on("mouseout", () => this.chart.updateSize()) 32 33 this.chart.updateSize(); 34 this.slider.updateSize(); 35 this.chart.updateTime({time: this.time, duration: 0}) 36 this.morphText.updateTime( {time: this.time} ); 37 this.setPlaying(true); 38 39 window.onresize = () => { 40 this.chart.updateSize(); 41 this.slider.updateSize(); 42 this.chart.updateTime({time: this.time, duration: 0}) 43 }; 44 } 45 startPlaying(){ 46 const year = this.time.getUTCFullYear(); 47 this.setTime(d3.utcParse("%Y")( (year + 1 > this.yearMax) ? ""+this.yearMin : ""+(year + 1) )); 48 49 this.interval = setInterval(() => { 50 const year = this.time.getUTCFullYear(); 51 this.setTime(d3.utcParse("%Y")( (year + 1 > this.yearMax) ? ""+this.yearMin : ""+(year + 1) )); 52 }, this.duration) 53 54 } 55 setPlaying(arg){ 56 this.playing = arg; 57 this.checkbox.property("checked", arg); 58 if (arg) this.startPlaying(); 59 if (!arg) this.stopPlaying(); 60 } 61 setDragging(arg){ 62 this.dragging = arg; 63 } 64 stopPlaying(){ 65 clearInterval(this.interval); 66 } 67 setTime(time){ 68 this.time = time; 69 if(this.chart) this.chart.updateTime({time: time, duration: this.duration}); 70 if(this.slider) this.slider.updateTime({time: time, duration: this.duration}); 71 if(this.morphText) this.morphText.updateTime({time: time, duration: this.duration}); 72 } 73} 74 75 76class MorphText { 77 constructor({ domain }) { 78 splitting.default({ 79 whitespace: true 80 }); 81 this.charSelection = d3.select(".splitting").selectAll(".char"); 82 this.charIndexScale = d3.scaleTime(domain, [0, this.charSelection.size() - 1]); 83 } 84 85 updateTime({time, duration}) { 86 const charIndex = Math.round(this.charIndexScale(time)); 87 const selected = [charIndex - 1, charIndex, charIndex + 1]; 88 const sibling1 = [charIndex - 2, charIndex + 2]; 89 const sibling2 = [charIndex - 3, charIndex + 3]; 90 const sibling3 = [charIndex - 4, charIndex + 4]; 91 this.charSelection.each(function(d, i) { 92 d3.select(this).classed("selected", selected.includes(i)) 93 d3.select(this).classed("sibling1", sibling1.includes(i)) 94 d3.select(this).classed("sibling2", sibling2.includes(i)) 95 d3.select(this).classed("sibling3", sibling3.includes(i)) 96 }) 97 } 98} 99 100 101class Chart { 102 103 constructor({dataMap, xDomain, yDomain, sDomain, parent}) { 104 105 this.parent = parent; 106 107 this.data = dataMap; 108 109 this.div = d3.select(".ve-bubbles"); 110 this.body = d3.select("body"); 111 this.svg = this.div.append("svg"); 112 this.container = this.svg.append("g"); 113 114 this.xScale = d3.scaleLog().domain(xDomain); 115 this.yScale = d3.scaleLinear().domain(yDomain); 116 this.sScale = d3.scaleSqrt().domain(sDomain); 117 118 this.cScale = d3.scaleOrdinal().domain(["americas", "africa", "asia", "europe"]) 119 //.range(["#7feb00", "#00d5e9", "#ff5872", "#ffe700"]) 120 .range(["#00bc8d", "#00bc8d", "#00bc8d", "#00bc8d"]) 121 .unknown("#00bc8d"); 122 123 this.lScale = d3.scaleOrdinal().domain(["americas", "africa", "asia", "europe"]) 124 //.range(["#00b900", "#0098df", "#da0025", "#fbaf09"]) 125 .range(["#00503c", "#00503c", "#00503c", "#00503c"]) 126 .unknown("#00503c"); 127 128 } 129 130 updateSize(margin){ 131 132 if (!margin) margin = {top: 0.1, left: 0.2, right: 0.2, bottom: 0.3}; 133 134 const width = this.div.node().clientWidth; 135 const height = this.div.node().clientHeight; 136 const hypo = Math.sqrt(width * width + height * height); 137 138 this.xScale.range([width * margin.left, width * (1 - margin.right)]); 139 this.yScale.range([height * (1 - margin.bottom), height * margin.top]); 140 this.sScale.range([1, hypo / 20]); 141 142 this.svg 143 .attr("width", width) 144 .attr("height", window.innerHeight) 145 //this.container 146 // .attr("transform", `translate(${width * margin.left},${height * margin.top})`) 147 } 148 updateTime({time, duration}) { 149 let data = this.data.get(time); 150 let dataArray = []; 151 if(data) { 152 dataArray = [...data.values()]; 153 }else{ 154 const prevYear = d3.utcParse("%Y")("" + time.getUTCFullYear()); 155 const nextYear = addYear(prevYear); 156 const fraction = (time - prevYear) / (nextYear - prevYear); 157 158 const prevData = this.data.get(prevYear); 159 const nextData = this.data.get(nextYear); 160 161 if(!prevData || !nextData) console.log(time) 162 163 const interpolate = (prev,next,enc) => prev[enc] + (next[enc] - prev[enc]) * fraction; 164 165 dataArray = [...prevData.keys()].map(key => { 166 const prev = prevData.get(key); 167 const next = nextData.get(key); 168 if(!prev || !next) return {geo: key}; 169 return { 170 geo: key, 171 GDP: interpolate(prev, next, "GDP"), 172 LEX: interpolate(prev, next, "LEX"), 173 POP: interpolate(prev, next, "POP"),
174 region: prev.region, 175 } 176 }) 177 } 178 179 180 181 182 const circles = this.container.selectAll("circle") 183 .data(dataArray, d => d.geo) 184 .style("visibility", d => !d.GDP || !d.LEX || !d.POP ? "hidden" : null) 185 186 circles.exit().remove(); 187 circles.enter().append("circle") 188 .attr("cx", d => this.xScale(d.GDP)) 189 .attr("cy", d => this.yScale(d.LEX)) 190 .attr("r", d => this.sScale(d.POP)) 191 .attr("fill", d => this.cScale(d.region)) 192 .attr("stroke", d => this.lScale(d.region)) 193 .style("opacity", this.parent.dragging ? 0.6 : 0.2); 194 195 if(!duration || !this.parent.playing) 196 circles.interrupt() 197 .attr("cx", d => this.xScale(d.GDP)) 198 .attr("cy", d => this.yScale(d.LEX)) 199 .attr("r", d => this.sScale(d.POP)) 200 .attr("fill", d => this.cScale(d.region)) 201 .attr("stroke", d => this.lScale(d.region)) 202 .style("opacity", this.parent.dragging ? 0.6 : 0.2); 203 else 204 circles 205 .transition().duration(duration).ease(d3.easeLinear) 206 .attr("cx", d => this.xScale(d.GDP)) 207 .attr("cy", d => this.yScale(d.LEX)) 208 .attr("r", d => this.sScale(d.POP)) 209 .attr("fill", d => this.cScale(d.region)) 210 .attr("stroke", d => this.lScale(d.region)) 211 .style("opacity", d => this.parent.dragging ? 0.6 : (0.05 + Math.random() * 0.4)); 212 213 214 215 } 216 217 218} 219 220 221class TimeSlider{ 222 constructor({domain, parent}){ 223 this.parent = parent; 224 225 this.margin = 15; 226 const r = 12; 227 this.wasPlaying = null; 228 229 const xToTime = (event) => { 230 let x = 0; 231 if(event.x > this.width - this.margin * 2) x = this.width - this.margin * 2; 232 else if(event.x < 0) x = 0; 233 else x = event.x; 234 return this.scale.invert(x) 235 } 236 237 this.div = d3.select(".ve-bubbles"); 238 239 this.svg = d3.select(".ve-ts-slider") 240 241 this.view = this.svg 242 .select("g") 243 .attr("transform", `translate(${10},${10})`) 244 245 const dragevents = { 246 start: (event) => { 247 this.wasPlaying = this.parent.playing; 248 this.parent.setPlaying(false); 249 this.parent.setDragging(true); 250 this.parent.setTime(xToTime(event)); 251 }, 252 move: (event) => { 253 this.parent.setTime(xToTime(event)); 254 }, 255 end: (event) => { 256 this.parent.setDragging(false); 257 this.parent.setTime(xToTime(event)); 258 this.parent.setPlaying(this.wasPlaying ?? true); 259 } 260 } 261 this.track = this.view.select("rect") 262 .attr("height", 10) 263 .attr("rx", 4) 264 .call(d3.drag() 265 .on("start.interrupt", () => this.knob.interrupt() ) 266 .on("end", dragevents.end) 267 .on("start", dragevents.start) 268 .on("drag", dragevents.move) 269 ); 270 this.position = this.margin; 271 this.knob = this.view.select("circle") 272 .attr("r", r) 273 .attr("cx", this.position) 274 .attr("cy", r/2) 275 276 this.scale = d3.scaleTime().domain(domain); 277 } 278 updateSize(){ 279 this.width = this.div.node().clientWidth * 0.5; 280 this.height = 50; 281 282 this.scale 283 .range([0, this.width - this.margin * 2]); 284 this.track 285 .attr("width", this.width - this.margin * 2); 286 this.svg 287 .attr("width", this.width) 288 .attr("height", this.height); 289 290 } 291 updateTime({time, duration}){ 292 const margin = 15; 293 const r = 12; 294 const target = margin + this.scale(time) - r; 295 if (this.position > target || !this.parent.playing) 296 this.knob 297 .interrupt() 298 .attr("cx", target); 299 else 300 this.knob 301 .transition().duration(duration).ease(d3.easeLinear) 302 .attr("cx", target); 303 304 this.position = target; 305 } 306} 307 308 309
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.