没有 "flipping" 的最小画笔大小
Minimum brush size without "flipping"
我正在尝试设置最小画笔大小,但没有如下 gif 中所示的“翻转”。我发现数据“跳跃”导致翻转在视觉上令人困惑,并且希望如果用户继续拖动画笔手柄则什么也不会发生。
我看过一些设置最小画笔大小的示例(例如这个问题的答案:),但它们都允许这种“翻转”或“镜像”。
这是答案中引用的更新后的 "Brush and Zoom" 示例。我知道代码正在计算画笔选择相对于总宽度的百分比,但我没有看到在哪里对值进行标准化以启用镜像(如果这有意义的话)。
刷机时如何避免'flip'?
d3.event.selection
对于 brush
处理程序返回当前 brush selection:
Returns the current brush selection for the specified node. ... For a two-dimensional brush, it is [[x0, y0], [x1, y1]], where x0 is the minimum x-value, y0 is the minimum y-value, x1 is the maximum x-value, and y1 is the maximum y-value. For an x-brush, it is [x0, x1]; ...
因此,在您的动画中 - 当 'right' 手侧变为 'left' 时,为 brushX 选择返回的基础数组将进行调整,以便最小值始终为索引 0,最大值为始终索引 1.
您可以通过例如:
检测到这一点
const flipped = (s[1] === previousS0) || (s[0] === previousS1)
您可以在下面的代码片段中看到这一点 - 在第一个画笔中,在画笔的 right-hand 边缘越过左侧(然后成为左侧边缘)的点上缓慢移动 - flipped
发生这种情况时,输出将立即为真。
在第二个画笔中,您可以通过检查画笔宽度百分比小于阈值 或 flipped
变量 [=21] 来防止它发生=].
const width = 400;
const height = 32;
const margin = {top: 20, bottom: 40, left: 20, right: 20}
let previousS0_1;
let previousS1_1;
let previousS0_2;
let previousS1_2;
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
const svg1 = d3.select("#svg1")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const svg2 = d3.select("#svg2")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const xScale = d3.scaleLinear()
.range([0, width])
.domain(d3.extent(data));
const xAxis1 = svg1.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale));
const xAxis2 = svg2.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale));
const brush1 = d3.brushX()
.extent([[0, 0], [width, height]])
.on("brush end", brushed1);
const brush2 = d3.brushX()
.extent([[0, 0], [width, height]])
.on("brush end", brushed2);
const context1 = svg1.append("g")
const context2 = svg2.append("g")
const brushGroup1 = context1.append("g")
.call(brush1)
.call(brush1.move, [xScale.range()[0] + 80, xScale.range()[1] - 80]);
const brushGroup2 = context2.append("g")
.call(brush2)
.call(brush2.move, [xScale.range()[0] + 80, xScale.range()[1] - 80]);
function brushed1() {
const s = d3.event.selection || xScale.range();
const brushPc = (((s[1] - s[0]) / width) * 100);
const flipped = (s[1] === previousS0_1) || (s[0] === previousS1_1)
let str = "";
str += `prevS: ${JSON.stringify([previousS0_1, previousS1_1])}`;
str += `s: ${JSON.stringify(s)}`;
str += ` flip: ${flipped}`;
d3.select("#output1").html(`<pre>s: ${str}</pre>`);
previousS0_1 = s[0];
previousS1_1 = s[1];
}
function brushed2() {
const s = d3.event.selection || xScale.range();
const brushPc = (((s[1] - s[0]) / width) * 100);
const flipped = (s[1] === previousS0_2) || (s[0] === previousS1_2)
let str = "";
str += `prevS: ${JSON.stringify([previousS0_2, previousS1_2])}`;
str += ` s: ${JSON.stringify(s)}`;
str += ` pc: ${JSON.stringify(brushPc)}`;
d3.select("#output2").html(`<pre>${str}</pre>`);
if (brushPc < 10 || flipped) {
brushGroup2.call(brush1.move, [previousS0_2, previousS1_2]);
return;
}
previousS0_2 = s[0];
previousS1_2 = s[1];
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div>
<div id="output1"></div>
<svg id="svg1"></svg>
</div>
<div>
<div id="output2"></div>
<svg id="svg2"></svg>
</div>
您也可以在原始 bl.ocks 示例的 brushed
事件中添加此逻辑 - 见下文:
function brushed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
var s = d3.event.selection || x2.range();
var brushPc = (((s[1] - s[0])/width)*100);
var flipped = (s[1] === previousS0) || (s[0] === previousS1); // <-- add 'flipped' check
if(brushPc < 10 || flipped){ // <-- consider along with brush percentage
brushGroup.call(brush.move, [previousS0, previousS1]);
return;
};
previousS0 = s[0];
previousS1 = s[1];
x.domain(s.map(x2.invert, x2));
focus.select(".area").attr("d", area);
focus.select(".axis--x").call(xAxis);
svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
.scale(width / (s[1] - s[0]))
.translate(-s[0], 0));
}
工作示例(根据@Gerardo-Furtado ):
let previousS0, previousS1, brushGroup;
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 100, left: 40},
margin2 = {top: 120, right: 20, bottom: 30, left: 40},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
height2 = +svg.attr("height") - margin2.top - margin2.bottom;
var parseDate = d3.timeParse("%b %Y");
var x = d3.scaleTime().range([0, width]),
x2 = d3.scaleTime().range([0, width]),
y = d3.scaleLinear().range([height, 0]),
y2 = d3.scaleLinear().range([height2, 0]);
var xAxis = d3.axisBottom(x),
xAxis2 = d3.axisBottom(x2),
yAxis = d3.axisLeft(y);
var brush = d3.brushX()
.extent([[0, 0], [width, height2]])
.on("brush end", brushed);
var zoom = d3.zoom()
.scaleExtent([1, Infinity])
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", zoomed);
var area = d3.area()
.curve(d3.curveMonotoneX)
.x(function(d) { return x(d.date); })
.y0(height)
.y1(function(d) { return y(d.price); });
var area2 = d3.area()
.curve(d3.curveMonotoneX)
.x(function(d) { return x2(d.date); })
.y0(height2)
.y1(function(d) { return y2(d.price); });
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var focus = svg.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var context = svg.append("g")
.attr("class", "context")
.attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
// fake data
var data = [];
for (var ix=0; ix<600; ix++) {
var yr = 2000 + Math.floor(ix / 12) + "";
var mth = ((ix % 12) + 1);
mth = (mth < 10 ? "0" : "") + mth;
var dt = new Date(`${yr}-${mth}-01`);
var price = Math.floor(Math.random() * 5) + 1
data.push({
date: dt,
price: price
});
}
//d3.csv("sp500.csv", type, function(error, data) {
//if (error) throw error;
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.price; })]);
x2.domain(x.domain());
y2.domain(y.domain());
focus.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area);
focus.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
focus.append("g")
.attr("class", "axis axis--y")
.call(yAxis);
context.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area2);
context.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height2 + ")")
.call(xAxis2);
brushGroup = context.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, x.range());
svg.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
//});
function brushed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
var s = d3.event.selection || x2.range();
var brushPc = (((s[1] - s[0])/width)*100);
var flipped = (s[1] === previousS0) || (s[0] === previousS1);
if(brushPc < 10 || flipped){
brushGroup.call(brush.move, [previousS0, previousS1]);
return;
};
previousS0 = s[0];
previousS1 = s[1];
x.domain(s.map(x2.invert, x2));
focus.select(".area").attr("d", area);
focus.select(".axis--x").call(xAxis);
svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
.scale(width / (s[1] - s[0]))
.translate(-s[0], 0));
}
function zoomed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
var t = d3.event.transform;
x.domain(t.rescaleX(x2).domain());
focus.select(".area").attr("d", area);
focus.select(".axis--x").call(xAxis);
context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
}
function type(d) {
d.date = parseDate(d.date);
d.price = +d.price;
return d;
};
.area {
fill: steelblue;
clip-path: url(#clip);
}
.zoom {
cursor: move;
fill: none;
pointer-events: all;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg width="400" height="200"></svg>
我正在尝试设置最小画笔大小,但没有如下 gif 中所示的“翻转”。我发现数据“跳跃”导致翻转在视觉上令人困惑,并且希望如果用户继续拖动画笔手柄则什么也不会发生。
我看过一些设置最小画笔大小的示例(例如这个问题的答案:
这是答案中引用的更新后的 "Brush and Zoom" 示例。我知道代码正在计算画笔选择相对于总宽度的百分比,但我没有看到在哪里对值进行标准化以启用镜像(如果这有意义的话)。
刷机时如何避免'flip'?
d3.event.selection
对于 brush
处理程序返回当前 brush selection:
Returns the current brush selection for the specified node. ... For a two-dimensional brush, it is [[x0, y0], [x1, y1]], where x0 is the minimum x-value, y0 is the minimum y-value, x1 is the maximum x-value, and y1 is the maximum y-value. For an x-brush, it is [x0, x1]; ...
因此,在您的动画中 - 当 'right' 手侧变为 'left' 时,为 brushX 选择返回的基础数组将进行调整,以便最小值始终为索引 0,最大值为始终索引 1.
您可以通过例如:
检测到这一点const flipped = (s[1] === previousS0) || (s[0] === previousS1)
您可以在下面的代码片段中看到这一点 - 在第一个画笔中,在画笔的 right-hand 边缘越过左侧(然后成为左侧边缘)的点上缓慢移动 - flipped
发生这种情况时,输出将立即为真。
在第二个画笔中,您可以通过检查画笔宽度百分比小于阈值 或 flipped
变量 [=21] 来防止它发生=].
const width = 400;
const height = 32;
const margin = {top: 20, bottom: 40, left: 20, right: 20}
let previousS0_1;
let previousS1_1;
let previousS0_2;
let previousS1_2;
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
const svg1 = d3.select("#svg1")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const svg2 = d3.select("#svg2")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const xScale = d3.scaleLinear()
.range([0, width])
.domain(d3.extent(data));
const xAxis1 = svg1.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale));
const xAxis2 = svg2.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale));
const brush1 = d3.brushX()
.extent([[0, 0], [width, height]])
.on("brush end", brushed1);
const brush2 = d3.brushX()
.extent([[0, 0], [width, height]])
.on("brush end", brushed2);
const context1 = svg1.append("g")
const context2 = svg2.append("g")
const brushGroup1 = context1.append("g")
.call(brush1)
.call(brush1.move, [xScale.range()[0] + 80, xScale.range()[1] - 80]);
const brushGroup2 = context2.append("g")
.call(brush2)
.call(brush2.move, [xScale.range()[0] + 80, xScale.range()[1] - 80]);
function brushed1() {
const s = d3.event.selection || xScale.range();
const brushPc = (((s[1] - s[0]) / width) * 100);
const flipped = (s[1] === previousS0_1) || (s[0] === previousS1_1)
let str = "";
str += `prevS: ${JSON.stringify([previousS0_1, previousS1_1])}`;
str += `s: ${JSON.stringify(s)}`;
str += ` flip: ${flipped}`;
d3.select("#output1").html(`<pre>s: ${str}</pre>`);
previousS0_1 = s[0];
previousS1_1 = s[1];
}
function brushed2() {
const s = d3.event.selection || xScale.range();
const brushPc = (((s[1] - s[0]) / width) * 100);
const flipped = (s[1] === previousS0_2) || (s[0] === previousS1_2)
let str = "";
str += `prevS: ${JSON.stringify([previousS0_2, previousS1_2])}`;
str += ` s: ${JSON.stringify(s)}`;
str += ` pc: ${JSON.stringify(brushPc)}`;
d3.select("#output2").html(`<pre>${str}</pre>`);
if (brushPc < 10 || flipped) {
brushGroup2.call(brush1.move, [previousS0_2, previousS1_2]);
return;
}
previousS0_2 = s[0];
previousS1_2 = s[1];
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div>
<div id="output1"></div>
<svg id="svg1"></svg>
</div>
<div>
<div id="output2"></div>
<svg id="svg2"></svg>
</div>
您也可以在原始 bl.ocks 示例的 brushed
事件中添加此逻辑 - 见下文:
function brushed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
var s = d3.event.selection || x2.range();
var brushPc = (((s[1] - s[0])/width)*100);
var flipped = (s[1] === previousS0) || (s[0] === previousS1); // <-- add 'flipped' check
if(brushPc < 10 || flipped){ // <-- consider along with brush percentage
brushGroup.call(brush.move, [previousS0, previousS1]);
return;
};
previousS0 = s[0];
previousS1 = s[1];
x.domain(s.map(x2.invert, x2));
focus.select(".area").attr("d", area);
focus.select(".axis--x").call(xAxis);
svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
.scale(width / (s[1] - s[0]))
.translate(-s[0], 0));
}
工作示例(根据@Gerardo-Furtado
let previousS0, previousS1, brushGroup;
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 100, left: 40},
margin2 = {top: 120, right: 20, bottom: 30, left: 40},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
height2 = +svg.attr("height") - margin2.top - margin2.bottom;
var parseDate = d3.timeParse("%b %Y");
var x = d3.scaleTime().range([0, width]),
x2 = d3.scaleTime().range([0, width]),
y = d3.scaleLinear().range([height, 0]),
y2 = d3.scaleLinear().range([height2, 0]);
var xAxis = d3.axisBottom(x),
xAxis2 = d3.axisBottom(x2),
yAxis = d3.axisLeft(y);
var brush = d3.brushX()
.extent([[0, 0], [width, height2]])
.on("brush end", brushed);
var zoom = d3.zoom()
.scaleExtent([1, Infinity])
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", zoomed);
var area = d3.area()
.curve(d3.curveMonotoneX)
.x(function(d) { return x(d.date); })
.y0(height)
.y1(function(d) { return y(d.price); });
var area2 = d3.area()
.curve(d3.curveMonotoneX)
.x(function(d) { return x2(d.date); })
.y0(height2)
.y1(function(d) { return y2(d.price); });
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var focus = svg.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var context = svg.append("g")
.attr("class", "context")
.attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
// fake data
var data = [];
for (var ix=0; ix<600; ix++) {
var yr = 2000 + Math.floor(ix / 12) + "";
var mth = ((ix % 12) + 1);
mth = (mth < 10 ? "0" : "") + mth;
var dt = new Date(`${yr}-${mth}-01`);
var price = Math.floor(Math.random() * 5) + 1
data.push({
date: dt,
price: price
});
}
//d3.csv("sp500.csv", type, function(error, data) {
//if (error) throw error;
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.price; })]);
x2.domain(x.domain());
y2.domain(y.domain());
focus.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area);
focus.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
focus.append("g")
.attr("class", "axis axis--y")
.call(yAxis);
context.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area2);
context.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height2 + ")")
.call(xAxis2);
brushGroup = context.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, x.range());
svg.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
//});
function brushed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
var s = d3.event.selection || x2.range();
var brushPc = (((s[1] - s[0])/width)*100);
var flipped = (s[1] === previousS0) || (s[0] === previousS1);
if(brushPc < 10 || flipped){
brushGroup.call(brush.move, [previousS0, previousS1]);
return;
};
previousS0 = s[0];
previousS1 = s[1];
x.domain(s.map(x2.invert, x2));
focus.select(".area").attr("d", area);
focus.select(".axis--x").call(xAxis);
svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
.scale(width / (s[1] - s[0]))
.translate(-s[0], 0));
}
function zoomed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
var t = d3.event.transform;
x.domain(t.rescaleX(x2).domain());
focus.select(".area").attr("d", area);
focus.select(".axis--x").call(xAxis);
context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
}
function type(d) {
d.date = parseDate(d.date);
d.price = +d.price;
return d;
};
.area {
fill: steelblue;
clip-path: url(#clip);
}
.zoom {
cursor: move;
fill: none;
pointer-events: all;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg width="400" height="200"></svg>