Skip to content

Instantly share code, notes, and snippets.

@curran
Last active October 27, 2019 20:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save curran/0ad2eef56811e04f3aa6 to your computer and use it in GitHub Desktop.
Save curran/0ad2eef56811e04f3aa6 to your computer and use it in GitHub Desktop.
Color and Texture with textures.js
license: mit

A test of textures.js that shows one approach for having independent scales for color and texture.

This ended up being more code than expected, because each (color, texture) combination needs to be defined independently. This is because each texture ends up as an SVG def, whose color properties (fill, stroke) cannot be changed per mark. Each (color, texture) pair must exist globally, then be applied to each mark.

<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/textures@1.2.0/dist/textures.js"></script>
<script src="https://unpkg.com/d3@5.9.2/dist/d3.min.js"></script>
<meta charset="utf-8">
<title>Texture Test</title>
</head>
<body>
<div id="example"></div>
<script>
var width = 500,
height = 500,
// An n X n grid of circles will be created.
n = 12,
x = d3.scaleLinear()
.domain([0, n])
.range([0, width]),
y = d3.scaleLinear()
.domain([0, n])
.range([0, height]),
radius = 20,
transitionDuration = 1000,
svg = d3.select("#example").append("svg")
.attr("width", width)
.attr("height", height)
// Center the SVG with respect to default width of bl.ocks.
.style("position", "absolute")
.style("left", 960 / 2 - width / 2),
// Textures need to be generated multiple times,
// once for each color they are paired with.
textureGenerators = [
function(){
return textures.lines().thicker();
},
function(){
return textures.circles().size(5);
},
function(){
return textures.paths().d("squares").size(8);
}
],
// Create a scale that encapsulates texture mappings.
textureScale = d3.scaleOrdinal()
.domain(["X", "Y", "Z"])
.range(textureGenerators),
// Create a scale that encapsulates colors.
// Colors from http://colorbrewer2.org/
colorScale = d3.scaleOrdinal()
.domain(["A", "B", "C"])
.range(["#1b9e77", "#d95f02", "#7570b3"]),
// Create a nested ordinal scale for color and texture.
colorTextureScale = d3.scaleOrdinal()
// The first level is for color.
.domain(colorScale.domain())
.range(colorScale.range().map(function(color){
// The second level is for texture.
return d3.scaleOrdinal()
.domain(textureScale.domain())
.range(textureScale.range().map(function(generateTexture){
// Generate a new texture for each (color, texture) pair.
return colorizeTexture(generateTexture(), color);
}))
}));
// Makes the given texture appear as the given color.
function colorizeTexture(texture, color){
// Use stroke, present on all textures.
var texture = texture.stroke(color);
// Use fill, present only on some textures (e.g. "circles", not "lines").
if(texture.fill){
texture.fill(color);
}
return texture;
}
// Initialize defs for each (texture, color) pair.
colorTextureScale.range().forEach(function(scale){
scale.range().forEach(svg.call, svg);
});
// Initialize the data grid.
var data = [];
for(var i = 0; i < n; i++){
for(var j = 0; j < n; j++){
data.push({
i: i,
j: j,
// "a" corresponds to color.
a: i < n / 3 ? "A" : i < (n * 2 / 3) ? "B" : "C",
// "b" corresponds to texture.
b: j < n / 3 ? "X" : j < (n * 2 / 3) ? "Y" : "Z"
});
}
}
// Create the marks.
var marks = svg.selectAll(".mark")
.data(data)
.enter().append("circle")
// The "mark" class is necessary, because
// selectAll("circle") conflicts with the circle texture.
.attr("class", "mark")
.attr("cx", function(d){ return x(d.i) + radius; })
.attr("cy", function(d){ return y(d.j) + radius; })
// Use the color scale for the stroke around each circle.
.style("stroke", function(d){ return colorScale(d.a); })
// Use the nested texture & color scale to define the texture.
.style("fill", function(d){
return colorTextureScale(d.a)(d.b).url();
});
// Periodically set a random radius on each circle.
function randomizeSize(){
marks.transition().duration(transitionDuration)
.attr("r", function(){ return Math.random() * radius; });
}
randomizeSize();
setInterval(randomizeSize, transitionDuration);
</script>
</body>
</html>
@nighliber
Copy link

This is great! Can you add more potential textures by just adding more to textureGenerators? Looks like it...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment