#javascript #d3.js #data-visualization
#javascript #d3.js #визуализация данных
Вопрос:
Допустим, у меня есть набор данных разных спортсменов, которые выполняют один и тот же тест в разные дни. Каждый день у них будет несколько испытаний / запусков. Я хочу визуализировать развитие каждого спортсмена в эти дни, используя d3.js но я изо всех сил пытаюсь понять, как я могу выполнить эту задачу.
Используя seaborn в Python или ggplot2 в R, я бы использовал facetplot, где каждый день является аспектом. В рамках этих аспектов у меня были бы испытания по оси x и производительность по оси I. Но как я могу сделать это в d3.js ?
D3.group и group позволяют мне группировать набор данных по спортсменам, и я понимаю, как я могу перебирать значения каждого спортсмена. Но я не понимаю, как я могу перейти отсюда к фактическому созданию фасетной диаграммы в d3.js .
Я пытался найти соответствующие учебные пособия по observable, но без особой удачи. Одна интересная и связанная с этим визуализация — [Козыри в гольфах bt Босток]. Еще один — это диаграмма рассеяния.
Не мог бы кто-нибудь, пожалуйста, помочь мне в правильном направлении? Я создал простой набор данных и диаграмму рассеяния, которые могут служить в качестве начального
const data = d3.range(10).map(i => ({
bib: Math.floor(i / 5) 1,
ratio: -1 Math.random() * 5,
run: [1, 2, 3, 4, 5][i % 5],
run: [1, 2, 3, 4, 5][i % 5],
name: ['GIRL1', 'GIRL2', 'GIRL3', 'GIRL4'][Math.floor(i / 5)]
}));
const width = 250;
const height = 150;
const svg = d3.select('svg')
.attr('width', width)
.attr('height', height)
const margin = {
top: 20,
right: 20,
bottom: 20,
left: 50
}
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const xScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.run))
.range([0, innerWidth])
const yScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.ratio))
.range([0, innerHeight])
const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
g.selectAll('circle')
.data(data)
.join('circle')
.attr('r', 3)
.attr('cx', d => xScale(d.run))
.attr('cy', d => yScale(d.ratio))
g.append("g")
.call(d3.axisBottom(xScale))
.attr('transform', `translate(0,${innerHeight})`);;
g.append("g")
.call(d3.axisLeft(yScale))
<script src="https://unpkg.com/d3@6.2.0/dist/d3.min.js"></script>
<svg></svg>
Пример построения графика из R:
Ответ №1:
Настройка SVG
Сначала давайте установим соглашение о том, как мы будем размещать и структурировать страницу. Только с одним рядом диаграмм это немного проще.
Нам нужно определить переменные на изображении выше. Тогда мы можем получить ширину и высоту каждого графика:
const height = 500;
const width = 500;
const margin = {
top: 50,
left: 50,
right: 50,
bottom: 50
}
const padding = 10; // labelled `pad` in image due to space constraints
const plotWidth = (width-padding)/numberOfPlots - padding;
const plotHeight = height-padding*2;
Конечно, вы можете организовать так, как вам нравится, мое включение этого частично сделано только для того, чтобы сделать остальную часть ответа более понятной.
Ниже я использую a g
для удержания области, ограниченной полями, чтобы упростить последующее позиционирование.
Теперь у нас также есть два разных масштаба, и вы уже сделали это в своем коде, но теперь мы можем определить диапазоны с plotWidth
помощью и plotHeight
с использованием приведенного выше соглашения о интервалах.
Построение графика данных
Теперь мы готовы к созданию графиков. Для этого мы создадим вложенный набор данных: мы хотим сгруппировать данные по графику, и, как вы заметили, мы можем использовать d3.group:
const grouped = d3.group(data,d=>d.bib);
Для каждой группировки мы создадим g
позицию и, используя перевод, основанный на приведенной выше структуре:
const plots = g.selectAll(".plot")
.data(grouped)
.enter()
.append("g")
.attr("transform", function(d,i) {
return "translate(" [i*(padding plotWidth) padding,padding] ")";
})
И мы готовы к построению графика с использованием вложенных данных:
plots.selectAll(null)
.data(d=>d[1])
.enter()
.append("circle")
... // and so forth.
Приведенный ниже фрагмент используется d=>d[1]
как место, где находится массив элементов, принадлежащих этой группе. d[0]
является идентификатором группы. Если бы мой массив данных был массивом массивов, я бы просто использовал d=>d;
.
Теперь мы могли бы добавить ось x на каждый график:
plots.append("g")
.attr("transform","translate(" [0,plotHeight] ")")
.call(d3.axisBottom(x));
И ось y:
g.append("g")
.attr("transform","translate(" [0,padding] ")");
.call(d3.axisLeft(y));
Я не добавил родительскую ось x, в зависимости от вашего использования вы можете пометить ее или нет. Я также заметил, что я сгруппировал ваши данные по bib, а не по run, но принципы остаются прежними: группируйте данные, вводите и размещайте родительские графики, затем добавляйте точки данных к родительским.
Пример
// Data and manipluation:
const data = d3.range(15).map(i => ({
bib: Math.floor(i / 5) 1,
ratio: -1 Math.random() * 5,
run: [1, 2, 3, 4, 5][i % 5],
run: [1, 2, 3, 4, 5][i % 5],
name: ['GIRL1', 'GIRL2', 'GIRL3', 'GIRL4'][Math.floor(i / 5)]
}));
const grouped = d3.group(data,d=>d.bib);
// Dimensions:
const height = 500;
const width = 500;
const margin = {
top: 10,
left: 50,
right: 50,
bottom: 50
}
const padding = 30; // labelled `pad` in image due to space constraints
const plotWidth = (width-padding)/grouped.size - padding;
const plotHeight = height-padding*2;
const svg = d3.select("body")
.append("svg")
.attr("width", margin.left width margin.right)
.attr("height", margin.top height margin.bottom);
const g = svg.append("g")
.attr("transform","translate(" [margin.left,margin.top] ")");
//Scales:
const xScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.run))
.range([0, plotWidth]);
const yScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.ratio))
.range([plotHeight, 0]);
// Place plots:
const plots = g.selectAll(null)
.data(grouped)
.enter()
.append("g")
.attr("transform", function(d,i) {
return "translate(" [i*(padding plotWidth) padding,padding] ")";
})
//Optional plot background:
plots.append("rect")
.attr("width",plotWidth)
.attr("height",plotHeight)
.attr("fill","#ddd");
// Plot actual data
plots.selectAll(null)
.data(d=>d[1])
.enter()
.append("circle")
.attr("r", 4)
.attr("cy", d=>yScale(d.ratio))
.attr("cx", d=>xScale(d.run))
// Plot line if needed:
plots.append("path")
.attr("d", function(d) {
return d3.line()
.x(d=>xScale(d.run))
.y(d=>yScale(d.ratio))
(d[1])
})
.attr("stroke", "#333")
.attr("stroke-width", 1)
.attr("fill","none")
// Plot names if needed:
plots.append("text")
.attr("x", plotWidth/2)
.attr("y", -10)
.text(function(d) {
return d[1][0].name;
})
.attr("text-anchor","middle");
// Plot axes
plots.append("g")
.attr("transform","translate(" [0,plotHeight] ")")
.call(d3.axisBottom(xScale).ticks(4));
g.append("g")
.attr("transform","translate(" [0,padding] ")")
.call(d3.axisLeft(yScale))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.1.0/d3.min.js"></script>
Что должно привести к:
Комментарии:
1. Спасибо, что нашли время для создания этого пошагового руководства. Теперь я действительно понимаю, как это сделать. Однако одно: какова логика, лежащая в основе plots.selectAll(null)? Тот, который я не получил
2.
selectAll(null)
обеспечивает пустой выбор, так что для каждого элемента в массиве данных вводится элемент. Какplots
и выбор пустыхg
элементов, мы могли бы использоватьselectAll("circle")
too или любой селектор. Поскольку вg
элементах нечего выбирать, у нас всегда будет пустое выделение.3. Если вам также нужно несколько строк. Как вы можете это сделать? 🙂
Ответ №2:
Ну, во второй раз сегодня @AndrewReid опередил меня, и, конечно, его ответ очень совершенен (мне нравится диаграмма).
Несмотря на это, вот моя запись. Большая разница в том, что я использовал d3.scaleBand
(и привязку данных) для размещения каждой дочерней линейной диаграммы, в то время как он делал это более вручную).
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v6.min.js"></script>
<style>
svg {
font-family: arial
}
.tick line {
stroke: white;
stroke-opacity: 0.7;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<svg></svg>
<script>
let data = [],
tests = ['PRETEST', 'TRENING1', 'TRENING2', 'TRENING3', 'POSTTEST'],
courses = ['COURSE 1', 'COURSE 2', 'COURSE 3', 'STRAIGHT-GLIDING'];
tests.forEach((i) => {
courses.forEach((j) => {
d3.range(5).map((k) => {
data.push({
test: i,
course: j,
run: k,
ratio: -1 Math.random() * 5,
});
});
});
});
const width = 1100;
const height = 350;
const margin = {
top: 20,
right: 10,
bottom: 20,
left: 30,
};
// place wrapper g with margins
const svg = d3
.select('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' margin.left ',' margin.top ')');
// calculate the outer scale band for each line graph
const outerXScale = d3
.scaleBand()
.domain(tests)
.range([0, width - margin.left - margin.right]);
// inner dimensions of chart based on bandwidth of outer scale
const innerWidth = outerXScale.bandwidth() - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// g for each inner chart
const testG = svg
.selectAll('.outer')
.data(d3.group(data, (d) => d.test))
.enter()
.append('g')
.attr('class', 'outer')
.attr('transform', function (d, i) {
return 'translate(' outerXScale(d[0]) ',' 0 ')';
});
// some styling
testG
.append('rect')
.attr('width', innerWidth)
.attr('height', innerHeight)
.attr('fill', '#f2f2f2');
testG
.append('rect')
.attr('width', innerWidth)
.attr('height', 17)
.attr('transform', 'translate(' 0 ',' -17 ')')
.attr('fill', '#e6e6e6');
// header
testG
.append('text')
.text(function (d) {
return d[0];
})
.attr('text-anchor', 'middle')
.attr('transform', 'translate(' innerWidth / 2 ',' -2 ')');
// inner scales
const innerXScale = d3
.scaleLinear()
.domain(d3.extent(data, (d) => d.run))
.range([0, innerWidth]);
const innerYScale = d3
.scaleLinear()
.domain(d3.extent(data, (d) => d.ratio))
.range([innerHeight, 0]);
testG
.append('g')
.call(d3.axisBottom(innerXScale).tickSize(-innerHeight))
.attr('transform', `translate(0,${innerHeight})`);
testG.append('g').call(d3.axisLeft(innerYScale).tickSize(-innerWidth));
testG
.selectAll('.line')
.data(function (d) {
return d3.group(d[1], (d) => d.course);
})
.enter()
.append('path')
.attr('d', function (d) {
return d3
.line()
.x((d) => innerXScale(d.run))
.y((d) => innerYScale(d.ratio))(d[1]);
})
.attr('fill', 'none')
.attr('stroke', function (d, i) {
return d3.schemeCategory10[i];
})
.attr('stroke-width', 1);
</script>
</body>
</html>
Комментарии:
1. Ха, упс, увидел ваш комментарий и немного подождал. Думаю, недостаточно. Было бы неплохо, если бы ТАК сказали, кто начал ответ, я бы сэкономил немного времени на этой неделе. С положительной стороны, возможно, я смогу заработать значок спортивного мастерства, если мы продолжим задавать одни и те же вопросы.
2. @AndrewReid, не беспокойся и не извиняйся, если я наступил тебе на пятки. На самом деле мне нравится видеть наши слегка разные подходы.
3. На пальцы не наступают, всегда приятно видеть ваши ответы, и мне также нравится сравнивать наши разные подходы.