Кисть и масштабирование с помощью D3.js и холста

Создание диаграмм, масштабируемых с помощью D3.js и Canvas (часть 3)

Автор Максимилиано Дути, разработчик Full Stack @ XOOR

Добро пожаловать обратно! Мы очень рады вернуться с новой блестящей публикацией из нашей серии. Мы продолжим работу над тем, что мы сделали до сих пор, поэтому, если вы каким-то образом оказались здесь, я рекомендую вам перейти к нашим первым двум сообщениям, а затем вернуться сюда:

Наша цель в этом третьем посте - добавить более крутой вариант масштабирования, при котором вы можете нарисовать рамку с помощью курсора мыши и увеличить непосредственно область внутри этого поля. Обычно это называется «Кисть и масштабирование».

Добавление некоторых элементов управления

Первым делом мы добавим на график новые кнопки. У нас уже есть несколько встроенных функций, и управлять ими будет легче, если мы сможем активировать / деактивировать их одним нажатием кнопки. Итак, в наш файл index.html мы добавим следующий код:

И мы добавляем следующие стили в наш styles.css:

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

Здесь начинается самое интересное

Пора оживить эти кнопки. Давайте перейдем к основному файлу JS и в конце добавим следующее (как всегда, пояснение после кода):

Первым делом мы получаем ссылку на нашу SVG-часть диаграммы. Помните, что мы используем SVG для рисования таких вещей, как оси и метки осей, и у нас есть холст ниже, где мы рисуем фактические точки диаграммы рассеяния. Логика здесь в том, что некоторые функции должны запускать события в SVG, а другие должны запускать события на холсте. Поэтому мы немного поиграем с SVG и свойством z-index холста, чтобы это произошло. Когда пользователь нажимает кнопку масштабирования, мы хотим, чтобы это происходило на холсте, как вы уже узнали из предыдущего поста. Но когда пользователь выбирает параметр кисти, мы хотим, чтобы SVG помещался поверх холста. Это позволит нам получить доступ к серии событий, которые предоставляет SVG, и которые будут полезны для реализации функции кисти.

Как видите, мы создаем две переменные, которые будут содержать обработчики кнопок масштабирования и кисти. И внутри обработчиков мы выполняем базовое назначение классов css, чтобы отразить текущую активную кнопку, а также делаем этот небольшой трюк с z-index, помещая холст поверх SVG (или наоборот), пока масштабирование или кисть функции активны.

Пришло время определить функциональность кисти. Для начала добавим в файл plot.js следующий фрагмент кода:

Первое, что мы делаем, это создаем переменную кисти с помощью d3-brush. Эта функция будет запускаться в пределах очищаемой области, определенной переданным экстентом, в нашем случае от начала координат (0,0) в верхнем левом углу до (ширина, высота) нашего диаграмму в правом нижнем углу. Затем мы назначаем несколько прослушивателей событий очищаемой области с помощью функции brush.on (event, handler).
Как вы уже могли представить, событие start будет запущено как только пользователь запускает действие кисти, событие кисть запускается, когда пользователь перетаскивает курсор мыши, и, наконец, когда пользователь отпускает кнопку мыши, запускается событие конец. Этот последний обработчик событий будет тем, который получит информацию о кисти, сгенерирует преобразование и применит масштабирование к выбранной области. Скоро мы реализуем эти функции-обработчики.

Обработчик события start.nokey необходим, потому что мы не хотим использовать клавиши пробела, alt и shift в событии кисти, поскольку мы будем создавать кисть, которая автоматически сохраняет соотношение сторон. Не стесняйтесь экспериментировать, добавляя эти функции обратно :)

Наконец, мы добавляем новую группу в SVG, которая будет содержать очищаемую область.

Обработчики событий кисти

Пора кодировать обработчики событий! Начнем с «стартового» события:

Мы определяем переменную с именем brushStartPoint. Мы сохраним в этой переменной координаты начала события кисти (это место, где пользователь щелкнул). Как вы можете видеть, мы обрабатываем только событие «mousedown», и что мы делаем, так это сохраняем как положение окна x / y, так и положение выбора x / y в SVG. Эта информация будет использована для расчета области выбора позже.

Теперь начнется интересное. Нам нужно реализовать обработчик событий «кисть». Этот довольно сложный, поэтому мы рассмотрим его по частям:

Первым делом мы определяем несколько переменных:

  • масштаб: сохраняет соотношение между шириной и высотой диаграммы
  • sourceEvent: хранит информацию о событии
  • мышь: сохраняет положение мыши (в окне) при запуске события

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

Затем мы определяем еще три переменные:

  • расстояние: сохранит расстояние по вертикальной оси. Обратите внимание: если вы хотите сохранить расстояние по оси X, вам придется рассчитать масштаб, выполнив высоту / ширину вместо ширины / высоты, как мы.
  • yPosition: это координата оси Y, до которой было нарисовано наше поле (допускаются отрицательные значения)
  • xCorMulti: сохранит значение коррекции для координаты оси X

Через некоторое время все это обретет смысл, наберитесь терпения!

Сохранение соотношения сторон коробки

Итак, проблема с масштабированием прямоугольника заключается в том, что мы хотим сохранить значения шкалы как по осям X, так и по Y. Если мы нарисуем прямоугольник и наш график будет иметь квадратную форму, мы потеряем соотношение между осями X и Y, и при увеличении масштаба наши данные будут искажены.

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

Давайте посмотрим на следующую картинку, чтобы понять, что мы здесь пытаемся сделать:

Центр квадрата - это место, где пользователь сделал первый щелчок по событию кисти. Давайте посмотрим, что происходит с четырьмя доступными движениями, которые пользователь может сделать после первого щелчка:

  1. Нижний правый квадрат
    Вертикальная ось вниз - ›В этом случае у нас есть положительное значение на вертикальной оси (помните, что на холсте начало координат находится вверху левый угол и значения увеличиваются вниз и вправо)
    Горизонтальная ось вправо - ›Мы основываем наше горизонтальное движение на том, насколько пользователь переместился по вертикали, чтобы сохранить соотношение сторон. Когда пользователь перемещается вправо, горизонтальные значения увеличиваются, поэтому они положительные.
    Значение xCorMulti - ›в этом случае будет 1, поскольку мы не хотим изменять значение. Если пользователь переместил 10 единиц по вертикали, мы хотим, чтобы в нашем поле было 10 единиц по горизонтали.
  2. Нижний левый квадрат
    Вертикальная ось вниз - ›То же, что и предыдущий
    Горизонтальная ось влево -› Мы основываем наше горизонтальное движение от того, насколько пользователь переместился по вертикали, чтобы сохранить соотношение сторон. Когда пользователь перемещается влево, значения по горизонтали уменьшаются, поэтому они становятся отрицательными.
    Значение xCorMulti - ›в этом случае будет -1, так как мы хотим инвертировать значение. Если пользователь переместил 10 единиц по вертикали, мы хотим, чтобы наш блок имел -10 единиц по горизонтали.
  3. Правый верхний квадрат
    Вертикальная ось вверх - ›В этом случае значение по вертикали будет отрицательным
    Горизонтальная ось вправо - ›То же, что и в 1, горизонтальное значение будет положительным
    xCorMulti value -› в этом случае будет -1, так как мы хотим инвертировать значение. Если пользователь переместил -10 единиц по вертикали, мы хотим, чтобы в нашем поле было 10 единиц по горизонтали.
  4. Верхний левый квадрат
    Вертикальная ось вверх - ›То же, что и предыдущий
    Горизонтальная ось вправо -› То же, что и в 2, значение по горизонтали будет отрицательным
    значение xCorMulti - ›в этом случае будет равно 1, поскольку мы не хотим изменять значение. Если пользователь переместил -10 единиц по вертикали, мы хотим, чтобы наш блок имел -10 единиц по горизонтали.

Надеюсь, что теперь поправочная переменная X имеет больше смысла.

Теперь продолжим код функции brush_brushEvent ().

Этот маленький кусочек говорит сам за себя. Обычно, если положение Y выходит за пределы графика вниз, мы вычисляем расстояние до предела графика и устанавливаем положение Y равным высоте графика. В ветке else мы заботимся о случае, когда позиция Y выходит за пределы графика вверху. В этом случае мы устанавливаем позицию Y равной нулю.

Пришло время вычислить положение X на основе положения Y:

Сначала мы вычисляем положение на горизонтальной оси, используя начальную точку события на X и суммируя расстояние, которое мы прошли по вертикальной оси, умноженное на масштаб (вычисленное в начале функции) и значение коррекции X.
Мы затем сохраните расстояние в константе oldDistance, которую мы будем использовать позже, чтобы исправить положение Y, если это необходимо. Почему? Потому что нам нужно выполнить те же проверки, что и по оси Y, но по оси X. Это означает, что нам нужно проверить, не вышла ли позиция X за пределы графика. Это может изменить наше значение расстояния, и, поскольку нам нужно сохранить соотношение сторон нашего блока, если расстояние изменилось по горизонтали, нам нужно исправить положение по вертикали. В этом случае можно быть уверенным, что вертикальное положение всегда будет в пределах графика.

И для завершения функции:

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

Следующее, что нужно объяснить, это еще один сложный вопрос. Я сделаю все, что в моих силах. Итак, когда мы рисуем квадрат / прямоугольник на холсте или SVG, нам нужно указать, где он начинается и каковы его ширина и высота. Ширина и высота уже были установлены на предыдущем шаге, как уже объяснялось. Но нам нужно обработать 2 особых случая, чтобы установить начальную точку прямоугольника (которая всегда находится в верхнем левом углу). Всякий раз, когда пользователь движется к положительным значениям (вниз или вправо), мы сохраняем нашу исходную точку на [0,0]. Но если пользователь переместился в сторону отрицательных значений по горизонтали или вертикали, наш верхний левый угол (начало прямоугольника) больше не будет [0,0]. Я рекомендую вам взять бумагу, карандаш и сделать несколько рисунков, чтобы лучше понять это.

Например, если мы переместим -10 единиц по вертикали, исходная точка нашего прямоугольника будет в точке [0, -10], и с этой точки мы применим к нему ширину и высоту, чтобы нарисовать его. То же самое происходит, если мы перемещаем -10 единиц по горизонтали, вместо этого перемещая нашу исходную точку на [-10, 0].

Наконец, мы вычисляем максимальное и минимальное значения для X и Y и сохраняем их в объекте lastSelection. С написанием этого кода наши поиски по созданию коробки с таким же соотношением сторон нашей диаграммы можно считать успешным. НО! нам все еще нужно поработать над частью масштабирования, иначе мы будем рисовать блоки, которые ничего не делают.

Событие конца кисти

Первое, что нам нужно сделать, это очистить выделение от события. Если мы этого не сделаем, нарисованный прямоугольник останется на графике, а нам это не нужно. Все, что нам нужно, - это обеспечить хороший UX, чтобы пользователи могли видеть, какой выбор они делают. Как только они отпустят кнопку мыши, мы хотим, чтобы прямоугольник исчез, и мы увеличили масштаб до границ прямоугольника.

Затем мы используем последнее примененное преобразование (см. Предыдущий пост, если вы не помните, как работают преобразования), чтобы масштабировать оси X и Y. Следующим шагом будет вычисление общего горизонтального расстояния нашего выделения. Мы делаем это с минимальным и максимальным положениями X.

Как мы знаем, на [x1, y1] у нас есть верхний левый угол нашего выделения. Что мы сделаем, так это преобразуем те значения x1 и y1, которые являются пикселями, в значения, которые имеют смысл в масштабах нашей диаграммы холста. Мы делаем это, используя функцию инвертирования масштабированных осей.

Следующее, что нужно сделать, это вычислить новое преобразование для нашей диаграммы. Помните, что мы увеличиваем масштаб, и масштаб трансформируется в диаграмму. Имея набор преобразований, мы снова приступаем к масштабированию наших осей, используя преобразование «увеличенное масштабирование».

Теперь мы готовы сделать крутой переход на холсте, чтобы вид был сфокусирован на области, нарисованной пользователем. Мы передаем событию то, что будет нашей новой точкой отсчета (верхний левый угол), и используем рассчитанные позиции X и Y и переводим в эти позиции, умноженные на -1. Причина проста, предположим, что наша новая точка отсчета находится в позиции [10, 10], чтобы переместить ее в [0, 0], нам нужно перевести эту позицию на [-10, -10].

А теперь ДА, готово! у нас реализована довольно приятная функция масштабирования окна.

Спасибо за прочтение и надеюсь, что все прояснилось ... если не стесняйтесь написать комментарий или написать нам по электронной почте [email protected].

Ваше здоровье!

Не пропустите наши публикации, подпишитесь на нас прямо сейчас в Twitter!