javascript - 根据数据动态设置SVG的宽高

标签 javascript d3.js svg dynamic bootstrap-modal

首先,我使用的是 jQuery-3.2.1 和 bootstrap-3.3.7,无法更改。

我的基本目标是启动引导模式并在此模式中显示动态甘特图。我说动态是因为图表中显示的项目数量以及时间刻度会不时发生变化。我的问题是我不知道如何实现动态方面,最终不得不定义模态中显示的 SVG 的高度和宽度。这意味着如果图表中的项目数量大于 SVG 的高度,这些项目将不会在图表中可见。同样,x 轴以刻度线/网格线之间的最小距离呈现,使得几乎无法阅读图表。

到目前为止我做了什么:

我在以下链接中找到了一张漂亮的图表 D3 v4 Gantt Chart并使用它进行一些小的修改以反转时间刻度,以便它从左到右显示最近到最旧的事件。

我有一个说明问题的 jsfiddle jsFiddle , 请参阅下面的屏幕截图,其中显示了刻度线的狭窄程度以及使用代码中定义的宽度和高度读取时间线的难度

var w = 650;
var h = 450;
var svg = d3.selectAll("#chart")
    .append("svg")
    .attr("width", w)
    .attr("height", h)
    .attr("class", "svg");

Gantt Chart

如何定义 SVG 和模态,以便在最小日期和最大日期之间的差异过大时,x 轴的刻度线/网格线仍然可读?我是否必须检查之间的天数,然后使用它来指定宽度,或者如何实现?与高度类似,我是否必须检查类别的数量并使用此数字来确定需要设置的 SVG 的高度,或者是否有其他方法可以实现此目的?

最后,正如您会在 fiddle 中注意到的那样,悬停在图表中的项目上时的工具提示与它们应该显示的位置完全不一致。我在某处看到有关在 bootstrap.min.js 文件中将 isSVG 设置为 false 的提示,但这没有任何效果。

如有任何帮助,我们将不胜感激 - 此外,如果需要,我们非常乐意进一步阐述,但我希望 fiddle 能够说明问题。预先感谢您的帮助。

最佳答案

在我看来,您想使刻度计数适应宽度,而不是相反(更多内容见下文)。在这种情况下,不要使用时间间隔,如 d3.timeDay,只需设置(近似)滴答数:

axis.ticks(10)

但是,如果实际上你想修改SVG的宽度,你可以计算天数并相应地设置SVG宽度:

var dayWidth = 30;//set the width for each day here
var numberOfDays = d3.timeDay.count(timeScale.domain()[0], timeScale.domain()[1]);//get the total number of days in the data
w = numberOfDays * dayWidth;
svg.attr("width", w);

这只是一个快速代码,让您了解如何做到这一点,更好的解决方案会相应地设置边距(我没有这样做,因为您的代码中有很多神奇的数字)。

关于高度,可以根据task的个数来计算,相应的设置SVG:

h = tasks.length * gap + topPadding + 40;
svg.attr("height", h);

请记住,这里仍然有神奇的数字。尽量避开它们。

下面是修改后的代码:

$(document).ready(function() {

  myObj = JSON.parse('{"QUAL":[{"task": "milk", "type": "Ordered", "startTime": "14/10", "endTime": "16/11"},{"task": "butter", "type": "Completed", "startTime": "22/09", "endTime": "23/09"},{"task": "butter", "type": "Completed", "startTime": "24/09", "endTime": "25/09"},{"task": "bread", "type": "Completed", "startTime": "04/10", "endTime": "15/10"},{"task": "water", "type": "Completed", "startTime": "11/10", "endTime": "16/10"},{"task": "fish", "type": "Discontinued", "startTime": "21/09", "endTime": "23/09"},{"task": "mince", "type": "Discontinued", "startTime": "26/09", "endTime": "27/09"},{"task": "soda", "type": "Discontinued", "startTime": "04/10", "endTime": "08/10"},{"task": "sugar", "type": "Discontinued", "startTime": "04/10", "endTime": "08/10"},{"task": "flour", "type": "Discontinued", "startTime": "09/10", "endTime": "11/10"},{"task": "shampoo", "type": "Discontinued", "startTime": "10/10", "endTime": "11/10"},{"task": "salt", "type": "On Hold", "startTime": "04/10", "endTime": "04/10"}]}')



  myObj2 = JSON.parse('{"QUAL":[{"task": "milk", "type": "Ordered", "startTime": "14/10", "endTime": "16/11"},{"task": "butter", "type": "Completed", "startTime": "22/09", "endTime": "23/09"},{"task": "butter", "type": "Completed", "startTime": "24/09", "endTime": "25/09"},{"task": "bread", "type": "Completed", "startTime": "04/10", "endTime": "15/10"},{"task": "water", "type": "Completed", "startTime": "11/10", "endTime": "16/10"},{"task": "fish", "type": "Discontinued", "startTime": "21/09", "endTime": "23/09"},{"task": "mince", "type": "Discontinued", "startTime": "26/09", "endTime": "27/09"}]}')


  var taskArray = [];


  $('.modal').on('hidden.bs.modal', function() {

    document.getElementById("chart").innerHTML = ""
    taskArray = [];

  });


  $('.modal').on('show.bs.modal', function() {

  });



  $('#btn1').on('click', function() {

    $.each(myObj.QUAL, function(i, j) {
      taskArray.push({
        task: j.task,
        type: j.type,
        startTime: j.startTime,
        endTime: j.endTime
      })
    })
    makeChart();
  });


  $('#btn2').on('click', function() {



    $.each(myObj2.QUAL, function(i, j) {
      taskArray.push({
        task: j.task,
        type: j.type,
        startTime: j.startTime,
        endTime: j.endTime
      })
    })
    makeChart();
  });

  makeChart = function() {


    var w,
      h,
      dayWidth = 30;

    $('#amsModal').modal({
      backdrop: 'static'
    });
    // $('#amsDetailBodyImage').css('display', 'none');

    var svg = d3.selectAll("#chart")
      //.selectAll("svg")
      .append("svg")
      .attr("class", "svg");

    //var dateFormat = d3.timeParse("%Y-%m-%d");
    var dateFormat = d3.timeParse("%d/%m");

    var timeScale = d3.scaleTime()
      .domain([d3.min(taskArray, function(d) {
          return dateFormat(d.startTime);
        }),
        d3.max(taskArray, function(d) {
          return dateFormat(d.endTime);
        })
      ]);

    var numberOfDays = d3.timeDay.count(timeScale.domain()[0], timeScale.domain()[1]);

    w = numberOfDays * dayWidth;

    svg.attr("width", w);

    timeScale.range([w - 150, 0]);

    var categories = new Array();

    for (var i = 0; i < taskArray.length; i++) {
      categories.push(taskArray[i].type);
    }

    var catsUnfiltered = categories; //for vert labels

    categories = checkUnique(categories);

    makeGant(taskArray, w);

    var title = svg.append("text")
      .text("Groceries")
      .attr("x", w / 2)
      .attr("y", 25)
      .attr("text-anchor", "middle")
      .attr("font-size", 18)
      .attr("fill", "#000000");



    function makeGant(tasks, pageWidth) {

      var barHeight = 20;
      var gap = barHeight + 4;
      var topPadding = 75;
      var sidePadding = 75;

      h = tasks.length * gap + topPadding + 40;
      svg.attr("height", h);

      var colorScale = d3.scaleLinear()
        .domain([0, categories.length])
        .range(["#00B9FA", "#F95002"])
        .interpolate(d3.interpolateHcl);

      makeGrid(sidePadding, topPadding, pageWidth, h);
      drawRects(tasks, gap, topPadding, sidePadding, barHeight, colorScale, pageWidth, h);
      vertLabels(gap, topPadding, sidePadding, barHeight, colorScale);


      //$('.modal-button').attr('disabled', true);
    }


    function drawRects(theArray, theGap, theTopPad, theSidePad, theBarHeight, theColorScale, w, h) {

      var bigRects = svg.append("g")
        .selectAll("rect")
        .data(theArray)
        .enter()
        .append("rect")
        .attr("x", 0)
        .attr("y", function(d, i) {
          return i * theGap + theTopPad - 2;
        })
        .attr("width", function(d) {
          return w - theSidePad / 2;
        })
        .attr("height", theGap)
        .attr("stroke", "none")
        .attr("fill", function(d) {
          for (var i = 0; i < categories.length; i++) {
            if (d.type == categories[i]) {
              return d3.rgb(theColorScale(i));
            }
          }
        })
        .attr("opacity", 0.2);


      var rectangles = svg.append('g')
        .selectAll("rect")
        .data(theArray)
        .enter();


      var innerRects = rectangles.append("rect")
        .attr("rx", 3)
        .attr("ry", 3)
        .attr("x", function(d) {
          // return timeScale(dateFormat(d.startTime)) + theSidePad;
          return timeScale(dateFormat(d.endTime)) + theSidePad;
        })
        .attr("y", function(d, i) {
          return i * theGap + theTopPad;
        })
        .attr("width", function(d) {
          //return (timeScale(dateFormat(d.endTime))-timeScale(dateFormat(d.startTime)));
          return (timeScale(dateFormat(d.startTime)) - timeScale(dateFormat(d.endTime)));
        })
        .attr("height", theBarHeight)
        .attr("stroke", "none")
        .attr("fill", function(d) {
          for (var i = 0; i < categories.length; i++) {
            if (d.type == categories[i]) {
              return d3.rgb(theColorScale(i));
            }
          }
        })


      var rectText = rectangles.append("text")
        .text(function(d) {
          return d.task;
        })
        .attr("x", function(d) {
          return (timeScale(dateFormat(d.endTime)) - timeScale(dateFormat(d.startTime))) / 2 + timeScale(dateFormat(d.startTime)) + theSidePad;
        })
        .attr("y", function(d, i) {
          return i * theGap + 14 + theTopPad;
        })
        .attr("font-size", 11)
        .attr("text-anchor", "middle")
        .attr("text-height", theBarHeight)
        .attr("fill", "#000000");


      rectText.on('mouseover', function(e) {
        // console.log(this.x.animVal.getItem(this));
        var tag = "";

        if (d3.select(this).data()[0].details != undefined) {
          tag = "Task: " + d3.select(this).data()[0].task + "<br/>" +
            "Type: " + d3.select(this).data()[0].type + "<br/>" +
            "Starts: " + d3.select(this).data()[0].startTime + "<br/>" +
            "Ends: " + d3.select(this).data()[0].endTime + "<br/>" +
            "Details: " + d3.select(this).data()[0].details;
        } else {
          tag = "Task: " + d3.select(this).data()[0].task + "<br/>" +
            "Type: " + d3.select(this).data()[0].type + "<br/>" +
            "Starts: " + d3.select(this).data()[0].startTime + "<br/>" +
            "Ends: " + d3.select(this).data()[0].endTime;
        }
        var output = document.getElementById("tag");

        var x = this.x.animVal.getItem(this) + "px";
        var y = this.y.animVal.getItem(this) + 25 + "px";

        output.innerHTML = tag;
        output.style.top = y;
        output.style.left = x;
        output.style.display = "block";
      }).on('mouseout', function() {
        var output = document.getElementById("tag");
        output.style.display = "none";
      });


      innerRects.on('mouseover', function(e) {
        //console.log(this);
        var tag = "";

        if (d3.select(this).data()[0].details != undefined) {
          tag = "Task: " + d3.select(this).data()[0].task + "<br/>" +
            "Type: " + d3.select(this).data()[0].type + "<br/>" +
            "Starts: " + d3.select(this).data()[0].startTime + "<br/>" +
            "Ends: " + d3.select(this).data()[0].endTime + "<br/>" +
            "Details: " + d3.select(this).data()[0].details;
        } else {
          tag = "Task: " + d3.select(this).data()[0].task + "<br/>" +
            "Type: " + d3.select(this).data()[0].type + "<br/>" +
            "Starts: " + d3.select(this).data()[0].startTime + "<br/>" +
            "Ends: " + d3.select(this).data()[0].endTime;
        }
        var output = document.getElementById("tag");

        var x = (this.x.animVal.value + this.width.animVal.value / 2) + "px";
        var y = this.y.animVal.value + 25 + "px";

        output.innerHTML = tag;
        output.style.top = y;
        output.style.left = x;
        output.style.display = "block";
      }).on('mouseout', function() {
        var output = document.getElementById("tag");
        output.style.display = "none";

      });
    }


    function makeGrid(theSidePad, theTopPad, w, h) {

      var xAxis = d3.axisBottom(timeScale)
        .ticks(d3.timeDay, 1)
        .tickSize(-h + theTopPad + 20, 0, 0)
        //			.tickFormat(d3.timeFormat('%d %b'));
        .tickFormat(d3.timeFormat('%d/%m'));
      var grid = svg.append('g')
        .attr('class', 'grid')
        //			.attr('transform', 'translate(' +theSidePad + ', ' + (h - 50) + ')')
        .attr('transform', 'translate(' + theSidePad + ', ' + (h - 20) + ')')
        .call(xAxis)
        .selectAll("text")
        .style("text-anchor", "middle")
        .attr("fill", "#000000")
        .attr("stroke", "none")
        .attr("font-size", 10)
        //							.attr("dy", "1em")
        .attr("dy", "0.35em")
        .attr("transform", "rotate(-65)");

    }

    function vertLabels(theGap, theTopPad, theSidePad, theBarHeight, theColorScale) {
      var numOccurances = new Array();
      var prevGap = 0;

      for (var i = 0; i < categories.length; i++) {
        numOccurances[i] = [categories[i], getCount(categories[i], catsUnfiltered)];
      }

      var axisText = svg.append("g") //without doing this, impossible to put grid lines behind text
        .selectAll("text")
        .data(numOccurances)
        .enter()
        .append("text")
        .text(function(d) {
          return d[0];
        })
        .attr("x", 10)
        .attr("y", function(d, i) {
          if (i > 0) {
            for (var j = 0; j < i; j++) {
              prevGap += numOccurances[i - 1][1];
              // console.log(prevGap);
              return d[1] * theGap / 2 + prevGap * theGap + theTopPad;
            }
          } else {
            return d[1] * theGap / 2 + theTopPad;
          }
        })
        .attr("font-size", 11)
        .attr("text-anchor", "start")
        .attr("text-height", 14)
        .attr("fill", function(d) {
          for (var i = 0; i < categories.length; i++) {
            if (d[0] == categories[i]) {
              //  console.log("true!");
              return d3.rgb(theColorScale(i)).darker();
            }
          }
        });

    }

    //from this stackexchange question: http://stackoverflow.com/questions/1890203/unique-for-arrays-in-javascript
    function checkUnique(arr) {
      var hash = {},
        result = [];
      for (var i = 0, l = arr.length; i < l; ++i) {
        if (!hash.hasOwnProperty(arr[i])) { //it works with objects! in FF, at least
          hash[arr[i]] = true;
          result.push(arr[i]);
        }
      }
      return result;
    }

    //from this stackexchange question: http://stackoverflow.com/questions/14227981/count-how-many-strings-in-an-array-have-duplicates-in-the-same-array
    function getCounts(arr) {
      var i = arr.length, // var to loop over
        obj = {}; // obj to store results
      while (i) obj[arr[--i]] = (obj[arr[i]] || 0) + 1; // count occurrences
      return obj;
    }

    // get specific from everything
    function getCount(word, arr) {
      return getCounts(arr)[word] || 0;
    }

    $('#amsModal').modal({
      backdrop: 'static'
    });

  };



});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<!DOCTYPE html>
<html>

<style>
  * {
    margin: 0;
    padding: 0;
  }
  
  #container {
    margin: 0 auto;
    position: relative;
    /*width: 1000px;*/
    overflow: visible;
  }
  
  #chart {
    /*    width: 800px;
    height: 400px;*/
    overflow: scroll;
    /*position: absolute;*/
  }
  
  .grid .tick {
    stroke: lightgrey;
    opacity: 0.3;
    shape-rendering: crispEdges;
  }
  
  .grid path {
    stroke-width: 0;
  }
  
  #tag {
    color: white;
    background: #FA283D;
    width: 150px;
    position: absolute;
    display: none;
    padding: 3px 6px;
    margin-left: -80px;
    font-size: 11px;
  }
  
  #tag:before {
    border: solid transparent;
    content: ' ';
    height: 0;
    left: 50%;
    margin-left: -5px;
    position: absolute;
    width: 0;
    border-width: 10px;
    border-bottom-color: #FA283D;
    top: -20px;
  }
  
  .container-fluid {
    margin-left: 10%;
    margin-right: 10%;
  }
  
  .button {
    max-width: 200px;
  }
  
  .modal-body-detail {
    max-height: calc(100vh - 200px);
    overflow-y: auto;
  }
</style>

<head>
  <title>Meh</title>
</head>

<body>

  <div class='container-fluid'>
    <div class="row">

      <button id="btn1" type="button" class="btn btn-primary">
          Long
        </button>

      <button id="btn2" type="button" class="btn btn-primary">
          Short
        </button>
    </div>


    <div class="modal fade autoModal " id="amsModal" tabindex="-1" role="dialog" aria-labelledby="amsModalLabel" aria-hidden="true">
      <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h4 class="modal-title" id="amsModalLabel">Orders</h4>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                <span aria-hidden="true">&times;</span>
              </button>
          </div>
          <div id="detailBody" class="modal-body ">
            <div id="container">
              <div id="chart"></div>
              <!-- chart -->
              <div id="tag"></div>
              <!-- tooltip on hover -->
            </div>
          </div>


          <div class="modal-footer">
            <button type="button" class="btn btn-secondary modal-button" data-dismiss="modal">Close</button>
          </div>
        </div>
      </div>
    </div>



  </div>

</body>

</html>

关于javascript - 根据数据动态设置SVG的宽高,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58584099/

相关文章:

javascript - 是不可变的 javascript 函数对象的实例

javascript - 为什么我的模态对话框窗口中的 "newsletter sign-up"表单会跳转?

d3.js - 参数 1 的类型不是 'SVGRect'

javascript - nvd3折线图自动设置填充不透明度

javascript - 使用 HTML 输入在 D3 中进行动态过滤

javascript - 添加 View 框时鼠标位置发生变化

html - 将鼠标悬停在 SVG 上可更改颜色

javascript - 在 React 表单中更新输入类型号

html - 使 Bootstrap 图像 (svg) 靠得更近

javascript - 在下一步渲染之前清除 html5 canavas strokeText 中的文本