【问题标题】:Trying to make dynamic D3 Chart with Bar and Difference line/value尝试使用条形图和差异线/值制作动态 D3 图表
【发布时间】:2021-05-21 13:48:45
【问题描述】:

关于创建条形图并显示差异值百分比和线条,我已要求以下帮助,以下是链接:

How to create a % difference arrow line with value in a bar chart using D3.js

我创建了这个线程来寻求帮助以使同一图表更具动态性,以下是我的尝试:

  1. 以适合任何分辨率的方式使其动态化,即做出响应(Div 的任何宽度和高度都可以呈现图表)
  2. 尝试做反向差分计算。(目前是计算正向差分计算并显示正向箭头,但反向计算需要反向显示箭头)

(已编辑: 例如:计算基本上会从最后一根柱线开始,然后一直持续到第一根柱线。例如:(6453-7345)/7345 = -12% 和 (6453-5388)/6453 = -17%。)

  1. 尝试通过第一个柱和最后一个柱的值来获取差异,即:

下面是代码:

var barData = [{
    "Time": "2019",
    "Value": 5388
    },
    {
    "Time": "2020",
    "Value": 6453
    },
    {
    "Time": "2021",
    "Value": 4345
    },
    {
    "Time": "2022",
    "Value": 7345
    },
    {
    "Time": "2023",
    "Value": 5345
    }];

    // Consider this width and Height are dynamic for div "graphID" because I am trying to responsive design
    const divWidth = 700,
    divHeight = 700;
    //Adding Margin to Viz Area
    var margin = {top: 30, right: 50, bottom: 0, left: 50},
    width = parseInt(divWidth,10) - margin.left - margin.right,
    height = parseInt(divHeight,10) - margin.top - margin.bottom;
    
    //To add svg in the visualization node i.e Dome node                    
    const svg = d3.select("#graphID").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    
    //Adding x axis width i.e based on Viz Width                    
    const xScale = d3.scaleBand()
    .domain(barData.map(d => d.Time))
    .range([0, width]);


    const xAxis = d3.axisBottom(xScale);
    
    //Adding g attribute to svg for x axis
    svg.append('g')
    //.attr('transform', 'translate(20,170)')
    .attr("transform", "translate(10," + (height - 50) + ")") 
    .call(xAxis);
    
                        
    /*
    //To get the Max value from an json object
    const maxVal = barData.reduce((acc, shot) => acc = acc > shot.Value ? acc : shot.Value, 0);
    const valMax = Math.max.apply(barData, barData.map(function(o) { return o.Value; }));
    alert(valMax);
    
    const yScale = d3.scaleLinear()
    .domain([0, maxVal+(maxVal/2)])
    .range([150, 0]);
    */
    
    const yAxisMax = Math.max.apply(barData, barData.map(function(o) { return o.Value; }));
    
    const yScale = d3.scaleLinear()
    .domain([0, yAxisMax+(yAxisMax/2)])
    .range([height, 0]);

    const yAxis = d3.axisLeft(yScale).ticks(4);
    
    svg.append('g')
    //.attr('transform', 'translate(50,170)')
    .attr("transform", "translate(10,-50)") 
    //.attr("transform", "translate(20," + (height - 50) + ")") 
    .call(yAxis);

    const bars = svg.selectAll('g.bar')
    .data(barData)
    .enter()
    .append('g')
    .classed('bar', true)
    .attr('transform', d => `translate(${xScale(d.Time) + 50 + xScale.bandwidth() / 20}, 170)`)

    bars.append('rect')
    .attr('x', -10)
    .attr('width', 40)
    .attr('y', d =>  -height + yScale(d.Value))
    .attr('height', d => yScale(d.Value) )
    .style('fill', 'blue')

    bars.append('text')
    .text(d => d.Value)
    .attr('text-anchor', 'middle')
    .attr('y', d => -(height - 55) + yScale(d.Value))

    bars.filter((d, i) => i < barData.length - 1)
    .append('path')
    .attr('d', (d, i) => `M 5,${-170 + yScale(d.Value)} V ${-210 + yScale(d.Value)} H ${xScale.bandwidth() - 5} V ${-180 + yScale(barData[i + 1].Value)}`)
    .style('stroke', 'gray')
    .style('fill', 'none')
    .attr('marker-end', 'url(#arrowhead)')

    bars.filter((d, i) => i < barData.length - 1)
    .append('rect')
    .attr('x', 15)
    .attr('y', d => -220 + yScale(d.Value))
    .attr('width', xScale.bandwidth() - 30)
    .attr('height', 20)
    .attr('rx', 10)
    .style('fill', 'white')
    .style('stroke', 'gray');

    bars.filter((d, i) => i < barData.length - 1)
    .append('text')
    .text((d, i) => `${barData[i + 1].Value > d.Value ? '+' : '-'}${Math.round((barData[i + 1].Value / d.Value * 100) - 100)}%`)
    .attr('x', xScale.bandwidth() / 2)
    .attr('y', d => -207 + yScale(d.Value))
    .attr('text-anchor', 'middle')
    .style('fill', 'black');
#graphID
{
    width:500px;
    height:700px
}
text {
  font-size: 12px;
  font-family: "Ubuntu";
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="graphID" width=500 height=700>

</div>

【问题讨论】:

  • 我可以理解需要响应大小 (1) 以及第一个和最后一个 (3) 之间的差异。我不明白的是问题2(反向差异计算)。你如何定义哪一对值应该显示反向差异?
  • 好的,对于问题 2,计算基本上是从最后一根柱线开始,然后一直持续到第一根柱线。例如:(6453-7345)/7345 = -12% 和 (6453-5388)/6453 = -17%。我将编辑帖子并在其中添加相同的内容。我希望这现在很清楚。如果没有,请告诉我。 :)
  • 我了解如何计算,我不了解如何显示两个方向的变化......我也认为一次完成所有问题需要大量编码。我建议将问题分为两个步骤(一个用于响应性,第二个用于差异计算),否则将很难管理...
  • 是的,明白,这就是为什么我一开始就试图让它响应。我只分享了第一个问题相关的代码。其他问题将在稍后讨论。您能否帮助我进行响应式实施。我在努力,但在挣扎。您的帮助将不胜感激。

标签: javascript html css d3.js


【解决方案1】:

这里是响应式大小的解决方案:

  1. 获取父容器的尺寸:
  const container = d3.select('#graph');
  const divWidth = parseInt(container.style('width'));
  const divHeight = parseInt(container.style('height'));
  1. 使用 DIV 尺寸创建 SVG:
const svg = container.append("svg")
 .attr("width", divWidth)
 .attr("height", divHeight);
  1. 使用边距计算图表大小:
const margin = {top: 30, right: 50, bottom: 50, left: 50};
const width = divWidth - margin.left - margin.right;
const height = divHeight - margin.top - margin.bottom;
  1. 添加父 &lt;g&gt; 并使用边距定位它。所有其他元素应放在:
const svgG = svg.append("g")
   .attr("transform", `translate(${margin.left},${margin.top})`);

    var barData = [{
        "Time": "2019",
        "Value": 5388
        },
        {
        "Time": "2020",
        "Value": 6453
        },
        {
        "Time": "2021",
        "Value": 4345
        },
        {
        "Time": "2022",
        "Value": 7345
        },
        {
        "Time": "2023",
        "Value": 5345
        }];

    const container = d3.select('#graph');
  const divWidth = parseInt(container.style('width'));
    const divHeight = parseInt(container.style('height'));

// Consider this width and Height are dynamic for div "graphID" because I am trying to responsive design
    const margin = {top: 30, right: 50, bottom: 50, left: 50};
  const width = divWidth - margin.left - margin.right;
  const height = divHeight - margin.top - margin.bottom;
        
        //To add svg in the visualization node i.e Dome node                    
 const svg = container.append("svg")
   .attr("width", divWidth)
   .attr("height", divHeight);
      
 const svgG = svg.append("g")
   .attr("transform", `translate(${margin.left},${margin.top})`);
        
const xScale = d3.scaleBand()
  .domain(barData.map(d => d.Time))
  .range([0, width]);

const xAxis = d3.axisBottom(xScale);
        
//Adding g attribute to svg for x axis
svgG.append('g')
    .attr("transform", `translate(0,${height})`) 
    .call(xAxis);
        
const yAxisMax = barData.reduce((max, item) => Math.max(max, item.Value), 0) * 1.5;
        
const yScale = d3.scaleLinear()
    .domain([0, yAxisMax])
    .range([height, 0]);

const yAxis = d3.axisLeft(yScale).ticks(4);
        
svgG.append('g')
    .call(yAxis);

const bars = svgG.selectAll('g.bar')
    .data(barData)
    .enter()
    .append('g')
  .classed('bar', true)
  .attr('transform', d => `translate(${xScale(d.Time) + xScale.bandwidth() / 2}, 0)`)

bars.append('rect')
    .attr('x', -20)
    .attr('width', 40)
    .attr('y', d =>  yScale(d.Value))
    .attr('height', d => height - yScale(d.Value) )
    .style('fill', 'blue')

bars.append('text')
    .text(d => d.Value)
    .attr('text-anchor', 'middle')
    .attr('y', d => yScale(d.Value))
  .attr('dy', -5)

bars.filter((d, i) => i < barData.length - 1)
    .append('path')
    .attr('d', (d, i) => `M 5,${yScale(d.Value) - 20} V ${Math.min(yScale(d.Value), yScale(barData[i + 1].Value)) - 60} H ${xScale.bandwidth() - 5} V ${yScale(barData[i + 1].Value) - 20}`)
        .style('stroke', 'gray')
        .style('fill', 'none')
        .attr('marker-end', 'url(#arrowhead)')

bars.filter((d, i) => i < barData.length - 1)
  .append('rect')
  .attr('x', 15)
  .attr('y', (d, i) => Math.min(yScale(d.Value), yScale(barData[i + 1].Value)) - 70)
  .attr('width', xScale.bandwidth() - 30)
  .attr('height', 20)
  .attr('rx', 10)
  .style('fill', 'white')
  .style('stroke', 'gray');

bars.filter((d, i) => i < barData.length - 1)
    .append('text')
    .text((d, i) => `${barData[i + 1].Value > d.Value ? '+' : '-'}${Math.round((barData[i + 1].Value / d.Value * 100) - 100)}%`)
  .attr('x', xScale.bandwidth() / 2)
  .attr('y', (d, i) => Math.min(yScale(d.Value), yScale(barData[i + 1].Value)) - 56)
  .attr('text-anchor', 'middle')
  .style('fill', 'black');
#graph
{
  width:500px;
  height:700px
 }
 
 text {
   font-size: 12px;
   font-family: "Ubuntu";
  }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="graph">
</div>

【讨论】:

  • 谢谢你,迈克尔,你太棒了。它真的对我理解和开始有很大帮助。我将尝试解决第二个和第三个问题,如果我需要任何帮助或卡在任何地方,那么将为此提出一个单独的线程。非常感谢:)
  • 只是想知道,制作同一张图表在水平方向上工作是否简单?还是代码有很多变化?
  • 没有差异标签会相对容易:您只需切换 Y 和 X 刻度 / 轴并将 yheight 替换为 xwidth 用于条形。有了标签就更难了,因为路径计算是基于布局的...
  • 嗨迈克尔,我明白了,也许我稍后会检查水平属性,但需要在下面提供帮助 --> 我正在尝试使用 barData 数组中的一些 -ve 值,但它没有按预期工作因为 y 轴默认从“0”开始,但是当我使用“const yAxisMin = barData.reduce((min, item) => Math.min(min, item.Value), 0) 通过动态最小值设置默认值时* 1.5;",那么它也没有按预期工作。是否需要进行其他更改?您能否通过传递 -ve 值来更新您的共享代码。你的帮助可观。 :)
  • 我不确定我是否理解问题所在。什么是 -ve 值?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-04-24
  • 2014-02-28
  • 1970-01-01
  • 2017-12-25
相关资源
最近更新 更多