skip to Main Content

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>

2

Answers


  1. Chosen as BEST ANSWER

    I calculated the desired distance of horizontal texture movement based on x attribute value of each bar.

    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, fruits, parameters)   //Initially, we did not shuffle fruits
    
    
    
    //when we click the "Shuffle Bars" butten, the positions of bars change randomly
    document.getElementById('shuffleBars').onclick = function(){
        $('#bar').empty()
        let shuffledFruits = shuffleArray(fruits) //get the shuffled fruits array
        createTextures(d3.select('#bar'), fruits, shuffledFruits, parameters)   //create texture based on the shuffled array
        drawBars(shuffledFruits) //draw bar chart with texture
    }
    
    /** Functions */
    function createTextures(e, fruits, shuffledFruits, parameters){
        //create 3 fruit textures
    
        let newIndexes = saveNewIndexes(fruits, shuffledFruits) //get the indexes of each fruit in the shuffled array
    
        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){
                    //horizontally move textures according to the position of the bar
                    let x = parseFloat(parameters['dotPattern_'+d.fruit+'_X']) + parseFloat(calculateShift(i, newIndexes[i].index))
    
                    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 calculateShift(initial_position, current_position){
        //Calculate the distance of horizontal texture movement
        //position means the index of bar (1st bar, 2nd bar or 3rd bar)
    
        //get value of x attribute of each bar
        let x = [47, 164.7, 282.4]
        let shift = x[current_position] - x[initial_position]
    
        return shift
    }
    function saveNewIndexes(fruits, shuffledFruits) {
        let newIndexes = [];
        for (let i = 0; i < fruits.length; i++) {
            const fruit = fruits[i];
            const shuffledFruit = shuffledFruits.find((f) => f.fruit === fruit.fruit);
            const newIndex = shuffledFruits.indexOf(shuffledFruit);
            newIndexes.push({ fruit: fruit.fruit, index: newIndex });
        }
        return newIndexes;
    }
    
    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. 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);
      //})
      // ...
    
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search