I created a bar chart consisting of three bars with textures generated based on given parameters. Clicking the "Shuffle Bars" button randomly changes the order of the bars.
However, when the positions of the bars change, the textures shift horizontally relative to the rectangular bar they are on, which is not desired. I want the textures to maintain their relative positions to the rectangles of bars. Here is a diagram, this can be clearly seen in the blue dashed circle.
I understand that the horizontal shift is due to the different positions of the three bars relative to the start point of <pattern>
. However, due to other parts of the project, I have to keep the <def>
in its current position and cannot move it, like place it inside each <rect>
of the bar.
I hope to achieve the desired relative position by controlling the x attribute of <pattern>
based on its position(marked as TODO in the code). It should be like x = parameters['dotPattern'+d.fruit+'_X'] + n
. n should be calculated using a function that takes the initial position and the current position as inputs, such as: n = f(initial_position, current_position). Position means which bar the vegetable is in, e.g., in the initial state, carrot is in the 1st bar (initial position) and then maybe it is moved to 2n bar (current position) in one random shuffle.
Do you know how to calculate this shift value (n)?
My codes:
let fruits = [{ fruit: 'carrot', value:10 },
{ fruit: 'celery', value:30 },
{ fruit: 'corn', value:20 },
]
//draw bar chart
let svg = d3.select("#barchart"),
margin = 200,
width = svg.attr("width") - margin,
height = svg.attr("height") - margin
let xScale = d3.scaleBand().range([0, width]).padding(0.4),
yScale = d3.scaleLinear().range([height, 0]);
let g = svg.append("g")
.attr("transform", "translate(" + 100 + "," + 100 + ")")
.attr("id", "bar")
drawBars(fruits)
//create textures from a given parameters
let parameters = {
"dotPattern_carrot_Rotate":"45",
"dotPattern_carrot_Density":"10",
"dotPattern_carrot_Size":"5",
"dotPattern_carrot_X":"0",
"dotPattern_carrot_Y":"0",
"dotPattern_carrot_Fill":"orange",
"dotPattern_celery_Rotate":"0",
"dotPattern_celery_Density":"40",
"dotPattern_celery_Size":"5",
"dotPattern_celery_SizeMax":"30",
"dotPattern_celery_X":"0",
"dotPattern_celery_Y":"0",
"dotPattern_celery_Fill":"green",
"dotPattern_corn_Rotate":"45",
"dotPattern_corn_Density":"10",
"dotPattern_corn_Size":"3",
"dotPattern_corn_X":"0",
"dotPattern_corn_Y":"0",
"dotPattern_corn_Fill":"gold",
}
createTextures(d3.select('#bar'), fruits, parameters)
//when we click the "Shuffle Bars" butten, the positions of bars change randomly
document.getElementById('shuffleBars').onclick = function(){
$('#bar').empty()
createTextures(d3.select('#bar'), fruits, parameters)
drawBars(shuffleArray(fruits))
}
/** Functions */
function createTextures(e, fruits, parameters){
let defs = e.append('defs')
let dotPattern = defs.selectAll(".dotPattern")
.data(fruits)
.enter()
.append("pattern")
.attr("id", (d, i) => 'dotPattern_'+d.fruit)
.attr("patternUnits", "userSpaceOnUse")
.attr("width", function (d,i){
return parameters['dotPattern_'+d.fruit+'_Density']
})
.attr("height", function (d,i){
return parameters['dotPattern_'+d.fruit+'_Density']
})
.attr('patternTransform', function (d,i){
let x = parameters['dotPattern_'+d.fruit+'_X'] //TODO
let y = parameters['dotPattern_'+d.fruit+'_Y']
let degree = parameters['dotPattern_'+d.fruit+'_Rotate']
return 'translate('+x+','+y+') rotate('+degree+')'
})
dotPattern.append('circle')
.attr('cx', function (d,i){
return 0.5*parameters['dotPattern_'+d.fruit+'_Density']
})
.attr('cy', function (d,i){
return 0.5*parameters['dotPattern_'+d.fruit+'_Density']
})
.attr('r', function (d,i){
return parameters['dotPattern_'+d.fruit+'_Size']
})
.attr('fill', function (d,i){
return parameters['dotPattern_'+d.fruit+'_Fill']
})
}
function drawBars(data){
xScale.domain(data.map(function(d) { return d.fruit; }));
yScale.domain([0, d3.max(data, function(d) { return d.value; })]);
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale));
g.append("g")
.call(d3.axisLeft(yScale).tickFormat(function(d){
return d;
}).ticks(10));
g.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return xScale(d.fruit); })
.attr("y", function(d) { return yScale(d.value); })
.attr("width", xScale.bandwidth())
.attr("height", function(d) { return height - yScale(d.value); })
.attr('stroke', "black")
.attr('stroke-width', '1')
.attr("fill", function(d,i) {
return "url(#dotPattern_" + d.fruit +")"
});
}
function shuffleArray(array) {
//The Fisher-Yates algorithm: shuffle an array and have a truly random distribution of items
let shuffledArray = [...array]
for (let i = shuffledArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = shuffledArray[i];
shuffledArray[i] = shuffledArray[j];
shuffledArray[j] = temp;
}
return shuffledArray
}
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<button id="shuffleBars">Shuffle Bars </button>
<svg id = "barchart" width="600" height="500"></svg>
2
Answers
I calculated the desired distance of horizontal texture movement based on x attribute value of each bar.
One of the solutions as follows: use
transform
attribute to translate the bars rather than set thex
attribute directly.No matter where the bars are, the pattern will assume that the bars are at the position 0 on the x-axis. The pattern will be setted before the bars are translated.