PageSourceSearch

https://vizabi.com/src/bubbles.js

js vizabi.com collected 2026-10-10 00:07:30 UTC 10,702 bytes, 309 lines download raw bytes

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.