Search code examples
javascriptcsssvgd3.js

Maintain SVG <pattern> textures position on a bar chart after shuffling bars?


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.

enter image description here

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>


Solution

  • One of the solutions as follows: use transform attribute to translate the bars rather than set the x 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.

    g.selectAll(".bar")
      .data(data)
      .enter()
      .append("rect")
      .attr("class", "bar")
      .attr("transform", d => `translate(${xScale(d.fruit)}, 0)`) // use transform to translate the bars
      //.attr("x", function (d) {
      //  return xScale(d.fruit);
      //})
      // ...