hierocles hierocles - 5 months ago 20
Node.js Question

Using str.replace() to fix clipPath case-sensitivity makes SVG file uneditable

I'm building a Node-powered command line interface to generate SVG maps. I'm having trouble producing SVG files that can be edited in programs like Inkscape, however, which limits the usefulness of the utility.

The issue is that a WebKit error causes D3-generated

clipPath
elements to become all-lowercase
clippath
elements instead, which of course isn't proper SVG and ends up ruining the image. To get around this, I do a global replace to convert it back to camelCase.

That workaround produces an SVG file that can be viewed fine in a browser, but can't be edited in an SVG editing program like Inkscape. If I don't do the global replace, the file can be edited just fine.

Any clue how to fix this issue?

GitHub repo, so you can test the CLI yourself: https://github.com/hierocles/housemapper-cli

Relevant code:

function makeMap(fileName) {
var document = jsdom.jsdom();

var us = JSON.parse(fs.readFileSync( __dirname + '/jsonfiles/us.json', 'utf8'));
var congress = JSON.parse(fs.readFileSync(__dirname + '/jsonfiles/us-cong-114.json', 'utf8'));

var css = "<![CDATA[ \
.background { \
fill: none; \
} \
.district { \
fill: #ccc; \
} \
.district-dem-yes { \
fill: #394DE5; \
} \
.district-dem-no { \
fill: #7585FF; \
} \
.district-rep-yes { \
fill: #EA513C; \
} \
.district-rep-no { \
fill: #EA998F; \
} \
.district-yes { \
fill: #03BC82; \
} \
.district-no { \
fill: #3BE2AD; \
} \
.state-boundaries { \
fill: none; \
stroke: #fff; \
stroke-width: 1px; \
} \
.district-boundaries { \
fill: none; \
stroke: #fff; \
stroke-width: 0.5px; \
stroke-linecap: round; \
stroke-linejoin: round; \
}\
]]>";

var width = 960,
height = 500;

var projection = d3.geo.albersUsa()
.scale(1000)
.translate([width/2, height/2]);

var path = d3.geo.path()
.projection(projection);

var svg = d3.select(document.body).append('svg')
.attr('xmlns', 'http://www.w3.org/2000/svg')
.attr('width', width)
.attr('height', height);

svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);

var defs = svg.append('defs');

defs.append('path')
.attr('id', 'land')
.datum(topojson.feature(us, us.objects.land))
.attr('d', path);

defs.append('style')
.attr('type', 'text/css')
.text(css);

defs.append('clipPath')
.attr('id', 'clip-land')
.append('use')
.attr('xlink:href', '#land');

var g = svg.append('g');

g.attr('clip-path', 'url(#clip-land)')
.selectAll('path')
.data(topojson.feature(congress, congress.objects.districts).features)
.enter().append('path')
.attr('d', path)
.attr('class', function(d) { return getColor(d); });

g.append('path')
.datum(topojson.mesh(congress, congress.objects.districts, function(a, b) { return a !== b && (a.id / 1000 | 0) === (b.id / 1000 | 0); }))
.attr('class', 'district-boundaries')
.attr('d', path);

g.append('path')
.datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))
.attr('class', 'state-boundaries')
.attr('d', path);

// This file cannot be edited:
var output = d3.select(document.body).html().replace(/clippath/g, 'clipPath');

// This file can be edited:
//var output = d3.select(document.body).html();

fs.writeFileSync(fileName, output);

}

Answer

The remaining problem with output2.svg is that the <use> reference in your clip path element wasn't correct.

What I did:

  1. <clippath> -> <clipPath>
  2. href="#land" -> xlink:href="#land"
  3. Add in the xlink namespace to the root element

    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="960" height="500">