Learning that only 2 colors are supported in a gradient, was a very early, and very sad surprise in my journey with Graphviz. It took me quite a bit of time to be able to solve it for my environment, but here it finally is.
Most importantly, the approach only works for SVG output. It solves the problem by replacing gradients in a post processing step.
Preparation
When rendering the graph in the dot language, every node is assigned a set of class names correlating to their color contributors. The class names and associated color values are persisted to a file next to the dot code file. Every node also has a unique id.
In my environment, this was already part of an earlier solution to enable other features. So maybe the information could be transferred more easily.
Post-Processing
I decided against parsing the SVG into an AST, due to the extreme size of the graphs I’m dealing with. Instead, the solution works entirely based on text transformations on the plain document.
- First we locate all gradients:
/^<linearGradient .+>/ - For all gradients, we extract their root
id:/id="([^_]+)_l_\d+"/ - The root
idwill point us to the actual node that references the gradient. Something like:<g id="${id}" .+>should match the entire SVG element. - My class names are all uniform, and can simply be found with
/t[a-f0-9]{16}/, but parsing theclassattribute shouldn’t be much of a challenge.
Now we can use the class names of the node to retrieve the color value associated with that class, giving us a list of colors directly correlating with the class names on the node. We store this list in a map with the id of the node as the key.
With these parts, we can construct a new list of <stop /> elements to insert into the gradient. Here is my JavaScript implementation as an example:
let svgOutput = svgInput;
for (const [id, colors] of targets.entries()) {
const gradientRegex = new RegExp(
`<linearGradient id="${id}.+</linearGradient>`,
"gs",
);
svgOutput = svgOutput.replace(gradientRegex, (substring) =>
substring.replace(
/<stop.+?<\/linearGradient>/s,
colors
.map(
(_, index) =>
`<stop offset="${index / (colors.length - 1)}" style="stop-color:${_};stop-opacity:1.;"/>`,
)
.join("\n"),
),
);
}
I went with a very lazy approach for the time being, using more regular expressions, to allow me to easily reuse the full attributes of the existing <linearGradient> element. So this really just replaces the <stop /> elements and nothing else.
