|
| 1 | +# Map Background Scale |
| 2 | + |
| 3 | +After the Euro 2024 final, our boss asked us to implement a map that colours each european country depending on their final position in the above mentioned competition. |
| 4 | + |
| 5 | + |
| 6 | + |
| 7 | +## Steps |
| 8 | + |
| 9 | +- We will take as starting point our previous example: _00-render-map-hover_ let's copy the content and execute _npm start_ |
| 10 | + |
| 11 | +```bash |
| 12 | +npm start |
| 13 | +``` |
| 14 | + |
| 15 | +We have the following information |
| 16 | + |
| 17 | +| Country | Result | |
| 18 | +| --------------------- | :------------: | |
| 19 | +| Spain | champion | |
| 20 | +| England | runner-up | |
| 21 | +| France | semifinals | |
| 22 | +| Netherlands | semifinals | |
| 23 | +| Germany | quarterfinals | |
| 24 | +| Portugal | quarterfinals | |
| 25 | +| Switzerland | quarterfinals | |
| 26 | +| Turkey | quarterfinals | |
| 27 | +| Italy | knockout-stage | |
| 28 | +| Belgium | knockout-stage | |
| 29 | +| Austria | knockout-stage | |
| 30 | +| Romania | knockout-stage | |
| 31 | +| Slovakia | knockout-stage | |
| 32 | +| Slovenia | knockout-stage | |
| 33 | +| Denmark | knockout-stage | |
| 34 | +| Georgia | knockout-stage | |
| 35 | +| Croatia | group-stage | |
| 36 | +| CzechRepublic | group-stage | |
| 37 | +| Hungary | group-stage | |
| 38 | +| Serbia | group-stage | |
| 39 | +| Poland | group-stage | |
| 40 | +| Scotland | group-stage | |
| 41 | +| Albania | group-stage | |
| 42 | +| Ukraine | group-stage | |
| 43 | +| Russia | DSQ | |
| 44 | +| Belarus | DSQ | |
| 45 | + |
| 46 | +- Let's port it to json (respecting the name of the countries that we are using in our eurobasket map) |
| 47 | + |
| 48 | +_./src/stats.ts_: |
| 49 | + |
| 50 | +```typescript |
| 51 | +export const finalStandingsEuro2024 = [ |
| 52 | + { country: "Spain", result: "champion" }, |
| 53 | + { country: "England", result: "runner-up" }, |
| 54 | + { country: "France", result: "semifinals" }, |
| 55 | + { country: "Netherlands", result: "semifinals" }, |
| 56 | + { country: "Germany", result: "quarterfinals" }, |
| 57 | + { country: "Portugal", result: "quarterfinals" }, |
| 58 | + { country: "Switzerland", result: "quarterfinals" }, |
| 59 | + { country: "Turkey", result: "quarterfinals" }, |
| 60 | + { country: "Italy", result: "knockout-stage" }, |
| 61 | + { country: "Belgium", result: "knockout-stage" }, |
| 62 | + { country: "Austria", result: "knockout-stage" }, |
| 63 | + { country: "Romania", result: "knockout-stage" }, |
| 64 | + { country: "Slovakia", result: "knockout-stage" }, |
| 65 | + { country: "Slovenia", result: "knockout-stage" }, |
| 66 | + { country: "Denmark", result: "knockout-stage" }, |
| 67 | + { country: "Georgia", result: "knockout-stage" }, |
| 68 | + { country: "Croatia", result: "group-stage" }, |
| 69 | + { country: "CzechRepublic", result: "group-stage" }, |
| 70 | + { country: "Hungary", result: "group-stage" }, |
| 71 | + { country: "Serbia", result: "group-stage" }, |
| 72 | + { country: "Poland", result: "group-stage" }, |
| 73 | + { country: "Scotland", result: "group-stage" }, |
| 74 | + { country: "Albania", result: "group-stage" }, |
| 75 | + { country: "Ukraine", result: "group-stage" }, |
| 76 | + { country: "Russia", result: "DSQ" }, |
| 77 | + { country: "Belarus", result: "DSQ" }, |
| 78 | +]; |
| 79 | + |
| 80 | +``` |
| 81 | + |
| 82 | +- Now we are going to assign a range of colores for our domain: |
| 83 | + |
| 84 | +_./src/index.ts_: |
| 85 | + |
| 86 | +```typescript |
| 87 | +import { finalStandingsEuro2024 } from "./stats"; |
| 88 | + |
| 89 | +// DIFF |
| 90 | +// set the affected color scale |
| 91 | +const color = d3 |
| 92 | + .scaleOrdinal([ |
| 93 | + "gold", |
| 94 | + "silver", |
| 95 | + "#006400", |
| 96 | + "#6D9062", |
| 97 | + "#DFF8D5", |
| 98 | + "#74B2DF", |
| 99 | + "black", |
| 100 | + ]) |
| 101 | + .domain([ |
| 102 | + "champion", |
| 103 | + "runner-up", |
| 104 | + "semifinals", |
| 105 | + "quarterfinals", |
| 106 | + "knockout-stage", |
| 107 | + "group-stage", |
| 108 | + "DSQ", |
| 109 | + ]); |
| 110 | +// DIFF |
| 111 | +``` |
| 112 | + |
| 113 | +We are using a color palette generated by [mycolor.space](https://mycolor.space/?hex=%23006400&sub=1): |
| 114 | + |
| 115 | + |
| 116 | + |
| 117 | +- Let's create a help function to map from country to color: we have to take into account that some European countries didn't qualify to the Euro 2024 (they don't exist on our list). |
| 118 | + |
| 119 | +```diff |
| 120 | +const color = d3 |
| 121 | + .scaleOrdinal([ |
| 122 | + "gold", |
| 123 | + "silver", |
| 124 | + "#006400", |
| 125 | + "#6D9062", |
| 126 | + "#DFF8D5", |
| 127 | + "#74B2DF", |
| 128 | + "black", |
| 129 | + ]) |
| 130 | + .domain([ |
| 131 | + "champion", |
| 132 | + "runner-up", |
| 133 | + "bronze", |
| 134 | + "semifinals", |
| 135 | + "quarterfinals", |
| 136 | + "knockout-stage", |
| 137 | + "group-stage", |
| 138 | + "DSQ", |
| 139 | + ]); |
| 140 | + |
| 141 | ++ const assignCountryBackgroundColor = (countryName: string) => { |
| 142 | ++ const item = finalStandingsEuro2024.find( |
| 143 | ++ item => item.country === countryName |
| 144 | ++ ); |
| 145 | ++ return item ? color(item.result) : "white"; |
| 146 | ++ }; |
| 147 | +``` |
| 148 | + |
| 149 | +- Before modifying our svg, the css file _map.css_ has to be updated: |
| 150 | + |
| 151 | +```diff |
| 152 | +.country { |
| 153 | + stroke-width: 1; |
| 154 | + stroke: #2f4858; |
| 155 | +- fill: #008c86; |
| 156 | +} |
| 157 | + |
| 158 | +.selected-country { |
| 159 | + stroke-width: 1; |
| 160 | + stroke: #bc5b40; |
| 161 | +- fill: #f88f70; |
| 162 | +} |
| 163 | +``` |
| 164 | + |
| 165 | +- Let's add a fill style to match country name with corresponding background color, using the function defined previously: |
| 166 | + |
| 167 | +```diff |
| 168 | +svg |
| 169 | + .selectAll("path") |
| 170 | + .data(geojson["features"]) |
| 171 | + .enter() |
| 172 | + .append("path") |
| 173 | + .attr("class", "country") |
| 174 | ++ .style("fill", function(d: any) { |
| 175 | ++ return assignCountryBackgroundColor(d.properties.geounit); |
| 176 | ++ }) |
| 177 | + // data loaded from json file |
| 178 | + .attr("d", geoPath as any) |
| 179 | +``` |
| 180 | + |
| 181 | +- Now it's time to modify mouseout and mouseover render features. In this case we will add a tooltip when mouse is over a country, which is going to display the name and final position of that country: |
| 182 | + |
| 183 | +```diff |
| 184 | ++ // Define the div for the tooltip |
| 185 | ++ const div = d3 |
| 186 | ++ .select("body") |
| 187 | ++ .append("div") |
| 188 | ++ .attr("class", "tooltip") |
| 189 | ++ .style("opacity", 0); |
| 190 | + |
| 191 | +svg |
| 192 | + .selectAll("path") |
| 193 | + .data(geojson["features"]) |
| 194 | + .enter() |
| 195 | + .append("path") |
| 196 | + .attr("class", "country") |
| 197 | + .style("fill", function (d: any) { |
| 198 | + return assignCountryBackgroundColor(d.properties.geounit); |
| 199 | + } |
| 200 | + // data loaded from json file |
| 201 | + .attr("d", geoPath as any) |
| 202 | +- .on("mouseover", function(d, i) { |
| 203 | ++ .on("mouseover", function (mouseEvent: MouseEvent, datum: any) { |
| 204 | + d3.select(this).attr("class", "selected-country"); |
| 205 | ++ const country = datum.properties.geounit; |
| 206 | ++ const item = finalStandingsEuro2024.find( |
| 207 | ++ (item) => item.country === country |
| 208 | ++ ); |
| 209 | ++ const countryResult = item ? item.result : "DNQ"; |
| 210 | ++ |
| 211 | ++ const coords = { x: mouseEvent.pageX, y: mouseEvent.pageY }; |
| 212 | ++ div.transition().duration(200).style("opacity", 0.9); |
| 213 | ++ div |
| 214 | ++ .html(`<span>${country}: ${countryResult}</span>`) |
| 215 | ++ .style("left", `${coords.x}px`) |
| 216 | ++ .style("top", `${coords.y - 28}px`); |
| 217 | + }) |
| 218 | + .on("mouseout", function(d, i) { |
| 219 | + d3.select(this).attr("class", "country"); |
| 220 | ++ div.transition() |
| 221 | ++ .duration(500) |
| 222 | ++ .style("opacity", 0); |
| 223 | + }); |
| 224 | +``` |
| 225 | + |
| 226 | +- This new div element requires some styling. Let's create a new css file, _./src/styles.css_ with the content below: |
| 227 | + |
| 228 | +```css |
| 229 | +div.tooltip { |
| 230 | + position: absolute; |
| 231 | + text-align: center; |
| 232 | + width: 70px; |
| 233 | + height: 28px; |
| 234 | + padding: 2px; |
| 235 | + font: 12px sans-serif; |
| 236 | + background: #f7f2cb; |
| 237 | + border: 0px; |
| 238 | + border-radius: 8px; |
| 239 | + pointer-events: none; |
| 240 | +} |
| 241 | +``` |
| 242 | + |
| 243 | +- And import this new css file in our html file _index.html_: |
| 244 | + |
| 245 | +```diff |
| 246 | + <link rel="stylesheet" type="text/css" href="./map.css" /> |
| 247 | ++ <link rel="stylesheet" type="text/css" href="./styles.css" /> |
| 248 | + <link rel="stylesheet" type="text/css" href="./base.css" /> |
| 249 | +``` |
| 250 | + |
| 251 | +- Let's give a try |
| 252 | + |
| 253 | +```bash |
| 254 | +npm start |
| 255 | +``` |
| 256 | + |
| 257 | +## About Basefactor + Lemoncode |
| 258 | + |
| 259 | +We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. |
| 260 | + |
| 261 | +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. |
| 262 | + |
| 263 | +[Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. |
| 264 | + |
| 265 | +For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: <http://lemoncode.net/master-frontend> |
0 commit comments