You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

洗牌后保持SVG <pattern>纹理在柱状图上的相对位置

问题描述

我制作了一个包含三根柱状条的图表,柱状条的纹理基于给定参数生成。点击「Shuffle Bars」按钮可随机打乱柱状条顺序,但洗牌后纹理会相对于所在柱状条发生水平偏移,不符合预期——我希望纹理能保持与柱状条矩形的相对位置。

我清楚偏移源于三根柱状条相对于<pattern>起始点的位置差异,但受项目限制,必须保留<defs>当前位置,不能将其放入每个柱状条的<rect>标签内。

我计划通过柱状条位置控制<pattern>的x属性实现预期效果,公式为x = parameters['dotPattern'+d.fruit+'_X'] + n,其中n需要根据初始位置和当前位置计算(位置指柱状条的序号,比如初始时胡萝卜在第1位,洗牌后可能移至第2位)。请问该如何计算这个偏移值n?


代码实现

HTML

<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>

JavaScript

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
    }

解决方案

核心思路是记录每个水果的初始位置偏移,洗牌后根据当前位置与初始位置的差值计算补偿偏移量n,让纹理起始点对齐柱状条左上角。

具体步骤

  1. 保存初始位置信息
    在初始化阶段,记录每个水果对应的初始柱状条x坐标:
// 初始化时保存每个水果的初始位置
const initialPositions = {};
xScale.domain(fruits.map(f => f.fruit));
fruits.forEach(d => {
  initialPositions[d.fruit] = xScale(d.fruit);
});
  1. 修改pattern的偏移计算逻辑
    在createTextures函数中,计算当前位置与初始位置的差值作为n,补偿纹理偏移:
.attr('patternTransform', function (d,i){
  // 获取当前柱状条的x坐标
  const currentX = xScale(d.fruit);
  // 计算偏移量n:用初始位置抵消当前位置的变化
  const n = initialPositions[d.fruit] - currentX;
  let x = parseInt(parameters['dotPattern_'+d.fruit+'_X']) + n;
  let y = parameters['dotPattern_'+d.fruit+'_Y'];
  let degree = parameters['dotPattern_'+d.fruit+'_Rotate'];

  return 'translate('+x+','+y+') rotate('+degree+')';
})
  1. 调整洗牌流程
    洗牌时先更新xScale的domain,确保能正确获取当前位置:
document.getElementById('shuffleBars').onclick = function(){
    $('#bar').empty();
    const shuffled = shuffleArray(fruits);
    xScale.domain(shuffled.map(d => d.fruit)); // 先更新坐标映射
    createTextures(d3.select('#bar'), shuffled, parameters);   
    drawBars(shuffled);
}

原理

因为patternUnits设为userSpaceOnUse,pattern的坐标基于整个SVG用户空间。当柱状条位置变化时,通过translate的x偏移抵消位置差,让纹理起始点始终对应柱状条左上角,从而保持纹理与柱状条的相对位置不变。


内容的提问来源于stack exchange,提问作者Hesper

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 16:09:54