Home > Mobile >  How can I simplify this JavaScript code that creates SVG images without breaking it?
How can I simplify this JavaScript code that creates SVG images without breaking it?

Time:01-03

I have figured out how to recreate the map from the 1985 game "Balance of Power" (enter image description here

My question revolves around how do I simplify the JavaScript code that I originally wrote to make it more generic without breaking it? (I am not very familiar with JavaScript and its idiosyncrasies but I am learning).

The code that I first wrote was HTML, CSS, and JavaScript. The HTML is simply, five tags, one for each country's land mass, within an tag that defines the overall width, height, and viewBox.

<svg id="map"  xmlns="http://www.w3.org/2000/svg" width="512" height="342" viewBox="0 0 512 342">
    <g id="usa_main"  xmlns="http://www.w3.org/2000/svg" />
    <g id="usa_alaska"  xmlns="http://www.w3.org/2000/svg" />
    <g id="canada_main"  xmlns="http://www.w3.org/2000/svg" />
    <g id="canada_victoria"  xmlns="http://www.w3.org/2000/svg" /> 
    <g id="mexico_main"  xmlns="http://www.w3.org/2000/svg" />
</svg>

The CSS is equally simple, code to turn the entire map background antique white and fill in a polygon with black when you cross over its border.

svg {
    background-color: antiquewhite;
}

g polygon:hover {
    fill: #000000;
}

The initial JavaScript that I wrote to create the SVG images was a little more complicated. First, I defined (A) some constants to hold map data and map references and (B) variables that would hold the polygon outlines that I would calculate from the original data.

const svgns = "http://www.w3.org/2000/svg"; 
const svg = document.querySelector("svg"); 

const usa_main = document.querySelector("g#usa_main"); 
const usa_alaska = document.querySelector("g#usa_alaska"); 
const canada_main = document.querySelector("g#canada_main");
const canada_victoria = document.querySelector("g#canada_victoria");
const mexico_main = document.querySelector("g#mexico_main"); 

const stroke_default = "black";
const fill_default = "none";

let outline = "";

let united_states = document.createElementNS(svgns, "polygon");
let alaska = document.createElementNS(svgns, "polygon");
let canada = document.createElementNS(svgns, "polygon");
let victoria = document.createElementNS(svgns, "polygon");
let mexico = document.createElementNS(svgns, "polygon");

const map_data = [
    {country:"United States",
     country_id:"usa_main",
     origin_x:"60",
     origin_y:"84",
     border_data:"3ES6ES6ES6ES6ES6ES6ES6ES3ESWSWS3EN2ESESESEES5WSSWSSWWSWSW3SESEENENE3NENENN4ESSWSWSE3SWSSEEN3ENENN6E3N9EENENEN3E3SWSSWSWWSWWSWSEES4WS3WSWWSSWSWS3WNNW5SW3NWSSWSSESSWSWS3WSSWWSSWWSWSWWSWSSW4SE7SWS3WNWNNENNWNNE4N2WN3WN6WSESSWNWWNWWS3WSWWS3WSWWSWSW4S2WNWNW4NW3N5W3NWNN5WN2WN2WN2WN6W4NWNW9NNE3NE2NENE2NE2NENE2NENE2NENENE2NE2NENE2N"},
    {country:"Alaska",
     country_id:"usa_alaska",
     origin_x:"30",
     origin_y:"56",
     border_data:"3EN3ENEN2ESE2NW3NEN6ES2ENEN2EN3WS2W2NE2N2EN5E2SENENENW3N2ES4EN3EN6ES2ENEN2ESES2ESES6E2SWSWS2WSWS2WSWS2WSWSWS2WSWSWS2WSWS2W5SE9S2W5NW2NW2NWN2WNWN2WN3WS2WS3WNEN2ENEN2WS2WS2WS9W2WS5W3N"},
    {country:"Canada",
     country_id:"canada_main",
     origin_x:"86",
     origin_y:"41",
     border_data:"2ESES2ENENEN2ES2EN3ENE2S2ES4ESES3ESE2S7EN3ESE2SES2ES2EN3EN2ENENESESE4N2ENENEN2ENEN3EN2ESESWSWSWSW2SWSW2SE2SWSES2ENENENENESE2SWS2W2S3WS2WNWS3WS2WNW2S2E2S3WN3WS2WS2WS2W2SWS2WS2WS3W7S3ESES2ESES3ESW2SW2SE2SES2E3NENE2NEN2EN2ENENE5NENEN2ENE3N3ES3ENESESES2E2SW2SW2SES2EN2E2NE2NE6SW3SE3S3ESES2E4SW2S5WS9W6WS2WS2WSWSWSENEN3EN3ES2E2SWSWS2W2SES2ESES3WSWS4WNEN2EN2WS2WSW2NE3N3WSWSWS9WWS5W2SWS5WNENENENEN2ENWNEN5W2NE3NWNWN4WS2WSWN3WN6WN6WN6WN6WN6WN6WN6WN4W6NE2NE2NENE2NEN2E9NW5N2ENEN2ENENEN2ENENEN2ENEN2ENEN2ENENEN"},
   {country:"Victoria Island",
    country_id:"canada_victoria",
    origin_x:"114",
    origin_y:"43",
    border_data:"2ES3EN2W2NEN3ESES2ENEN2ESES2E2SES3ESWS4WN2W2SES2WNWNWNWN6WS3W3N"},
   {country:"Mexico",
    country_id:"mexico_main",
    origin_x:"47",
    origin_y:"124",
    border_data:"6ES2ES2ES2ES5E2SE3S5E3SE4SESESE4SW2SW3SE2SW2SES2ESESEN2ES2ENENE3NEN3ESEN3ESWSW3SW2SWS4W3S4WSWS2WN6WS2WNWNWN2WNWN2WNWNWNWNWNW3NENE3N2W6NW3NW5NW3NW5NWNW9S3SE3SE8S2W2NW4NW2NW2NWNW2N2E2NW3NE6N"
     
   }
]

Iterating through the map data, I created a polygon from the original data, set its stroke and fill, and then assigned it to a dedicated polygon object, one for each country. An If/Then statement was used to control which outline data was assigned to which country polygon object and then append the polygon object to the map.

map_data.forEach((country, index) => {
    
    outline = createOutline(country.origin_x, country.origin_y, country.border_data);
    
    console.log(index);
    console.log(country.country_id);
    console.log(outline);
    
    // This code uses if/then statements and dedicated SVG polygon elements (one for each country)
    if (country.country_id === "usa_main") {
        united_states.setAttribute("points", outline);
        united_states.setAttribute("stroke", stroke_default);
        united_states.setAttribute("fill", fill_default);
        
        usa_main.appendChild(united_states);
    }
    
    if (country.country_id === "usa_alaska") {
        alaska.setAttribute("points", outline);
        alaska.setAttribute("stroke", stroke_default);
        alaska.setAttribute("fill", fill_default);
        
        usa_alaska.appendChild(alaska);
    }
    
    if (country.country_id === "canada_main") {
        canada.setAttribute("points", outline);
        canada.setAttribute("stroke", stroke_default);
        canada.setAttribute("fill", fill_default);
        
        canada_main.appendChild(canada); // <= country_svg doesn't work ???
    }
    
    if (country.country_id === "canada_victoria") {
        victoria.setAttribute("points", outline);
        victoria.setAttribute("stroke", stroke_default);
        victoria.setAttribute("fill", fill_default);
        
        canada_victoria.appendChild(victoria);
    }
    
    if (country.country_id === "mexico_main") {
        mexico.setAttribute("points", outline);
        mexico.setAttribute("stroke", stroke_default);
        mexico.setAttribute("fill", fill_default);
        
        mexico_main.appendChild(mexico);
    }
    
});

Here is the code that calculated the points that made up each country's outline, included here just for completeness.

function createOutline(origin_x, origin_y, direction_string) {
    var points = "";
    var current_x = parseInt(origin_x);
    var current_y = parseInt(origin_y);
    
    for (let i = 0; i <= direction_string.length; i  ) {
        points = points   current_x.toString()   ","   current_y.toString()   " ";
        

        k = direction_string.substr(i, 1).charCodeAt(0) - 49;
        l = i   1;

        if (k < 0 || k > 8) {
            k = 1;
            l = i;
        }

        direction = direction_string[l];

        switch (direction) {
            case "N":
                current_y = current_y - k;
                break;
            case "S":
                current_y = current_y   k;
                break;
            case "E":
                current_x = current_x   k;
                break;
            case "W":
                current_x = current_x - k;
                break;
        }
    }
    
    return points;
}

This appeared to work fine. Five SVG land masses were calculated and displayed (CodePen enter image description here

I now tried to simplify the code, replacing the four If/Then statements with a Switch/Case statement (everything else remained the same).

map_data.forEach((country, index) => {
    
    outline = createOutline(country.origin_x, country.origin_y, country.border_data);
    
    country_svg.setAttribute("points", outline);
    country_svg.setAttribute("stroke", stroke_default);
    country_svg.setAttribute("fill", fill_default);
    
    console.log(index);
    console.log(country.country_id);
    console.log(outline);
    console.log(country_svg);

    // This code uses a switch statment instead of individual if/then statements but still uses dedicated SVG polygon elements (one for each country)
    switch (country.country_id) {
        case "usa_main":
            united_states.setAttribute("points", outline);
            united_states.setAttribute("stroke", stroke_default);
            united_states.setAttribute("fill", fill_default);
            usa_main.appendChild(united_states);
            console.log(usa_main); 
            break;
        case "usa_alaska":
            alaska.setAttribute("points", outline);
            alaska.setAttribute("stroke", stroke_default);
            alaska.setAttribute("fill", fill_default);
            usa_alaska.appendChild(alaska);
            console.log(usa_alaska);
            break;
        case "canada_main":
            canada.setAttribute("points", outline);
            canada.setAttribute("stroke", stroke_default);
            canada.setAttribute("fill", fill_default);
            canada_main.appendChild(canada);
            console.log(canada_main);
            break;
        case "canada_victoria":
            victoria.setAttribute("points", outline);
            victoria.setAttribute("stroke", stroke_default);
            victoria.setAttribute("fill", fill_default);
            canada_victoria.appendChild(victoria);
            console.log(canada_victoria);
            break;
        case "mexico_main":
            mexico.setAttribute("points", outline);
            mexico.setAttribute("stroke", stroke_default);
            mexico.setAttribute("fill", fill_default);
            mexico_main.appendChild(mexico);
            console.log(mexico_main);
            break;
        default:
    }
    
});

This also appeared to work fine. Five SVG land masses were calculated and displayed (CodePen enter image description here

Now I tried to get rid of the five individual polygon objects, one for each country land mass, and replace them with a single polygon object that I could use for each country.

let country_svg = document.createElementNS(svgns, "polygon"); // This replaces the five country-specific SVG polygons

map_data.forEach((country, index) => {
    
    outline = createOutline(country.origin_x, country.origin_y, country.border_data);
    
    country_svg.setAttribute("points", outline);
    country_svg.setAttribute("stroke", stroke_default);
    country_svg.setAttribute("fill", fill_default);
    
    console.log(index);
    console.log(country.country_id);
    console.log(outline);
    console.log(country_svg);
    
    // This code still uses a switch statement but substitutes a generic SVG polygon element for individual country SVG polygon elements (it doesn't work)
    switch (country.country_id) {
        case "usa_main":
            usa_main.appendChild(country_svg);
            console.log(usa_main); 
            break;
        case "usa_alaska":
            usa_alaska.appendChild(country_svg);
            console.log(usa_alaska);
            break;
        case "canada_main":
            canada_main.appendChild(country_svg);
            console.log(canada_main);
            break;
        case "canada_victoria":
            canada_victoria.appendChild(country_svg);
            console.log(canada_victoria);
            break;
        case "mexico_main":
            mexico_main.appendChild(country_svg);
            console.log(mexico_main);
            break;
        default:
    }
 
    country_svg.setAttribute("points", "");
    
});

This did not appear to work. The console shows the same points and polygons by the first two forEach code examples are generated by this code as nothing shows up onscreen (CodePen enter image description here

Since I am new to JavaScript I was hoping someone could point out why this last bit of code is not working. Thanks in advance.

CodePudding user response:

It was pointed out to me in the comments that while I am storing calculated outline data in the point attributes of individual SVG elements, the underlying polygon object, country_svg, is what is will be rendered onscreen. It's obvious to me now that I was creating a one-to-many relationship between the country_svg polygon and the point attributes, which I did not realize earlier.

Since I am replacing a previous polygons data with the next set of polygon data, only one land mass, Mexico, will be rendered onscreen.

enter image description here

Note: The reason this didn't happen in the code in my original post is because this line clears out the polygon's points data as a last step. When this line is commented out, Mexico appears.

country_svg.setAttribute("points", "");

I've refactored my code to replace the single country_svg polygon with two arrays - country_svg[] and country_polygon[]. Both arrays are sized to the amount of country data that I have in map_data[].

let country_svg = [map_data.length - 1];
let country_polygon = [map_data.length - 1];

Each outline is stored in an element of country_polygon[].

outline = createOutline(country.origin_x, country.origin_y, country.border_data);
country_polygon[index] = document.createElementNS(svgns, "polygon");

country_polygon[index].setAttribute("points", outline);
country_polygon[index].setAttribute("stroke", stroke_default);
country_polygon[index].setAttribute("fill", fill_default);

And then that country_polygon[] data is stored in a corresponding element of country_svg[].

country_svg[index] = country_polygon[index];

Which is appended to its corresponding country's SVG element in the map.

 switch (country.country_id) {
        case "usa_main":
            usa_main.appendChild(country_svg[index]);
            console.log(usa_main); 
            break;
        case "usa_alaska":
            usa_alaska.appendChild(country_svg[index]);
            console.log(usa_alaska);
            break;
        case "canada_main":
            canada_main.appendChild(country_svg[index]);
            console.log(canada_main);
            break;
        case "canada_victoria":
            canada_victoria.appendChild(country_svg[index]);
            console.log(canada_victoria);
            break;
        case "mexico_main":
            mexico_main.appendChild(country_svg[index]);
            console.log(mexico_main);
            break;
        default:
    }

The result is that all country land masses are drawn on the map.

enter image description here

Here is the JS/CSS/HTML code in its entirety (CodePen SVG North America 0.3)

const svgns = "http://www.w3.org/2000/svg"; // variable for the namespace
const svg = document.querySelector("svg"); // targeting the svg itself

const usa_main = document.querySelector("g#usa_main"); // targeting the united states div
const usa_alaska = document.querySelector("g#usa_alaska"); 
const canada_main = document.querySelector("g#canada_main");
const canada_victoria = document.querySelector("g#canada_victoria");
const mexico_main = document.querySelector("g#mexico_main"); 

const stroke_default = "black";
const fill_default = "none";

const map_data = [
    {country:"United States",
     country_id:"usa_main",
     origin_x:"60",
     origin_y:"84",
     border_data:"3ES6ES6ES6ES6ES6ES6ES6ES3ESWSWS3EN2ESESESEES5WSSWSSWWSWSW3SESEENENE3NENENN4ESSWSWSE3SWSSEEN3ENENN6E3N9EENENEN3E3SWSSWSWWSWWSWSEES4WS3WSWWSSWSWS3WNNW5SW3NWSSWSSESSWSWS3WSSWWSSWWSWSWWSWSSW4SE7SWS3WNWNNENNWNNE4N2WN3WN6WSESSWNWWNWWS3WSWWS3WSWWSWSW4S2WNWNW4NW3N5W3NWNN5WN2WN2WN2WN6W4NWNW9NNE3NE2NENE2NE2NENE2NENE2NENENE2NE2NENE2N"},
    {country:"Alaska",
     country_id:"usa_alaska",
     origin_x:"30",
     origin_y:"56",
     border_data:"3EN3ENEN2ESE2NW3NEN6ES2ENEN2EN3WS2W2NE2N2EN5E2SENENENW3N2ES4EN3EN6ES2ENEN2ESES2ESES6E2SWSWS2WSWS2WSWS2WSWSWS2WSWSWS2WSWS2W5SE9S2W5NW2NW2NWN2WNWN2WN3WS2WS3WNEN2ENEN2WS2WS2WS9W2WS5W3N"},
    {country:"Canada",
     country_id:"canada_main",
     origin_x:"86",
     origin_y:"41",
     border_data:"2ESES2ENENEN2ES2EN3ENE2S2ES4ESES3ESE2S7EN3ESE2SES2ES2EN3EN2ENENESESE4N2ENENEN2ENEN3EN2ESESWSWSWSW2SWSW2SE2SWSES2ENENENENESE2SWS2W2S3WS2WNWS3WS2WNW2S2E2S3WN3WS2WS2WS2W2SWS2WS2WS3W7S3ESES2ESES3ESW2SW2SE2SES2E3NENE2NEN2EN2ENENE5NENEN2ENE3N3ES3ENESESES2E2SW2SW2SES2EN2E2NE2NE6SW3SE3S3ESES2E4SW2S5WS9W6WS2WS2WSWSWSENEN3EN3ES2E2SWSWS2W2SES2ESES3WSWS4WNEN2EN2WS2WSW2NE3N3WSWSWS9WWS5W2SWS5WNENENENEN2ENWNEN5W2NE3NWNWN4WS2WSWN3WN6WN6WN6WN6WN6WN6WN6WN4W6NE2NE2NENE2NEN2E9NW5N2ENEN2ENENEN2ENENEN2ENEN2ENEN2ENENEN"},
   {country:"Victoria Island",
    country_id:"canada_victoria",
    origin_x:"114",
    origin_y:"43",
    border_data:"2ES3EN2W2NEN3ESES2ENEN2ESES2E2SES3ESWS4WN2W2SES2WNWNWNWN6WS3W3N"},
   {country:"Mexico",
    country_id:"mexico_main",
    origin_x:"47",
    origin_y:"124",
    border_data:"6ES2ES2ES2ES5E2SE3S5E3SE4SESESE4SW2SW3SE2SW2SES2ESESEN2ES2ENENE3NEN3ESEN3ESWSW3SW2SWS4W3S4WSWS2WN6WS2WNWNWN2WNWN2WNWNWNWNWNW3NENE3N2W6NW3NW5NW3NW5NWNW9S3SE3SE8S2W2NW4NW2NW2NWNW2N2E2NW3NE6N"
     
   }
]

let country_svg = [map_data.length - 1];
let country_polygon = [map_data.length - 1]; // = document.createElementNS(svgns, "polygon");
let outline = "";

map_data.forEach((country, index) => {
    
    outline = createOutline(country.origin_x, country.origin_y, country.border_data);
    country_polygon[index] = document.createElementNS(svgns, "polygon");
    
    country_polygon[index].setAttribute("points", outline);
    country_polygon[index].setAttribute("stroke", stroke_default);
    country_polygon[index].setAttribute("fill", fill_default);
    
    country_svg[index] = country_polygon[index];
    
    switch (country.country_id) {
        case "usa_main":
            usa_main.appendChild(country_svg[index]);
            console.log(usa_main); 
            break;
        case "usa_alaska":
            usa_alaska.appendChild(country_svg[index]);
            console.log(usa_alaska);
            break;
        case "canada_main":
            canada_main.appendChild(country_svg[index]);
            console.log(canada_main);
            break;
        case "canada_victoria":
            canada_victoria.appendChild(country_svg[index]);
            console.log(canada_victoria);
            break;
        case "mexico_main":
            mexico_main.appendChild(country_svg[index]);
            console.log(mexico_main);
            break;
        default:
    }
    
});

function createOutline(origin_x, origin_y, direction_string) {
    var points = "";
    var current_x = parseInt(origin_x);
    var current_y = parseInt(origin_y);
    
    for (let i = 0; i <= direction_string.length; i  ) {
        points = points   current_x.toString()   ","   current_y.toString()   " ";
        

        k = direction_string.substr(i, 1).charCodeAt(0) - 49;
        l = i   1;

        if (k < 0 || k > 8) {
            k = 1;
            l = i;
        }

        direction = direction_string[l];

        switch (direction) {
            case "N":
                current_y = current_y - k;
                break;
            case "S":
                current_y = current_y   k;
                break;
            case "E":
                current_x = current_x   k;
                break;
            case "W":
                current_x = current_x - k;
                break;
        }
    }
    
    return points;
}
svg {
    background-color: antiquewhite;
}

g polygon:hover {
    fill: #000000;
}
<svg id="map"  xmlns="http://www.w3.org/2000/svg" width="512" height="342" viewBox="0 0 512 342">
    <g id="usa_main"  xmlns="http://www.w3.org/2000/svg" />
    <g id="usa_alaska"  xmlns="http://www.w3.org/2000/svg" />
    <g id="canada_main"  xmlns="http://www.w3.org/2000/svg" />
    <g id="canada_victoria"  xmlns="http://www.w3.org/2000/svg" /> 
    <g id="mexico_main"  xmlns="http://www.w3.org/2000/svg" />
</svg>

I like it better than my original code but I will probably look for ways to refactor it a bit more, avoiding premature optimization.

  • Related