d3JS: отображать версию данных с меньшей плотностью для большого набора данных при уменьшении масштаба на линейной/областной диаграмме.

Я создаю диаграмму, аналогичную Майку Бостоку с масштабируемой диаграммой с областями. .

Для моего конкретного проекта у меня есть куча датчиков, которые записывают значения каждые 30 секунд (температура, свет, влажность и звук). У меня работает реализация масштабирования, однако, когда я уменьшаю масштаб, скажем, до года, плотность диаграммы замедляет работу браузера, а графика также не читается.

Как я могу отредактировать скрипт, чтобы плотность линейного графика менялась относительно величины масштабирования? Другими словами, область x контролирует количество точек на линии значений. Я хотел бы иметь полную плотность (запись каждые 30 секунд), когда я увеличиваю масштаб до одного часа, и я хотел бы гораздо более низкую плотность (запись каждый день), когда я уменьшаю масштаб. Любые идеи? Реализация со сценарием по ссылке выше была бы полезна.

Спасибо!

    <!DOCTYPE html>
    <html>
      <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
        <script type="text/javascript" src="d3/d3.js"></script>
        <script type="text/javascript" src="d3/d3.csv.js"></script>
        <script type="text/javascript" src="d3/d3.time.js"></script>
        <link type="text/css" rel="stylesheet" href="style.css"/>
        <style type="text/css">

    svg {
      font-size: 10px;
    }

    .axis {
      shape-rendering: crispEdges;
    }

    .axis path, .axis line {
      fill: none;
      stroke-width: .5px;
    }

    .x.axis path {
      stroke: #000;
    }

    .x.axis line {
      stroke: #fff;
      stroke-opacity: .5;
    }

    .y.axis line {
      stroke: #ddd;
    }

    path.line {
      fill: none;
      stroke: #000;
      stroke-width: .5px;
    }

    rect.pane {
      cursor: move;
      fill: none;
      pointer-events: all;
    }

        </style>
      </head>
      <body>
        <div id="body">
          <div id="footer">
            <span>…</span>
            <div class="hint">mousewheel to zoom, drag to pan</div>
          </div>
        </div>
        <script type="text/javascript">

    var m = [79, 80, 160, 79],
        w = 1280 - m[1] - m[3],
        h = 800 - m[0] - m[2],
        parse = d3.time.format("%Y-%m-%d").parse,
        format = d3.time.format("%Y");

    // Scales. Note the inverted domain for the y-scale: bigger is up!
    var x = d3.time.scale().range([0, w]),
        y = d3.scale.linear().range([h, 0]),
        xAxis = d3.svg.axis().scale(x).orient("bottom").tickSize(-h, 0).tickPadding(6),
        yAxis = d3.svg.axis().scale(y).orient("right").tickSize(-w).tickPadding(6);

    // An area generator.
    var area = d3.svg.area()
        .interpolate("step-after")
        .x(function(d) { return x(d.date); })
        .y0(y(0))
        .y1(function(d) { return y(d.value); });

    // A line generator.
    var line = d3.svg.line()
        .interpolate("step-after")
        .x(function(d) { return x(d.date); })
        .y(function(d) { return y(d.value); });

    var svg = d3.select("body").append("svg:svg")
        .attr("width", w + m[1] + m[3])
        .attr("height", h + m[0] + m[2])
      .append("svg:g")
        .attr("transform", "translate(" + m[3] + "," + m[0] + ")");

    var gradient = svg.append("svg:defs").append("svg:linearGradient")
        .attr("id", "gradient")
        .attr("x2", "0%")
        .attr("y2", "100%");

    gradient.append("svg:stop")
        .attr("offset", "0%")
        .attr("stop-color", "#fff")
        .attr("stop-opacity", .5);

    gradient.append("svg:stop")
        .attr("offset", "100%")
        .attr("stop-color", "#999")
        .attr("stop-opacity", 1);

    svg.append("svg:clipPath")
        .attr("id", "clip")
      .append("svg:rect")
        .attr("x", x(0))
        .attr("y", y(1))
        .attr("width", x(1) - x(0))
        .attr("height", y(0) - y(1));

    svg.append("svg:g")
        .attr("class", "y axis")
        .attr("transform", "translate(" + w + ",0)");

    svg.append("svg:path")
        .attr("class", "area")
        .attr("clip-path", "url(#clip)")
        .style("fill", "url(#gradient)");

    svg.append("svg:g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + h + ")");

    svg.append("svg:path")
        .attr("class", "line")
        .attr("clip-path", "url(#clip)");

    svg.append("svg:rect")
        .attr("class", "pane")
        .attr("width", w)
        .attr("height", h)
        .call(d3.behavior.zoom().on("zoom", zoom));

    d3.csv("flights-departed.csv", function(data) {

      // Parse dates and numbers.
      data.forEach(function(d) {
        d.date = parse(d.date);
        d.value = +d.value;
      });

      // Compute the maximum price.
      x.domain([new Date(1999, 0, 1), new Date(2003, 0, 0)]);
      y.domain([0, d3.max(data, function(d) { return d.value; })]);

      // Bind the data to our path elements.
      svg.select("path.area").data([data]);
      svg.select("path.line").data([data]);

      draw();
    });

    function draw() {
      svg.select("g.x.axis").call(xAxis);
      svg.select("g.y.axis").call(yAxis);
      svg.select("path.area").attr("d", area);
      svg.select("path.line").attr("d", line);
      d3.select("#footer span").text("U.S. Commercial Flights, " + x.domain().map(format).join("-"));
    }

    function zoom() {
      d3.event.transform(x); // TODO d3.behavior.zoom should support extents
      draw();
    }

        </script>
      </body>
    </html>

person ekatz    schedule 10.01.2014    source источник
comment
На самом деле это не было бы тривиальной задачей, поскольку вам пришлось бы изменить способ преобразования базовых данных в элементы DOM. В частности, вам придется уменьшить количество представленных точек данных в зависимости от уровня масштабирования.   -  person Lars Kotthoff    schedule 10.01.2014
comment
@LarsKotthoff прав, это не тривиально. Я делаю в значительной степени это, и я слушаю события масштабирования и пересчитываю данные, которые я передаю на диаграмму, в зависимости от масштаба масштабирования. Имейте в виду, что «панорамирование» регистрируется как масштабирование с масштабом «1». Также имейте в виду, что масштабирование d3 просто увеличивает векторный рисунок, а НЕ масштабирует фактические данные.   -  person ari gold    schedule 11.01.2014
comment
Просто чтобы ответить на комментарии @LarsKotthoff: поскольку это линейная/областная диаграмма, проще изменить набор данных на лету, поскольку для всего набора данных всегда есть только один объект DOM. В других типах диаграмм вам нужно было бы вводить и выходить из объектов DOM, когда вы меняли точность набора данных, что значительно замедляло работу (хотя, возможно, все же лучше, чем пытаться отобразить все сразу!).   -  person AmeliaBR    schedule 12.01.2014


Ответы (2)


Ларс и Ари были правы, это определенно не тривиальная проблема. Но я подумал, что это важно, пригодится многим людям (включая, вероятно, и меня в будущем), и поэтому стоит потратить время на то, чтобы разобраться.

Итак, вы можете следовать дальше, вот моя адаптация графика ежедневного количества полетов Майка Бостока, так что он отображает недельное / ежемесячное / годовое среднее ежедневное количество рейсов при уменьшении масштаба (вместо отдельных дней) и отображает только подмножество данных, которые могут отображаться при любом уровне масштабирования:
https://jsfiddle.net/ncy5J/2/

Вот пошаговая разбивка того, что мне нужно было сделать:

  1. Получите очень большую таблицу данных csv, работающую как встроенная переменная в скрипте JSFiddle. Я предполагаю, что вы не будете делать это таким образом, но я упоминаю об этом, потому что это было хлопотно. Пришлось добавить \n\ в конце каждой строки, прежде чем я мог запустить d3.csv.parse() в строке.

  2. Создайте альтернативные массивы данных для недель, месяцев и лет и рассчитайте среднесуточные значения для этих периодов времени:

    • Используйте d3.nest с ключевой функцией, которая использует d3 interval.floor() функции для объединения всех дат одного и того же года, месяца и т. д.;

    • Используйте Array.forEach во вложенных массивах с пользовательской функцией, чтобы получить доступ к массиву вложенных объектов, вычислить среднее значение их значений, а затем заменить объект, созданный nest(), на объект, соответствующий формату исходных данных (код ниже).

  3. Переместите шаг привязки данных с инициализации на функцию повторного рисования и измените эту функцию, чтобы она принимала массив данных в качестве параметра.

  4. Обновите методы d3.behavior.zoom, чтобы они соответствовали API D3 версии 3 (в исходном примере использовалась версия 2.4, в которой были другие методы для привязки поведения масштабирования к масштабу).

  5. Измените функцию zoom, вызываемую поведением масштабирования, на

    • Доступ к видимой области данных по шкале x (которая автоматически обновляется при изменении масштаба);

    • Рассчитайте промежуток времени, охватываемый этим доменом;

    • Выберите один из моих четырех массивов данных, который будет иметь соответствующий уровень точности для этого диапазона;

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

    • Вызовите функцию перерисовки с соответствующим фрагментом массива в качестве переданных данных.

Вот пользовательская процедура вложения/усреднения из шага 2:

AllData.yearly = d3.nest().key(function(d){
                    return d3.time.year.floor(d.date);
                })
                .entries(data);
AllData.yearly.forEach(meanDaily);

function meanDaily(nestedObject, i, array){
    //This function is passed to the Array.forEach() method
    //which passes in:
    // (1) the element in the array
    // (2) the element's index, and
    // (3) the array as a whole

    //It replaces the element in the array 
    //(an object with properties key and values, where
    // values is an array of nested objects)
    //with a single object that has the nesting key
    //value parsed back into a date, 
    //and the mean of the nested values' value
    //as the value.  It makes sense, I swear.
    array[i] = {date:new Date(nestedObject.key), 
            value:d3.mean(nestedObject.values, 
                          function(d) {return d.value;}
                         )};
}

Метод масштабирования — это просто базовый javascript, ключевая часть которого заключается в том, что вы можете получить доступ к видимому домену непосредственно из x-шкалы, а затем использовать его, чтобы выяснить, какие точки данных передать функции рисования.

P.S. Интересно посмотреть на данные в разных средних масштабах. Резкий спад количества рейсов после 11 сентября выделяется на дневных, недельных и месячных графиках, но исчезает из среднегодовых показателей. Вместо этого среднегодовые показатели показывают, что в 2002 году в целом было меньше среднесуточных рейсов, чем в 2001 году, что напоминает о том, что многие люди боялись летать еще долго после того, как запрет на полеты был снят.

person AmeliaBR    schedule 11.01.2014
comment
P.S. Просто вернулся и перечитал вопрос; конечно, если вы заинтересованы только в графическом отображении первого показания в час/день, а не среднего значения, вы должны пропустить шаг 2 из моей процедуры и изменить шаг 5, чтобы создать соответствующим образом отфильтрованный массив. Если ваши данные в реальном времени, то это может быть проще, чем обновлять все массивы средних значений! - person AmeliaBR; 12.01.2014
comment
У меня аналогичный случай, у меня есть данные о температуре за месяц (точки данных каждые 8 ​​минут, т.е. ~ 5000 точек), хранящиеся в массиве. Я использую масштабирование для уменьшения данных (загрузка каждого n-го элемента через цикл for), я не хочу, чтобы разрешение данных было меньше highResArray.lenth/10. Я задал zoomScaleExtent(1,2000), но масштабирование с помощью колеса не является линейным, поэтому, когда я линейно сопоставляю масштаб масштабирования(2000,1) с коэффициентом уменьшения (1,10) ("1" означает: взять каждый элемент массив высокого разрешения, '10' означает: взять каждый 10-й элемент массива высокого разрешения) коэффициент уменьшения 10 долгое время переходит в 1 только в 2000 году - person Ahmad Karim; 12.06.2019
comment
Кроме того, скрипки не существует. - person Ahmad Karim; 12.06.2019
comment
Обновлена ​​ссылка на скрипку. Если вы увидите больше таких неработающих ссылок в будущем, просто измените домен fiddle.jshell.net на jsfiddle.net. Что касается вашей проблемы с масштабированием, @AhmadKarim, вы, вероятно, захотите передать значения, полученные из события масштабирования, через функцию масштабирования. - person AmeliaBR; 13.06.2019
comment
Спасибо за обновленную ссылку, я использую шкалу (линейную), но не знаю, как колесико мыши связано с масштабом масштабирования. Что я знаю точно, так это то, что это не линейная шкала. В любом случае, я изменил область своего масштаба отображения на меньшее значение, чтобы коэффициент уменьшения менялся раньше, а не когда диаграмма увеличивается до конца. - person Ahmad Karim; 13.06.2019

Вот обновленное решение для D3 v6, работающее в ObservableHQ (и основанное на чрезвычайно полезной работе в более раннем ответе AmeliaBR):

Весь код и пояснения смотрите в блокноте ObservableHQ. Несколько ключевых частей, которые могут быть полезны…

Чтобы выбрать наилучшую плотность данных для текущего уровня масштабирования, вы можете узнать ширину отдельного дня (в пикселях экрана) по увеличенной шкале x, а затем выбрать наибольшую плотность, при которой одна единица времени (день/месяц/неделя /год) останется уже определенной ширины в пикселях:

// Return largest of "daily", "weekly", "monthly" or "yearly"
// where a single time unit stays narrower than `thresholdPixels`
// on d3 time scale `x`
function bestDensityForScale(x, thresholdPixels=5) {
  const dayWidth = x(Date.UTC(2000, 0, 2)) - x(Date.UTC(2000, 0, 1));
  const chooseDensity = d3.scaleThreshold()
    .domain([dayWidth * 7, dayWidth * 30, dayWidth * 365])
    .range(["daily", "weekly", "monthly", "yearly"]);
  return chooseDensity(thresholdPixels);
}

Вы можете настроить thresholdPixels, чтобы получить желаемый эффект. Думайте об этом как о максимальной ширине плоского участка на графике с низкой плотностью, прежде чем он будет заменен данными с более высокой плотностью. Например, если размер одного месяца при текущем масштабе превышает 5 пикселей, переключитесь на данные за неделю. И если одна неделя будет охватывать 5 пикселей, переключитесь на ежедневно.

Чтобы вычислить среднесуточные данные за большие периоды времени, полезно использовать d3.rollups:

const weekly = d3.rollups(
  data,
  v => d3.mean(v, d => d.value),
  d => d3.timeWeek.floor(d.date)
).map(([date, value]) => ({date, value}));

(map в конце изменяет массивы, возвращаемые rollups['2010-01-01', 197062], обратно в тот же формат объекта, что и ежедневные данные — {date: '2010-01-01', value: 197062}.)

person medmunds    schedule 20.04.2021