I have a bar chart in d3 that uses focus/context. When the user brushes to select the dates, I want to snap to the dates available, rather than them being able to brush to any date between the beginning and end. This brush snapping example uses a twelve hour interval. In my case, there is no regular interval. Is there a way to snap to the dates with data?
<code>const margin = {
top: 20,
right: 20,
bottom: 90,
left: 50
},
margin2 = {
top: 230,
right: 20,
bottom: 30,
left: 50
},
width = 960 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom,
height2 = 300 - margin2.top - margin2.bottom;
const parseTime = d3.timeParse("%Y-%m-%d %H:%M");
const x = d3.scaleTime().range([0, width]),
x2 = d3.scaleTime().range([0, width]),
y = d3.scaleLinear().range([height, 0]),
y2 = d3.scaleLinear().range([height2, 0]),
dur = d3.scaleLinear().range([0, 12]);
const xAxis = d3.axisBottom(x).tickSize(0),
xAxis2 = d3.axisBottom(x2).tickSize(0),
yAxis = d3.axisLeft(y).tickSize(0);
const brush = d3.brushX()
.extent([
[0, 0],
[width, height2]
])
.on("start brush end", brushed);
const svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
const focus = svg.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const context = svg.append("g")
.attr("class", "context")
.attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
d3.csv("https://raw.githubusercontent.com/sprucegoose1/sample-data/main/data.csv").then((data) => {
const parseTime = d3.timeParse("%Y-%m-%d %H:%M");
const mouseoverTime = d3.timeFormat("%a %e %b %Y %H:%M");
const minTime = d3.timeFormat("%b%e, %Y");
const parseDate = d3.timeParse("%b %Y");
data.forEach((d) => {
d.date = parseTime(d.date);
d.end = parseTime(d.end);
d.distance = +d.distance;
return d;
},
(error, data) => {
if (error) throw error;
})
let total = 0;
data.forEach((d) => total = d.distance + total);
const minDate = d3.min(data, d => d.date)
const xMin = d3.min(data, d => d.date)
const yMax = Math.max(20, d3.max(data, d => d.distance))
x.domain([xMin, d3.max(data, d => d.date)])
y.domain([0, yMax]);
x2.domain(x.domain());
y2.domain(y.domain());
var rects = focus.append("g");
rects.attr("clip-path", "url(#clip)");
rects.selectAll("rects")
.data(data)
.enter().append("rect")
.style("fill","royalblue")
.attr("class", "rects")
.attr("x", d => x(d.date))
.attr("y", d => y(d.distance))
.attr("width", 10)
.attr("height", d => height - y(d.distance))
focus.append("g")
.attr("class", "axis x-axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
focus.append("g")
.attr("class", "axis axis--y")
.call(yAxis);
focus.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x", 0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Distance in meters");
svg.append("text")
.attr("transform",
"translate(" + ((width + margin.right + margin.left) / 2) + " ," +
(height + margin.top + margin.bottom) + ")")
.style("text-anchor", "middle")
.text("Date");
var rects = context.append("g");
rects.attr("clip-path", "url(#clip)");
rects.selectAll("rects")
.data(data)
.enter().append("rect")
.style("fill", "royalblue")
.attr("class", "rects")
.attr("x", d => x2(d.date))
.attr("y", d => y2(d.distance))
.attr("width", 10)
.attr("height", d => height2 - y2(d.distance));
context.append("g")
.attr("class", "axis x-axis")
.attr("transform", "translate(0," + height2 + ")")
.call(xAxis2);
context.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, x.range());
});
function brushed(event) {
var s = event.selection || x2.range();
x.domain(s.map(x2.invert, x2));
focus.selectAll(".rects")
.attr("x", d => x(d.date))
.attr("y", d => y(d.distance))
.attr("width", 10)
.attr("height", d => height - y(d.distance))
focus.select(".x-axis").call(xAxis);
var e = event.selection;
var selectedrects = focus.selectAll('.rects').filter(() => {
var xValue = this.getAttribute('x');
return e[0] <= xValue && xValue <= e[1];
});
}</code>
<code>const margin = {
top: 20,
right: 20,
bottom: 90,
left: 50
},
margin2 = {
top: 230,
right: 20,
bottom: 30,
left: 50
},
width = 960 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom,
height2 = 300 - margin2.top - margin2.bottom;
const parseTime = d3.timeParse("%Y-%m-%d %H:%M");
const x = d3.scaleTime().range([0, width]),
x2 = d3.scaleTime().range([0, width]),
y = d3.scaleLinear().range([height, 0]),
y2 = d3.scaleLinear().range([height2, 0]),
dur = d3.scaleLinear().range([0, 12]);
const xAxis = d3.axisBottom(x).tickSize(0),
xAxis2 = d3.axisBottom(x2).tickSize(0),
yAxis = d3.axisLeft(y).tickSize(0);
const brush = d3.brushX()
.extent([
[0, 0],
[width, height2]
])
.on("start brush end", brushed);
const svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
const focus = svg.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const context = svg.append("g")
.attr("class", "context")
.attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
d3.csv("https://raw.githubusercontent.com/sprucegoose1/sample-data/main/data.csv").then((data) => {
const parseTime = d3.timeParse("%Y-%m-%d %H:%M");
const mouseoverTime = d3.timeFormat("%a %e %b %Y %H:%M");
const minTime = d3.timeFormat("%b%e, %Y");
const parseDate = d3.timeParse("%b %Y");
data.forEach((d) => {
d.date = parseTime(d.date);
d.end = parseTime(d.end);
d.distance = +d.distance;
return d;
},
(error, data) => {
if (error) throw error;
})
let total = 0;
data.forEach((d) => total = d.distance + total);
const minDate = d3.min(data, d => d.date)
const xMin = d3.min(data, d => d.date)
const yMax = Math.max(20, d3.max(data, d => d.distance))
x.domain([xMin, d3.max(data, d => d.date)])
y.domain([0, yMax]);
x2.domain(x.domain());
y2.domain(y.domain());
var rects = focus.append("g");
rects.attr("clip-path", "url(#clip)");
rects.selectAll("rects")
.data(data)
.enter().append("rect")
.style("fill","royalblue")
.attr("class", "rects")
.attr("x", d => x(d.date))
.attr("y", d => y(d.distance))
.attr("width", 10)
.attr("height", d => height - y(d.distance))
focus.append("g")
.attr("class", "axis x-axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
focus.append("g")
.attr("class", "axis axis--y")
.call(yAxis);
focus.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x", 0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Distance in meters");
svg.append("text")
.attr("transform",
"translate(" + ((width + margin.right + margin.left) / 2) + " ," +
(height + margin.top + margin.bottom) + ")")
.style("text-anchor", "middle")
.text("Date");
var rects = context.append("g");
rects.attr("clip-path", "url(#clip)");
rects.selectAll("rects")
.data(data)
.enter().append("rect")
.style("fill", "royalblue")
.attr("class", "rects")
.attr("x", d => x2(d.date))
.attr("y", d => y2(d.distance))
.attr("width", 10)
.attr("height", d => height2 - y2(d.distance));
context.append("g")
.attr("class", "axis x-axis")
.attr("transform", "translate(0," + height2 + ")")
.call(xAxis2);
context.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, x.range());
});
function brushed(event) {
var s = event.selection || x2.range();
x.domain(s.map(x2.invert, x2));
focus.selectAll(".rects")
.attr("x", d => x(d.date))
.attr("y", d => y(d.distance))
.attr("width", 10)
.attr("height", d => height - y(d.distance))
focus.select(".x-axis").call(xAxis);
var e = event.selection;
var selectedrects = focus.selectAll('.rects').filter(() => {
var xValue = this.getAttribute('x');
return e[0] <= xValue && xValue <= e[1];
});
}</code>
const margin = {
top: 20,
right: 20,
bottom: 90,
left: 50
},
margin2 = {
top: 230,
right: 20,
bottom: 30,
left: 50
},
width = 960 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom,
height2 = 300 - margin2.top - margin2.bottom;
const parseTime = d3.timeParse("%Y-%m-%d %H:%M");
const x = d3.scaleTime().range([0, width]),
x2 = d3.scaleTime().range([0, width]),
y = d3.scaleLinear().range([height, 0]),
y2 = d3.scaleLinear().range([height2, 0]),
dur = d3.scaleLinear().range([0, 12]);
const xAxis = d3.axisBottom(x).tickSize(0),
xAxis2 = d3.axisBottom(x2).tickSize(0),
yAxis = d3.axisLeft(y).tickSize(0);
const brush = d3.brushX()
.extent([
[0, 0],
[width, height2]
])
.on("start brush end", brushed);
const svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
const focus = svg.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const context = svg.append("g")
.attr("class", "context")
.attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
d3.csv("https://raw.githubusercontent.com/sprucegoose1/sample-data/main/data.csv").then((data) => {
const parseTime = d3.timeParse("%Y-%m-%d %H:%M");
const mouseoverTime = d3.timeFormat("%a %e %b %Y %H:%M");
const minTime = d3.timeFormat("%b%e, %Y");
const parseDate = d3.timeParse("%b %Y");
data.forEach((d) => {
d.date = parseTime(d.date);
d.end = parseTime(d.end);
d.distance = +d.distance;
return d;
},
(error, data) => {
if (error) throw error;
})
let total = 0;
data.forEach((d) => total = d.distance + total);
const minDate = d3.min(data, d => d.date)
const xMin = d3.min(data, d => d.date)
const yMax = Math.max(20, d3.max(data, d => d.distance))
x.domain([xMin, d3.max(data, d => d.date)])
y.domain([0, yMax]);
x2.domain(x.domain());
y2.domain(y.domain());
var rects = focus.append("g");
rects.attr("clip-path", "url(#clip)");
rects.selectAll("rects")
.data(data)
.enter().append("rect")
.style("fill","royalblue")
.attr("class", "rects")
.attr("x", d => x(d.date))
.attr("y", d => y(d.distance))
.attr("width", 10)
.attr("height", d => height - y(d.distance))
focus.append("g")
.attr("class", "axis x-axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
focus.append("g")
.attr("class", "axis axis--y")
.call(yAxis);
focus.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x", 0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Distance in meters");
svg.append("text")
.attr("transform",
"translate(" + ((width + margin.right + margin.left) / 2) + " ," +
(height + margin.top + margin.bottom) + ")")
.style("text-anchor", "middle")
.text("Date");
var rects = context.append("g");
rects.attr("clip-path", "url(#clip)");
rects.selectAll("rects")
.data(data)
.enter().append("rect")
.style("fill", "royalblue")
.attr("class", "rects")
.attr("x", d => x2(d.date))
.attr("y", d => y2(d.distance))
.attr("width", 10)
.attr("height", d => height2 - y2(d.distance));
context.append("g")
.attr("class", "axis x-axis")
.attr("transform", "translate(0," + height2 + ")")
.call(xAxis2);
context.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, x.range());
});
function brushed(event) {
var s = event.selection || x2.range();
x.domain(s.map(x2.invert, x2));
focus.selectAll(".rects")
.attr("x", d => x(d.date))
.attr("y", d => y(d.distance))
.attr("width", 10)
.attr("height", d => height - y(d.distance))
focus.select(".x-axis").call(xAxis);
var e = event.selection;
var selectedrects = focus.selectAll('.rects').filter(() => {
var xValue = this.getAttribute('x');
return e[0] <= xValue && xValue <= e[1];
});
}
<code> body {
font-family: avenir next, sans-serif;
font-size: 12px;
}
.axis {
stroke-width: 0.5px;
stroke: #888;
font: 10px avenir next, sans-serif;
}
.axis>path {
stroke: #888;
}
.handle {
width: 6px !important;
fill: #000 !important;
margin-left: 0px !important;
display: block;
}</code>
<code> body {
font-family: avenir next, sans-serif;
font-size: 12px;
}
.axis {
stroke-width: 0.5px;
stroke: #888;
font: 10px avenir next, sans-serif;
}
.axis>path {
stroke: #888;
}
.handle {
width: 6px !important;
fill: #000 !important;
margin-left: 0px !important;
display: block;
}</code>
body {
font-family: avenir next, sans-serif;
font-size: 12px;
}
.axis {
stroke-width: 0.5px;
stroke: #888;
font: 10px avenir next, sans-serif;
}
.axis>path {
stroke: #888;
}
.handle {
width: 6px !important;
fill: #000 !important;
margin-left: 0px !important;
display: block;
}
<code><script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<div id="totalDistance">
</div></code>
<code><script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<div id="totalDistance">
</div></code>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<div id="totalDistance">
</div>