英文:
how to create d3.js bar chart as a Svelte component and bind d3 to SVG properly?
问题 {#heading}
以下是d3.js图表的代码部分的中文翻译:
const data = [{
"date": "2010-08-06",
"count": 32348
},
{
"date": "2010-08-07",
"count": 32454
},
{
"date": "2010-08-08",
"count": 32648
},
{
"date": "2010-08-09",
"count": 32812
},
{
"date": "2010-08-10",
"count": 32764
},
{
"date": "2010-08-11",
"count": 32668
},
{
"date": "2010-08-12",
"count": 32484
},
{
"date": "2010-08-13",
"count": 32167
},
{
"date": "2010-08-14",
"count": 32304
},
{
"date": "2010-08-15",
"count": 32446
},
{
"date": "2010-08-16",
"count": 32670
},
{
"date": "2010-08-17",
"count": 32778
},
{
"date": "2010-08-18",
"count": 32756
},
{
"date": "2010-08-19",
"count": 32580
}
]
const formatDate4 = d3.timeFormat("%m%d%Y")
const formatDate5 = d3.timeFormat('%Y-%m-%d')
const formatDate6 = d3.timeFormat("%b %d, %Y");
const bisectDate = d3.bisector(function(d) {
return d.date;
}).left;
var parseTime = d3.timeParse("%Y-%m-%d")
const pageWidth = window.innerWidth
let wrapWidth = 969,
mapRatio = .51
let margin = {
top: 0,
right: 0,
bottom: 10,
left: 0
}
let timeW = 960,
timeH = 450
let timeMargin = {
top: 20,
right: 250,
bottom: 80,
left: 60
},
timeMargin2 = {
top: 410,
right: 250,
bottom: 30,
left: 60
},
timeWidth = timeW - timeMargin.left - timeMargin.right,
timeHeight = timeH - timeMargin.top - timeMargin.bottom,
timeHeight2 = timeH - timeMargin2.top - timeMargin2.bottom;
let timeseries = d3.select("#timeseries-container").append('svg')
.attr('id', 'timeseries')
.attr("width", timeWidth + timeMargin.left + timeMargin.right)
.attr("height", timeHeight + timeMargin.top + timeMargin.bottom)
var graph = timeseries.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var parseDate = d3.timeParse("%Y-%m-%d");
var x2 = d3.scaleTime().range([0, timeWidth]),
x3 = d3.scaleTime().range([0, timeWidth]),
y2 = d3.scaleLinear().range([timeHeight, 0]),
y3 = d3.scaleLinear().range([timeHeight2, 0]);
var xAxis2 = d3.axisBottom(x2).ticks(5)
.tickFormat(d3.timeFormat("%b %d %Y")),
yAxis2 = d3.axisLeft(y2).ticks(3);
timeseries.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", timeWidth)
.attr("height", timeHeight)
var chartfocus = timeseries.append("g")
.attr("class", "chartfocus")
.attr("transform", "translate(" + timeMargin.left + "," + timeMargin.top + ")");
const natlData = data;
console.log('natlData', natlData)
updateChart(natlData)
function updateChart(data) {
const dataOld = data.map(a => ({ ...a }));
data.forEach(d => {
d.date = parseTime(d.date);
})
x2.domain(d3.extent(data, function(d) {
return d.date;
}));
const charttooltip = d3.select("#time").append("div")
.attr("class", "charttooltip");
const chartguideline = d3.select("body").append("div")
.attr("class", "chartguideline");
let topY = d3.max(data, d => d.count)
timeseries.selectAll(".axis").remove();
let bisectDate = d3.bisector(function(d) {
return d.date;
}).left;
chartfocus
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseout", mouseout)
y2.domain([0, topY]).nice();
d3.select(".axis--y")
.transition(1000)
.call(yAxis2)
d3.selectAll('.bar').remove()
let dailyBars = chartfocus.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("clip-path", "url(#clip)")
.attr('class', d => 'bar d' + formatDate4(d.date))
y2.domain([0, topY]).nice();
d3.select(".axis--y")
.transition(1000)
.call(yAxis2)
dailyBars.transition()
.attr("width", d => x2(d3.timeDay.offset(d.date)) - x2(d.date))
.attr('x', d => x2(d.date))
.attr("y", d => y2(d.count))
.attr("height", d => timeHeight - y2(d.count))
chartfocus.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + timeHeight + ")")
.call(xAxis2);
chartfocus.append("g")
.attr("class", "axis axis--y")
.call(yAxis2);
chartfocus.append("rect")
.attr("class", "chartzoom")
.attr("width", timeWidth)
.attr("height", timeHeight)
d3.selectAll('.focus').remove()
function mouseover(event) {}
function mousemove(event, d) {
let outerMargins = pageWidth - wrapWidth
let outerLeftMargin = outerMargins / 2
let ex = event.x - timeMargin.left - outerLeftMargin
var x0 = x2.invert(ex),
i = bisectDate(data, x0, 1),
d0 = data[i - 1],
<details>
<summary>英文:</summary>
I have a focus/context bar chart that I created in d3. I want to put it in Svelte so that I can use it as a component. But I'm getting stuck figuring out how to bind the elements created in d3 to the html in the svelte component.
Below is the code for the d3.js chart
<!-- begin snippet: js hide: true console: true babel: false -->
<!-- language: lang-js -->
const data = [{
"date": "2010-08-06",
"count": 32348
},
{
"date": "2010-08-07",
"count": 32454
},
{
"date": "2010-08-08",
"count": 32648
},
{
"date": "2010-08-09",
"count": 32812
},
{
"date": "2010-08-10",
"count": 32764
},
{
"date": "2010-08-11",
"count": 32668
},
{
"date": "2010-08-12",
"count": 32484
},
{
"date": "2010-08-13",
"count": 32167
},
{
"date": "2010-08-14",
"count": 32304
},
{
"date": "2010-08-15",
"count": 32446
},
{
"date": "2010-08-16",
"count": 32670
},
{
"date": "2010-08-17",
"count": 32778
},
{
"date": "2010-08-18",
"count": 32756
},
{
"date": "2010-08-19",
"count": 32580
}
]
const formatDate4 = d3.timeFormat("%m%d%Y")
const formatDate5 = d3.timeFormat('%Y-%m-%d')
const formatDate6 = d3.timeFormat("%b %d, %Y");
const bisectDate = d3.bisector(function(d) {
return d.date;
}).left;
var parseTime = d3.timeParse("%Y-%m-%d")
const pageWidth = window.innerWidth
let wrapWidth = 969,
mapRatio = .51
let margin = {
top: 0,
right: 0,
bottom: 10,
left: 0
}
let timeW = 960,
timeH = 450
let timeMargin = {
top: 20,
right: 250,
bottom: 80,
left: 60
},
timeMargin2 = {
top: 410,
right: 250,
bottom: 30,
left: 60
},
timeWidth = timeW - timeMargin.left - timeMargin.right,
timeHeight = timeH - timeMargin.top - timeMargin.bottom,
timeHeight2 = timeH - timeMargin2.top - timeMargin2.bottom;
let timeseries = d3.select("#timeseries-container").append('svg')
.attr('id', 'timeseries')
.attr("width", timeWidth + timeMargin.left + timeMargin.right)
.attr("height", timeHeight + timeMargin.top + timeMargin.bottom)
var graph = timeseries.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var parseDate = d3.timeParse("%Y-%m-%d");
var x2 = d3.scaleTime().range([0, timeWidth]),
x3 = d3.scaleTime().range([0, timeWidth]),
y2 = d3.scaleLinear().range([timeHeight, 0]),
y3 = d3.scaleLinear().range([timeHeight2, 0]);
var xAxis2 = d3.axisBottom(x2).ticks(5)
.tickFormat(d3.timeFormat("%b %d %Y")),
yAxis2 = d3.axisLeft(y2).ticks(3);
timeseries.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", timeWidth)
.attr("height", timeHeight)
var chartfocus = timeseries.append("g")
.attr("class", "chartfocus")
.attr("transform", "translate(" + timeMargin.left + "," + timeMargin.top + ")");
const natlData = data;
console.log('natlData', natlData)
updateChart(natlData)
function updateChart(data) {
const dataOld = data.map(a => ({ ...a
}));
data.forEach(d => {
d.date = parseTime(d.date);
})
x2.domain(d3.extent(data, function(d) {
return d.date;
}));
const charttooltip = d3.select("#time").append("div")
.attr("class", "charttooltip");
const chartguideline = d3.select("body").append("div")
.attr("class", "chartguideline");
let topY = d3.max(data, d => d.count)
timeseries.selectAll(".axis").remove();
let bisectDate = d3.bisector(function(d) {
return d.date;
}).left;
chartfocus
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseout", mouseout)
y2.domain([0, topY]).nice();
d3.select(".axis--y")
.transition(1000)
.call(yAxis2)
d3.selectAll('.bar').remove()
let dailyBars = chartfocus.selectAll("bar")
.data(data)
.enter().append("rect")
.attr("clip-path", "url(#clip)")
.attr('class', d => 'bar d' + formatDate4(d.date))
y2.domain([0, topY]).nice();
d3.select(".axis--y")
.transition(1000)
.call(yAxis2)
dailyBars.transition()
.attr("width", d => x2(d3.timeDay.offset(d.date)) - x2(d.date))
.attr('x', d => x2(d.date))
.attr("y", d => y2(d.count))
.attr("height", d => timeHeight - y2(d.count))
chartfocus.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + timeHeight + ")")
.call(xAxis2);
chartfocus.append("g")
.attr("class", "axis axis--y")
.call(yAxis2);
chartfocus.append("rect")
.attr("class", "chartzoom")
.attr("width", timeWidth)
.attr("height", timeHeight)
d3.selectAll('.focus').remove()
function mouseover(event) {}
function mousemove(event, d) {
let outerMargins = pageWidth - wrapWidth
let outerLeftMargin = outerMargins / 2
let ex = event.x - timeMargin.left - outerLeftMargin
var x0 = x2.invert(ex),
i = bisectDate(data, x0, 1),
d0 = data[i - 1],
d1 = data[i],
d = x0 - d0.date > d1.date - x0 ? d1 : d0;
let dateX = d.date
let dateXSimple = formatDate5(dateX)
indvData = dataOld.filter(d => d.date == dateXSimple)
dailyCount = []
dailyCounts = []
let tooltip_str = formatDate5(dateX) + "</p>Count: <strong>" + indvData[0].count
charttooltip
.style('left', event.x - outerLeftMargin + 'px')
.style("top", 0)
.attr('x', 0)
.html(d => tooltip_str)
chartguideline
.style('left', event.x - 1 + 'px')
.style("top", timeMargin.top + 'px')
.attr("width", d => x2(d3.timeDay.offset(dateX)) - x2(dateX))
.style('height', timeHeight + timeMargin.top - timeMargin.bottom + 45 + 'px')
.attr('x', 0)
charttooltip
.style("visibility", "visible")
chartguideline
.style("visibility", "visible")
}
function mouseout() {
d3.selectAll('.bar').style('fill', '#aaa')
d3.selectAll('.bar').transition()
.attr("width", d => x2(d3.timeDay.offset(d.date)) - x2(d.date))
charttooltip.transition()
.duration(0)
.style("visibility", "hidden")
chartguideline.transition()
.duration(0)
.style("visibility", "hidden")
}
};
<!-- language: lang-css -->
.chartzoom {
cursor: move;
fill: none;
pointer-events: all;
}
.charttooltip {
position: absolute;
pointer-events: none;
padding: 10px;
background: #fff;
visibility: hidden;
opacity: .9;
-moz-box-shadow: 0 0 15px #aaa;
-webkit-box-shadow: 0 0 15px #aaa;
box-shadow: 0 0 15px #aaa;
margin-left: 10px;
}
.charttooltip:before {
right: 100%;
top: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-color: rgba(255, 255, 255, 0);
border-right-color: #ffffff;
border-width: 10px;
margin-top: -10px;
}
.chartguideline {
position: absolute;
width: 1px;
opacity: .5;
pointer-events: none;
background: #ef4136;
visibility: hidden;
}
.bar {
fill: #aaa;
stroke-width: 1px;
stroke: #000;
}
<!-- language: lang-html -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<div id="time">
<div id="timeseries-container"></div>
</div>
<!-- end snippet -->
The code I'm using for the Svelte component is below. I'm trying to put the main SVG in the body, and then bind my d3 code to it, but I'm not able to get that working. Any help would be greatly appreciated.
I also made a [REPL][1], if that's easier.
<script>
import * as d3 from "d3";
const data = [
{
date: "2010-08-06",
count: 32348,
},
{
date: "2010-08-07",
count: 32454,
},
{
date: "2010-08-08",
count: 32648,
},
{
date: "2010-08-09",
count: 32812,
},
{
date: "2010-08-10",
count: 32764,
},
{
date: "2010-08-11",
count: 32668,
},
{
date: "2010-08-12",
count: 32484,
},
{
date: "2010-08-13",
count: 32167,
},
{
date: "2010-08-14",
count: 32304,
},
{
date: "2010-08-15",
count: 32446,
},
{
date: "2010-08-16",
count: 32670,
},
{
date: "2010-08-17",
count: 32778,
},
{
date: "2010-08-18",
count: 32756,
},
{
date: "2010-08-19",
count: 32580,
},
];
let el;
const formatDate4 = d3.timeFormat("%m%d%Y");
const formatDate5 = d3.timeFormat("%Y-%m-%d");
const formatDate6 = d3.timeFormat("%b %d, %Y");
const bisectDate = d3.bisector(function (d) {
return d.date;
}).left;
var parseTime = d3.timeParse("%Y-%m-%d");
const pageWidth = window.innerWidth;
let wrapWidth = 969,
mapRatio = 0.51;
let margin = { top: 0, right: 0, bottom: 10, left: 0 };
let timeW = 960,
timeH = 450;
let timeMargin = { top: 20, right: 250, bottom: 80, left: 60 },
timeMargin2 = { top: 410, right: 250, bottom: 30, left: 60 },
timeWidth = timeW - timeMargin.left - timeMargin.right,
timeHeight = timeH - timeMargin.top - timeMargin.bottom,
timeHeight2 = timeH - timeMargin2.top - timeMargin2.bottom;
let timeseries = d3
.select(el)
.attr("id", "timeseries")
.attr("width", timeWidth + timeMargin.left + timeMargin.right)
.attr("height", timeHeight + timeMargin.top + timeMargin.bottom);
var graph = timeseries
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var parseDate = d3.timeParse("%Y-%m-%d");
var x2 = d3.scaleTime().range([0, timeWidth]),
x3 = d3.scaleTime().range([0, timeWidth]),
y2 = d3.scaleLinear().range([timeHeight, 0]),
y3 = d3.scaleLinear().range([timeHeight2, 0]);
var xAxis2 = d3.axisBottom(x2).ticks(5).tickFormat(d3.timeFormat("%b %d %Y")),
yAxis2 = d3.axisLeft(y2).ticks(3);
timeseries
.append("defs")
.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", timeWidth)
.attr("height", timeHeight);
var chartfocus = timeseries
.append("g")
.attr("class", "chartfocus")
.attr(
"transform",
"translate(" + timeMargin.left + "," + timeMargin.top + ")"
);
const natlData = data;
console.log("natlData", natlData);
updateChart(natlData);
function updateChart(data) {
const dataOld = data.map((a) => ({ ...a }));
data.forEach((d) => {
d.date = parseTime(d.date);
});
x2.domain(
d3.extent(data, function (d) {
return d.date;
})
);
const charttooltip = d3
.select("#time")
.append("div")
.attr("class", "charttooltip");
const chartguideline = d3
.select("body")
.append("div")
.attr("class", "chartguideline");
let topY = d3.max(data, (d) => d.count);
timeseries.selectAll(".axis").remove();
let bisectDate = d3.bisector(function (d) {
return d.date;
}).left;
chartfocus
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseout", mouseout);
y2.domain([0, topY]).nice();
d3.select(".axis--y").transition(1000).call(yAxis2);
d3.selectAll(".bar").remove();
let dailyBars = chartfocus
.selectAll("bar")
.data(data)
.enter()
.append("rect")
.attr("clip-path", "url(#clip)")
.attr("class", (d) => "bar d" + formatDate4(d.date));
y2.domain([0, topY]).nice();
d3.select(".axis--y").transition(1000).call(yAxis2);
dailyBars
.transition()
.attr("width", (d) => x2(d3.timeDay.offset(d.date)) - x2(d.date))
.attr("x", (d) => x2(d.date))
.attr("y", (d) => y2(d.count))
.attr("height", (d) => timeHeight - y2(d.count));
chartfocus
.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + timeHeight + ")")
.call(xAxis2);
chartfocus.append("g").attr("class", "axis axis--y").call(yAxis2);
chartfocus
.append("rect")
.attr("class", "chartzoom")
.attr("width", timeWidth)
.attr("height", timeHeight);
d3.selectAll(".focus").remove();
function mouseover(event) {}
function mousemove(event, d) {
let outerMargins = pageWidth - wrapWidth;
let outerLeftMargin = outerMargins / 2;
let ex = event.x - timeMargin.left - outerLeftMargin;
var x0 = x2.invert(ex),
i = bisectDate(data, x0, 1),
d0 = data[i - 1],
d1 = data[i],
d = x0 - d0.date > d1.date - x0 ? d1 : d0;
let dateX = d.date;
let dateXSimple = formatDate5(dateX);
indvData = dataOld.filter((d) => d.date == dateXSimple);
dailyCount = [];
dailyCounts = [];
let tooltip_str =
formatDate5(dateX) + "</p>Count: <strong>" + indvData[0].count;
charttooltip
.style("left", event.x - outerLeftMargin + "px")
.style("top", 0)
.attr("x", 0)
.html((d) => tooltip_str);
chartguideline
.style("left", event.x - 1 + "px")
.style("top", timeMargin.top + "px")
.attr("width", (d) => x2(d3.timeDay.offset(dateX)) - x2(dateX))
.style(
"height",
timeHeight + timeMargin.top - timeMargin.bottom + 45 + "px"
)
.attr("x", 0);
charttooltip.style("visibility", "visible");
chartguideline.style("visibility", "visible");
}
function mouseout() {
d3.selectAll(".bar").style("fill", "#aaa");
d3.selectAll(".bar")
.transition()
.attr("width", (d) => x2(d3.timeDay.offset(d.date)) - x2(d.date));
charttooltip.transition().duration(0).style("visibility", "hidden");
chartguideline.transition().duration(0).style("visibility", "hidden");
}
}
</script>
<div id="time">
<div id="timeseries-container">
<svg width="100%" viewBox="0 0 {timeW} {timeH}" id="chart" bind:this={el} />
</div>
</div>
<style>
.chartzoom {
cursor: move;
fill: none;
pointer-events: all;
}
.charttooltip {
position: absolute;
pointer-events: none;
padding: 10px;
background: #fff;
visibility: hidden;
opacity: 0.9;
-moz-box-shadow: 0 0 15px #aaa;
-webkit-box-shadow: 0 0 15px #aaa;
box-shadow: 0 0 15px #aaa;
margin-left: 10px;
}
.charttooltip:before {
right: 100%;
top: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-color: rgba(255, 255, 255, 0);
border-right-color: #ffffff;
border-width: 10px;
margin-top: -10px;
}
.chartguideline {
position: absolute;
width: 1px;
opacity: 0.5;
pointer-events: none;
background: #ef4136;
visibility: hidden;
}
.bar {
fill: #aaa;
stroke-width: 1px;
stroke: #000;
}
[1]: https://svelte.dev/repl/5e39f2a5622a43ba830b055b8fd6acf9?version=4.1.2
</details>
# 答案1
**得分**: 2
你已经非常接近了!只需等待HTML元素被挂载到页面上,然后再执行任何d3渲染逻辑,因为它们会操作DOM。在它们"存在"之前,你不能操作它们,对吗?
我看到你将svg绑定到了`el`变量上。在初始化阶段,它是`undefined`,所以`d3.select(el)`会选择到空内容,这就是原因。
要解决这个问题,只需将任何涉及DOM的操作包装在`onMount`生命周期回调中。
<details>
<summary>英文:</summary>
You're very close! Just need to wait for the HTML elements to be mounted onto the page before executing any d3 render logics, cus they manipulate the DOM. You can't manipulate things before they even "exist", can you?
I see that you bind the svg to `el` variable. It's `undefined` during the initialization phase, so `d3.select(el)` would select nothing, that's why.
To fix it, simply wrap anything DOM related in the `onMount` lifecycle callback.
import { onMount } from "svelte"
// ......
let el;
let timeW = 960,
timeH = 450;
// ......
onMount(() => {
let timeseries = d3
.select(el)
// ...
})
答案2 {#2}
得分: 1
你也可以只使用一个action。这不需要任何导入、状态变量声明或this
绑定:
<script>
// ...
function initialize(svg) {
d3.select(svg)...
}
</script>
<svg use:initialize ...>
英文:
You also just use an action. This does not require any imports, state variable declarations or this
bindings:
<script>
// ...
function initialize(svg) {
d3.select(svg)...
}
</script>
<svg use:initialize ...>
(Also, by using d3 like this you are losing most of the advantages of Svelte. Unless any of its advanced algorithms are used, I would recommend just building the SVG directly in Svelte markup. That way it stays declarative and more readable.)