洗牌后保持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,让纹理起始点对齐柱状条左上角。
具体步骤
- 保存初始位置信息
在初始化阶段,记录每个水果对应的初始柱状条x坐标:
// 初始化时保存每个水果的初始位置 const initialPositions = {}; xScale.domain(fruits.map(f => f.fruit)); fruits.forEach(d => { initialPositions[d.fruit] = xScale(d.fruit); });
- 修改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+')'; })
- 调整洗牌流程
洗牌时先更新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
相关产品推荐
相关产品推荐

