Пошаговый подход к визуализации финансовых наборов данных
Передача данных и отображение этих визуализаций на различных устройствах и платформах – сложная задача.
D3 (Data-Driven Documents) решает эту извечную дилемму. Он предоставляет разработчикам и аналитикам возможность создавать индивидуальные визуализации для Web с полной свободой. D3.js позволяет нам привязывать данные к DOM (Document Object Model). Затем применять преобразования, основанные на данных, для создания изысканных визуализаций данных.
В этом учебнике мы разберемся, как заставить библиотеку D3.js работать на нас.
Начало работы
Мы будем строить график, иллюстрирующий движение финансового инструмента за определенный период времени. Эта визуализация напоминает графики цен, предоставляемые Yahoo Finance. Мы разберем различные компоненты, необходимые для построения интерактивного ценового графика, который отслеживает конкретную акцию.
Требуемые компоненты:
- Загрузка и анализ данных
- Элемент SVG
- X и Y оси
- Закрыть диаграмма линии цен
- Простая скользящая средняя кривая диаграммы с некоторыми расчетами
- Громчечная диаграмма
- Мыши перекрестие и легенда
- Загрузка и разбор данных
- SVG-элемент
- Оси X и Y
- Линейный график цены закрытия
- Простой график кривой скользящего среднего с некоторыми вычислениями
- Гистограмма серии объемов
- Перекрестие и легенда при наведении мыши
Загрузка и анализ данных
const loadData = d3.json('sample-data.json').then(data => {
const chartResultsData = data['chart']['result'][0];
const quoteData = chartResultsData['indicators']['quote'][0];
return chartResultsData['timestamp'].map((time, index) => ({
date: new Date(time * 1000),
high: quoteData['high'][index],
low: quoteData['low'][index],
open: quoteData['open'][index],
close: quoteData['close'][index],
volume: quoteData['volume'][index]
}));
});Сначала мы воспользуемся модулем fetch для загрузки данных нашего образца. D3-fetch также поддерживает другие форматы, такие как TSV и CSV файлы. Затем данные будут обработаны, чтобы вернуть массив объектов. Каждый объект содержит временную метку сделки, высокую цену, низкую цену, цену открытия, цену закрытия и объем сделки.
body {
background: #00151c;
}
#chart {
background: #0e3040;
color: #67809f;
}Добавьте приведенные выше базовые свойства CSS, чтобы персонализировать стиль вашей диаграммы для максимальной визуальной привлекательности.
Добавление элемента SVG
const initialiseChart = data => {
const margin = { top: 50, right: 50, bottom: 50, left: 50 };
const width = window.innerWidth - margin.left - margin.right;
const height = window.innerHeight - margin.top - margin.bottom;
// add SVG to the page
const svg = d3
.select('#chart')
.append('svg')
.attr('width', width + margin['left'] + margin['right'])
.attr('height', height + margin['top'] + margin['bottom'])
.call(responsivefy)
.append('g')
.attr('transform', `translate(${margin['left']}, ${margin['top']})`);Затем мы можем использовать метод append(), чтобы добавить SVG-элемент к элементу с id, chart. Далее мы используем метод attr() для назначения ширины и высоты SVG-элемента. Затем мы вызываем метод responsivefy() (первоначально написанный Бренданом Судолом). Это позволяет элементу SVG иметь возможность реагировать на изменения размера окна.
Не забудьте добавить элемент группы SVG к вышеуказанному элементу SVG, прежде чем переводить его, используя значения из константы margin.
Рендеринг осей X и Y
Перед рендерингом компонента осей нам нужно определить область и диапазон, которые затем будут использованы для создания масштабов осей
// find data range
const xMin = d3.min(data, d => {
return d['date'];
});
const xMax = d3.max(data, d => {
return d['date'];
});
const yMin = d3.min(data, d => {
return d['close'];
});
const yMax = d3.max(data, d => {
return d['close'];
});
// scales for the charts
const xScale = d3
.scaleTime()
.domain([xMin, xMax])
.range([0, width]);
const yScale = d3
.scaleLinear()
.domain([yMin - 5, yMax])
.range([height, 0]);Оси x и y линейного графика цены закрытия состоят из даты сделки и цены закрытия соответственно. Поэтому мы должны определить минимальное и максимальное значения x и y, используя d3.max() и d3.min(). Затем мы можем воспользоваться функциями scaleTime() и scaleLinear() D3-scale для создания временной шкалы на оси x и линейной шкалы на оси y соответственно. Диапазон шкал определяется шириной и высотой нашего SVG-элемента.
// create the axes component
svg
.append('g')
.attr('id', 'xAxis')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(xScale));
svg
.append('g')
.attr('id', 'yAxis')
.attr('transform', `translate(${width}, 0)`)
.call(d3.axisRight(yScale));После этого шага нам нужно добавить к SVG-элементу первый элемент g, который вызывает метод d3.axisBottom(), принимая в качестве параметра xScale для создания оси x. Затем ось x переводится в нижнюю часть области графика. Аналогично, ось y генерируется путем добавления элемента g, вызова метода d3.axisRight() с yScale в качестве параметра, перед переводом оси y в правую часть области графика.
Построение линейного графика цены закрытия
// generates close price line chart when called
const line = d3
.line()
.x(d => {
return xScale(d['date']);
})
.y(d => {
return yScale(d['close']);
});
// Append the path and bind data
svg
.append('path')
.data([data])
.style('fill', 'none')
.attr('id', 'priceChart')
.attr('stroke', 'steelblue')
.attr('stroke-width', '1.5')
.attr('d', line);Теперь мы можем добавить элемент path внутрь нашего основного элемента SVG, а затем передать наш разобранный набор данных, data. Мы устанавливаем атрибут d с помощью нашей вспомогательной функции line. которая вызывает метод d3.line(). Атрибуты x и y линии принимают анонимные функции и возвращают дату и цену закрытия соответственно.
К этому моменту ваш график должен выглядеть так:

Построение кривой простой скользящей средней
Вместо того чтобы полагаться исключительно на цену закрытия как на единственный технический индикатор, мы используем простую скользящую среднюю. Эта средняя определяет восходящие и нисходящие тренды для конкретной ценной бумаги.
const movingAverage = (data, numberOfPricePoints) => {
return data.map((row, index, total) => {
const start = Math.max(0, index - numberOfPricePoints);
const end = index;
const subset = total.slice(start, end + 1);
const sum = subset.reduce((a, b) => {
return a + b['close'];
}, 0);
return {
date: row['date'],
average: sum / subset.length
};
});
};Мы определяем вспомогательную функцию movingAverage для расчета простого скользящего среднего. Эта функция принимает два параметра, а именно набор данных и количество ценовых точек, или периодов. Затем она возвращает массив объектов, каждый из которых содержит дату и среднее значение для каждой точки данных.
// calculates simple moving average over 50 days
const movingAverageData = movingAverage(data, 49);
// generates moving average curve when called
const movingAverageLine = d3
.line()
.x(d => {
return xScale(d['date']);
})
.y(d => {
return yScale(d['average']);
})
.curve(d3.curveBasis);
svg
.append('path')
.data([movingAverageData])
.style('fill', 'none')
.attr('id', 'movingAverageLine')
.attr('stroke', '#FF8900')
.attr('d', movingAverageLine);Для нашего текущего контекста функция movingAverage() рассчитывает простую скользящую среднюю за период в 50 дней. Аналогично графику линии цены закрытия, мы добавляем элемент path в наш основной SVG-элемент, затем передаем наш набор данных скользящего среднего и устанавливаем атрибут d с помощью нашей вспомогательной функции movingAverageLine. Единственное отличие от вышеописанного заключается в том, что мы передали d3.curveBasis в d3.line().curve(), чтобы получить кривую.
В результате мы получаем простую кривую скользящей средней, наложенную поверх нашего текущего графика:

Отображение гистограммы серии объемов
В этом компоненте мы будем отображать объем торгов в виде гистограммы с цветовой кодировкой, занимающей один и тот же SVG-элемент. Полосы окрашиваются в зеленый цвет, когда цена закрытия акции выше цены закрытия предыдущего дня. Они красные, когда цена закрытия ниже цены закрытия предыдущего дня. Это иллюстрирует объем торгов для каждой торговой даты. Это можно использовать вместе с приведенным выше графиком для анализа движения цен.
/* Volume series bars */
const volData = data.filter(d => d['volume'] !== null && d['volume'] !== 0);
const yMinVolume = d3.min(volData, d => {
return Math.min(d['volume']);
});
const yMaxVolume = d3.max(volData, d => {
return Math.max(d['volume']);
});
const yVolumeScale = d3
.scaleLinear()
.domain([yMinVolume, yMaxVolume])
.range([height, 0]);Оси x и y гистограммы серии объемов состоят из даты сделки и объема соответственно. Таким образом, нам нужно переопределить минимальное и максимальное значения y и использовать scaleLinear() для оси y. Диапазон этих масштабов определяется шириной и высотой нашего SVG-элемента. Мы будем повторно использовать xScale, поскольку ось x гистограммы аналогично соответствует торговой дате.
svg
.selectAll()
.data(volData)
.enter()
.append('rect')
.attr('x', d => {
return xScale(d['date']);
})
.attr('y', d => {
return yVolumeScale(d['volume']);
})
.attr('fill', (d, i) => {
if (i === 0) {
return '#03a678';
} else {
return volData[i - 1].close > d.close ? '#c0392b' : '#03a678';
}
})
.attr('width', 1)
.attr('height', d => {
return height - yVolumeScale(d['volume']);
});Этот раздел основан на вашем понимании того, как метод theselectAll() работает с методами enter() и append(). Вы можете прочитать эту статью (написанную самим Майком Бостоком), если вы не знакомы с этими методами. Это может быть важно, так как эти методы используются как часть паттерна enter-update-exit, о котором я расскажу в одном из следующих уроков.
Чтобы отобразить бары, мы сначала используем selectAll(), чтобы вернуть пустой выбор или пустой массив. Затем мы передадим volData, чтобы определить высоту каждого бара. Метод enter() сравнивает набор данных volData с выбором из selectAll(), который в настоящее время пуст. В настоящее время DOM не содержит ни одного элемента . Поэтому метод append() принимает аргумент ‘rect‘, который создает новый элемент в DOM для каждого объекта в volData.
Ниже приведена разбивка атрибутов полос. Мы будем использовать следующие атрибуты: x, y, fill, width и height.
.attr('x', d => {
return xScale(d['date']);
})
.attr('y', d => {
return yVolumeScale(d['volume']);
})Первый метод attr() определяет координату x. Он принимает анонимную функцию, которая возвращает дату. Аналогично, второй метод attr() определяет координату y. Он принимает анонимную функцию, которая возвращает объем. Они определяют положение каждого бара.
.attr('width', 1)
.attr('height', d => {
return height - yVolumeScale(d['volume']);
});Каждой полосе мы присваиваем ширину в 1 пиксель. Чтобы полоса растянулась от вершины (определяемой y) до оси x, просто вычтите высоту из значения y.
.attr('fill', (d, i) => {
if (i === 0) {
return '#03a678';
} else {
return volData[i - 1].close > d.close ? '#c0392b' : '#03a678';
}
})Помните, как столбики будут обозначены цветом? Мы будем использовать атрибут fill для определения цвета каждого бара. Для акций, которые закрылись выше цены закрытия предыдущего дня, столбик будет зеленого цвета. В противном случае столбик будет красным.
Вот как должен выглядеть ваш текущий график:

Рендеринг перекрестия и легенды для интерактивности
Мы подошли к последнему шагу этого руководства, в котором мы создадим перекрестие при наведении мыши, отображающее линии падения. При наведении курсора мыши на различные точки графика будут обновляться легенды. В результате мы получим полную информацию (цена открытия, цена закрытия, высокая цена, низкая цена и объем) для каждой торговой даты.
В следующем разделе приведена ссылка на отличный пример Мика Стабба.
// renders x and y crosshair
const focus = svg
.append('g')
.attr('class', 'focus')
.style('display', 'none');
focus.append('circle').attr('r', 4.5);
focus.append('line').classed('x', true);
focus.append('line').classed('y', true);
svg
.append('rect')
.attr('class', 'overlay')
.attr('width', width)
.attr('height', height)
.on('mouseover', () => focus.style('display', null))
.on('mouseout', () => focus.style('display', 'none'))
.on('mousemove', generateCrosshair);
d3.select('.overlay').style('fill', 'none');
d3.select('.overlay').style('pointer-events', 'all');
d3.selectAll('.focus line').style('fill', 'none');
d3.selectAll('.focus line').style('stroke', '#67809f');
d3.selectAll('.focus line').style('stroke-width', '1.5px');
d3.selectAll('.focus line').style('stroke-dasharray', '3 3');Перекрестие состоит из полупрозрачного круга с линиями, состоящими из черточек. Приведенный выше блок кода обеспечивает стилизацию отдельных элементов. При наведении мыши на перекрестие оно генерируется на основе функции, приведенной ниже.
const bisectDate = d3.bisector(d => d.date).left;
function generateCrosshair() {
//returns corresponding value from the domain
const correspondingDate = xScale.invert(d3.mouse(this)[0]);
//gets insertion point
const i = bisectDate(data, correspondingDate, 1);
const d0 = data[i - 1];
const d1 = data[i];
const currentPoint = correspondingDate - d0['date'] > d1['date'] - correspondingDate ? d1 : d0;
focus.attr('transform',`translate(${xScale(currentPoint['date'])}, ${yScale(currentPoint['close'])})`);
focus
.select('line.x')
.attr('x1', 0)
.attr('x2', width - xScale(currentPoint['date']))
.attr('y1', 0)
.attr('y2', 0);
focus
.select('line.y')
.attr('x1', 0)
.attr('x2', 0)
.attr('y1', 0)
.attr('y2', height - yScale(currentPoint['close']));
updateLegends(currentPoint);
}Затем мы можем воспользоваться методом d3.bisector() для определения точки вставки, которая выделит ближайшую точку данных на графике линии цены закрытия. После определения CurrentPoint линии падения будут обновлены. Метод updateLegends() использует текущую точку в качестве параметра.
const updateLegends = currentData => { d3.selectAll('.lineLegend').remove();
const updateLegends = currentData => {
d3.selectAll('.lineLegend').remove();
const legendKeys = Object.keys(data[0]);
const lineLegend = svg
.selectAll('.lineLegend')
.data(legendKeys)
.enter()
.append('g')
.attr('class', 'lineLegend')
.attr('transform', (d, i) => {
return `translate(0, ${i * 20})`;
});
lineLegend
.append('text')
.text(d => {
if (d === 'date') {
return `${d}: ${currentData[d].toLocaleDateString()}`;
} else if ( d === 'high' || d === 'low' || d === 'open' || d === 'close') {
return `${d}: ${currentData[d].toFixed(2)}`;
} else {
return `${d}: ${currentData[d]}`;
}
})
.style('fill', 'white')
.attr('transform', 'translate(15,9)');
};Метод updateLegends() обновляет легенду, отображая дату, цену открытия, цену закрытия, высокую цену, низкую цену и объем выбранной при наведении курсора мыши точки на графике линии закрытия. Как и в случае с гистограммами объема, мы будем использовать метод selectAll() с методами enter() и append().
Для отображения легенд мы будем использовать метод selectAll(‘.lineLegend’) для выбора легенд, а затем вызовем метод remove() для их удаления. Далее мы передаем ключи легенд, legendKeys, которые будут использоваться для определения высоты каждой полосы. Вызывается метод enter(), который сравнивает набор данных volData и на выборке из selectAll(), которая в настоящее время пуста. В настоящее время DOM не содержит ни одного элемента . Поэтому метод append() принимает аргумент ‘rect’, который создает новый элемент в DOM для каждого объекта в volData.
Далее добавляем легенды с их соответствующими свойствами. Далее мы обрабатываем значения, преобразуя цены в 2 знака после запятой. Мы также устанавливаем для объекта даты локаль по умолчанию для удобства чтения.
Заключительные мысли
Поздравляем! Вы достигли конца этого руководства. Как было показано выше, D3.js является простым, но динамичным инструментом. Он позволяет создавать пользовательские визуализации для всех ваших наборов данных. В ближайшие недели я выпущу вторую часть этой серии, в которой подробно рассмотрю паттерн входа-обновления-выхода D3.js. Тем временем вы можете ознакомиться с документацией по API, другими учебниками и другими интересными визуализациями, созданными с помощью D3.js.
Не стесняйтесь ознакомиться с исходным кодом, а также с полной демонстрацией этого учебника. Спасибо, и я надеюсь, что сегодня вы узнали что-то новое!
Особая благодарность Дебби Леонг за рецензирование этой статьи.
Оригинал: “https://www.freecodecamp.org/news/how-to-build-historical-price-charts-with-d3-js-72214aaf6ba3/”