Динамически установить ширину и высоту SVG на основе данных - PullRequest
1 голос
/ 28 октября 2019

Во-первых, я использую jQuery-3.2.1 и bootstrap-3.3.7 и не могу это изменить.

Моя основная цель - запустить модал начальной загрузки и отобразить динамическую диаграмму Ганта в этомпокадрово. Я называю динамическим, поскольку количество элементов, отображаемых на графике, а также шкала времени могут и будут время от времени различаться. Моя проблема в том, что я не знаю, как добиться динамического аспекта, и в итоге мне нужно определить высоту и ширину SVG, который отображается в модальном режиме. Это означает, что если количество отображаемых элементов превышает высоту SVG, эти элементы не будут видны на диаграмме. Точно так же ось X рендерится с минимальным расстоянием между галочками / линиями сетки, что делает почти невозможным чтение графика.

Что я сделал до сих пор:

Я нашел приятный виддиаграмма по следующей ссылке D3 v4 Диаграмма Ганта и использовала это с некоторыми незначительными изменениями, чтобы повернуть шкалу времени так, чтобы она шла слева направо, отображая самые последние и самые старые события.

У меня есть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, которую нужно установить, или есть другой способ достижения этого?

Наконец, как выКак вы заметите в скрипке, всплывающая подсказка над элементами на графике совсем не совпадает с тем местом, где они должны отображаться. Я где-то видел совет о том, чтобы установить isSVG в false в файле bootstrap.min.js, но это не имело никакого эффекта.

Любая помощь с этим будет высоко цениться - также, более чем рада продолжить в случае необходимости, ноЯ надеюсь, что скрипка проиллюстрирует проблему. Заранее спасибо за помощь.

1 Ответ

1 голос
/ 28 октября 2019

Мне кажется, что вы хотите адаптировать количество тиков к ширине, а не наоборот (подробнее об этом ниже). В этом случае вместо использования интервала, такого как 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);

Это просто быстрый код, чтобы вы могли увидеть, как вы можете это сделать, лучшее решение соответственно устанавливает поля (я несделать это, потому что в вашем коде много магических чисел).

Что касается высоты, вы можете рассчитать ее на основе количества задач и установить 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>
...