@@ -19,128 +19,154 @@ A powerful, lightweight visualization for COM-480 built with Hugo, Tailwind CSS.
1919- [ Languages] ( visualizations/languages/ ) : see the distribution of books by language as a waffle chart.
2020- [ Publishers] ( visualizations/publishers/ ) : compare gender representation across major publishers.
2121- [ Library] ( visualizations/library/ ) : browse and filter books on an interactive shelf.
22- ## Scale countries by amount of books published by author's from these countries
23- # Cartogram
24- !-- prettier-ignore-start -->
25- script src="https://unpkg.com/topojson@3/dist/topojson.min.js"> </script >
26- script src="{{< asset-url "js/cartogram.js" >}}"></script >
27- {< d3 >}}
28- onst width = container.clientWidth;
29- onst height = 500;
30- onst svg = d3.select(container).append("svg")
31- .attr("width", width)
32- .attr("height", height);
33- onst genderLabels = {
34- "m": "Male",
35- "w": "Female",
36- "w;m": "Female & Male",
37- "m;m": "Male & Male",
38- "m;m;m": "Male & Male & Male",
39- "w;w": "Female & Female"
40- ;
41- onst genders = Object.keys(genderLabels);
42- onst isDark = document.documentElement.classList.contains("dark");
43- onst labelColor = isDark ? congoColors.neutral100 : "#000000 ";
44- onst selector = d3.select(container).insert("div", "svg")
45- .style("margin-bottom", "10px");
46- elector.append("label")
47- .text("Select gender: ")
48- .style("color", labelColor);
49- onst select = selector.append("select")
50- .style("margin-bottom", "8px")
51- .style("padding", "6px 12px")
52- .style("background", congoColors.primary400)
53- .style("color", congoColors.neutral100)
54- .style("border", "none")
55- .style("border-radius", "4px")
56- .style("cursor", "pointer");
57- romise.all([
58- d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json "),
59- d3.csv("{{< asset-url "data/nationalities.csv" >}}")
60- ).then(([ topology, data] ) => {
61- const dataById = new Map(
62- data
63- .filter(d => d[ "ID"] && d[ "ID"] !== "nan")
64- .map(d => [ +d[ "ID"] , { counts: +d[ "counts"] , name: d[ "nationality"] }] )
65- );
66- const nameById = new Map(
67- topology.objects.countries.geometries.map(d => [ +d.id, d.properties?.name ?? "Unknown"] )
68- );
69- // Split geometries
70- const problematicIDs = [ 10, 643, 242] ; // Antarctica, Russia
71- const cartoGeometries = topology.objects.countries.geometries
72- .filter(d => !problematicIDs.includes(+d.id));
73- const staticGeometries = topology.objects.countries.geometries
74- .filter(d => problematicIDs.includes(+d.id)); // Russia yes, Antarctica no
75- const values = cartoGeometries.map(d => dataById.get(+d.id)?.counts ?? 0);
76- const lo = d3.min(values), hi = d3.max(values);
77- const scale = d3.scaleLinear().domain([ lo, hi] ).range([ 1, 20] );
78- const colorScale = d3.scaleSequentialLog(d3.interpolate(
79- congoColors.neutral100,
80- congoColors.primary500
81- )).domain([ 1, hi] );
82- const color = d => {
83- const counts = dataById.get(+d.id)?.counts ?? 0;
84- return counts === 0 ? congoColors.neutral100 : colorScale(counts);
85- };
86- // Tooltip handlers
87- const onMouseover = (event, d) => {
88- const id = +d.id;
89- const entry = dataById.get(id);
90- const name = entry?.name ?? nameById.get(id) ?? "Unknown";
91- const counts = entry?.counts ?? 0;
92- tooltip.style("opacity", 1)
93- .html(` <strong>${name}</strong><br/>${counts} book${counts !== 1 ? "s" : ""} ` );
94- };
95- const onMousemove = (event) => {
96- const [ x, y] = d3.pointer(event, container);
97- tooltip
98- .style("left", ` ${x + 12}px ` )
99- .style("top", ` ${y - 28}px ` );
100- };
101- const onMouseout = () => tooltip.style("opacity", 0);
102- carto
103- .properties(d => ({ id: d.ID }))
104- .value(d => scale(dataById.get(+d.id)?.counts ?? 0));
105- const features = carto(topology, cartoGeometries).features;
106- const cartoFeatures = carto(topology, cartoGeometries).features;
107- const normalFeatures = cartoGeometries.map(d => topojson.feature(topology, d));
108- // Draw exluded countries because of anti-meridian as static layer first
109- svg.selectAll(".static-country")
110- .data(staticGeometries.map(d => topojson.feature(topology, d)))
111- .enter().append("path")
112- .attr("class", "static-country")
113- .attr("d", staticPath)
114- .attr("fill", color)
115- .attr("stroke", congoColors.neutral100)
116- .attr("stroke-width", 0.5)
117- .on("mouseover", onMouseover)
118- .on("mousemove", onMousemove)
119- .on("mouseout", onMouseout);
120- // Draw cartogram countries
121- const paths = svg.selectAll(".carto-country")
122- .data(cartoFeatures)
123- .enter().append("path")
124- .attr("class", "carto-country")
125- .attr("d", carto.path)
126- .attr("fill", color)
127- .attr("stroke", congoColors.neutral100)
128- .attr("stroke-width", 0.5)
129- .on("mouseover", onMouseover)
130- .on("mousemove", onMousemove)
131- .on("mouseout", onMouseout);
132- // Toggle between distorted and normal
133- button.on("click", () => {
134- distorted = !distorted;
135- button.text(distorted ? "Show Normal Map" : "Show Cartogram");
136- paths.transition().duration(750)
137- .attr("d", (d, i) => distorted
138- ? carto.path(d)
139- : staticPath(normalFeatures[ i] )
140- );
141- });
142- ).catch(err => {
143- console.error("Error loading data:", err);
144- );
145- {< /d3 >}}
146- !-- prettier-ignore-end -->
22+
23+ ### Scale countries by amount of books published by author's from these countries
24+
25+
26+ ## Cartogram
27+
28+ <!-- prettier-ignore-start -->
29+ <script src =" https://unpkg.com/topojson@3/dist/topojson.min.js " ></script >
30+ <script src =" {{< asset-url " js /cartogram.js" >}}" ></script>
31+
32+ {{< d3 >}}
33+ const width = container.clientWidth;
34+ const height = 500;
35+ const svg = d3.select(container).append(" svg" )
36+ .attr(" width" , width)
37+ .attr(" height" , height);
38+
39+ const genderLabels = {
40+ " m" : " Male" ,
41+ " w" : " Female" ,
42+ " w;m" : " Female & Male" ,
43+ " m;m" : " Male & Male" ,
44+ " m;m;m" : " Male & Male & Male" ,
45+ " w;w" : " Female & Female"
46+ };
47+ const genders = Object.keys(genderLabels);
48+ const isDark = document.documentElement.classList.contains(" dark" );
49+ const labelColor = isDark ? congoColors.neutral100 : " #000000 " ;
50+
51+ const selector = d3.select(container).insert(" div" , " svg" )
52+ .style(" margin- bottom" , " 10px " );
53+ selector.append(" label" )
54+ .text(" Select gender: " )
55+ .style(" color" , labelColor);
56+ const select = selector.append(" select" )
57+ .style(" margin- bottom" , " 8px " )
58+ .style(" padding" , " 6px 12px " )
59+ .style(" background" , congoColors.primary400)
60+ .style(" color" , congoColors.neutral100)
61+ .style(" border" , " none" )
62+ .style(" border- radius" , " 4px " )
63+ .style(" cursor" , " pointer" );
64+
65+ Promise.all([
66+ d3.json(" https: // cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"),
67+ d3 .csv (" {{< asset-url " data/ nationalities .csv " >}}" )
68+ ]).then (([topology , data ]) => {
69+ const dataById = new Map (
70+ data
71+ .filter (d => d[" ID" ] && d[" ID" ] !== " nan" )
72+ .map (d => [+ d[" ID" ], { counts: + d[" counts" ], name: d[" nationality" ] }])
73+ );
74+ const nameById = new Map (
75+ topology .objects .countries .geometries .map (d => [+ d .id , d .properties ? .name ?? " Unknown" ])
76+ );
77+
78+
79+ // Split geometries
80+ const problematicIDs = [10 , 643 , 242 ]; // Antarctica, Russia
81+ const cartoGeometries = topology .objects .countries .geometries
82+ .filter (d => ! problematicIDs .includes (+ d .id ));
83+
84+ const staticGeometries = topology .objects .countries .geometries
85+ .filter (d => problematicIDs .includes (+ d .id )); // Russia yes, Antarctica no
86+
87+
88+ const values = cartoGeometries .map (d => dataById .get (+ d .id )? .counts ?? 0 );
89+
90+ const lo = d3 .min (values), hi = d3 .max (values);
91+
92+ const scale = d3 .scaleLinear ().domain ([lo, hi]).range ([1 , 20 ]);
93+ const colorScale = d3 .scaleSequentialLog (d3 .interpolate (
94+ congoColors .neutral100 ,
95+ congoColors .primary500
96+ )).domain ([1 , hi]);
97+
98+ const color = d => {
99+ const counts = dataById .get (+ d .id )? .counts ?? 0 ;
100+ return counts === 0 ? congoColors .neutral100 : colorScale (counts);
101+ };
102+
103+ // Tooltip handlers
104+ const onMouseover = (event , d ) => {
105+ const id = + d .id ;
106+ const entry = dataById .get (id);
107+ const name = entry? .name ?? nameById .get (id) ?? " Unknown" ;
108+ const counts = entry? .counts ?? 0 ;
109+ tooltip .style (" opacity" , 1 )
110+ .html (` <strong>${ name} </strong><br/>${ counts} book${ counts !== 1 ? " s" : " " } ` );
111+ };
112+
113+ const onMousemove = (event ) => {
114+ const [x , y ] = d3 .pointer (event , container);
115+ tooltip
116+ .style (" left" , ` ${ x + 12 } px` )
117+ .style (" top" , ` ${ y - 28 } px` );
118+ };
119+
120+ const onMouseout = () => tooltip .style (" opacity" , 0 );
121+
122+ carto
123+ .properties (d => ({ id: d .ID }))
124+ .value (d => scale (dataById .get (+ d .id )? .counts ?? 0 ));
125+
126+ const features = carto (topology, cartoGeometries).features ;
127+ const cartoFeatures = carto (topology, cartoGeometries).features ;
128+ const normalFeatures = cartoGeometries .map (d => topojson .feature (topology, d));
129+
130+ // Draw exluded countries because of anti-meridian as static layer first
131+ svg .selectAll (" .static-country" )
132+ .data (staticGeometries .map (d => topojson .feature (topology, d)))
133+ .enter ().append (" path" )
134+ .attr (" class" , " static-country" )
135+ .attr (" d" , staticPath)
136+ .attr (" fill" , color)
137+ .attr (" stroke" , congoColors .neutral100 )
138+ .attr (" stroke-width" , 0.5 )
139+ .on (" mouseover" , onMouseover)
140+ .on (" mousemove" , onMousemove)
141+ .on (" mouseout" , onMouseout);
142+
143+ // Draw cartogram countries
144+ const paths = svg .selectAll (" .carto-country" )
145+ .data (cartoFeatures)
146+ .enter ().append (" path" )
147+ .attr (" class" , " carto-country" )
148+ .attr (" d" , carto .path )
149+ .attr (" fill" , color)
150+ .attr (" stroke" , congoColors .neutral100 )
151+ .attr (" stroke-width" , 0.5 )
152+ .on (" mouseover" , onMouseover)
153+ .on (" mousemove" , onMousemove)
154+ .on (" mouseout" , onMouseout);
155+
156+ // Toggle between distorted and normal
157+ button .on (" click" , () => {
158+ distorted = ! distorted;
159+ button .text (distorted ? " Show Normal Map" : " Show Cartogram" );
160+
161+ paths .transition ().duration (750 )
162+ .attr (" d" , (d , i ) => distorted
163+ ? carto .path (d)
164+ : staticPath (normalFeatures[i])
165+ );
166+ });
167+
168+ }).catch (err => {
169+ console .error (" Error loading data:" , err);
170+ });
171+ {{< / d3 > }}
172+ <!-- prettier- ignore- end -->
0 commit comments