问题
我有正值和负值,我想按“对数”标度绘制它们。
想象一下一个刻度,刻度均匀地分布于以下值:
-1000, -100, -10, -1, 0, 1, 10, 100, 1000
我想在其中输入0,根据对数定义为-Inf
,这使情况进一步复杂化。
但是,我认为这个请求是不合理的。对于任何数据科学家来说,似乎都想对高度不同的价值观进行反驳,这似乎是一个明智的规模。
如何在d3中创建这样的比例尺和轴?
想法
如果使用a technique like this one,则可能用2个d3.scaleLog()
或3个音阶来巧妙地做到这一点。
我希望可以有一种简便的方法将其与d3.scalePow()
配合在.exponent(0.1)
中,但是除非我混淆了日志规则,否则无法获得{{1} }排除在.scaleLog()
之外(尽管您可以在某些范围内将其近似估算)。
答案 0 :(得分:5)
我们不能拥有像这样的真实对数刻度,甚至不能像这样具有两个对数刻度的组合。我们需要为零值设置一个截止点,这可能会根据您的数据而引入错误。否则,要使这样的标度函数非常简单,只需将零位零值设置为零,就可以为负号和正号调用不同的标度。
这种比例的组合可能看起来像:
var positive = d3.scaleLog()
.domain([1e-6,1000])
.range([height/2,0])
var negative = d3.scaleLog()
.domain([-1000,-1e-6])
.range([height,height/2])
var scale = function(x) {
if (x > 1e-6) return positive(x);
else if (x < -1e-6) return negative(x);
else return height/2; // zero value.
}
还有一个例子:
var width = 500;
var height = 300;
var positive = d3.scaleLog()
.domain([1e-1,1000])
.range([height/2,0])
var negative = d3.scaleLog()
.domain([-1000,-1e-1])
.range([height,height/2])
var scale = function(x) {
if (x > 1e-6) return positive(x);
else if (x < -1e-6) return negative(x);
else return height/2; // zero value.
}
var line = d3.line()
.y(function(d) { return scale(d) })
.x(function(d,i) { return (i); })
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
var data = d3.range(width).map(function(d) {
return (d - 250) * 4;
})
svg.append("path")
.attr("d", line(data) );
path {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
创建单个秤
以上可能是概念证明。
现在更棘手的部分是制作轴。我们可以为上述两个刻度创建轴,并通过某种手动校正将其保留为零。但是,使用上述示例作为示例,使用我们自己的interpolotor创建标尺会更容易。这为我们提供了一个可以为其创建轴的比例尺。我们的插值器可能看起来像:
// Interpolate an output value:
var interpolator = function(a,b) {
var y0 = a;
var y1 = b;
var yd = b-a;
var k = 0.0001;
var positive = d3.scaleLog()
.domain([k,1])
.range([(y0 + y1)/2 ,y1])
var negative = d3.scaleLog()
.domain([-1,-k])
.range([y0, (y1 + y0)/2])
return function(t) {
t = (t - 0.5) * 2; // for an easy range of -1 to 1.
if (t > k) return positive(t);
if (t < -1 + k) return y0;
if (t < -k) return negative(t);
else return (y0 + y1) /2;
}
}
然后我们可以将其应用于常规的旧d3线性比例尺:
d3.scaleLinear().interpolate(interpolator)...
这会将域中的数字插值到我们指定的范围内。它在很大程度上采用了上述内容并将其用作d3插值器:a
,b
是域限制,t
是0到1之间的规范化域,而{{1} }定义零值。有关以下k
的更多信息。
为了获得报价,假设一个不错的舍入域只有十个以十为底的舍入数字,我们可以使用:
k
应用这个,我们得到:
// Set the ticks:
var ticks = [0];
scale.domain().forEach(function(d) {
while (Math.abs(d) >= 1) {
ticks.push(d); d /= 10;
}
})
var margin = {left: 40, top: 10, bottom: 10}
var width = 500;
var height = 300;
var svg = d3.select("body")
.append("svg")
.attr("width",width+margin.left)
.attr("height",height+margin.top+margin.bottom)
.append("g").attr("transform","translate("+[margin.left,margin.top]+")");
var data = d3.range(width).map(function(d) {
return (d - 250) * 4;
})
// Interpolate an output value:
var interpolator = function(a,b) {
var y0 = a;
var y1 = b;
var yd = b-a;
var k = 0.0001;
var positive = d3.scaleLog()
.domain([k,1])
.range([(y0 + y1)/2 ,y1])
var negative = d3.scaleLog()
.domain([-1,-k])
.range([y0, (y1 + y0)/2])
return function(t) {
t = (t - 0.5) * 2; // for an easy range of -1 to 1.
if (t > k) return positive(t);
if (t < -1 + k) return y0;
if (t < -k) return negative(t);
else return (y0 + y1) /2;
}
}
// Create a scale using it:
var scale = d3.scaleLinear()
.range([height,0])
.domain([-1000,1000])
.interpolate(interpolator);
// Set the ticks:
var ticks = [0];
scale.domain().forEach(function(d) {
while (Math.abs(d) >= 1) {
ticks.push(d); d /= 10;
}
})
// Apply the scale:
var line = d3.line()
.y(function(d) { return scale(d) })
.x(function(d,i) { return (i); })
// Draw a line:
svg.append("path")
.attr("d", line(data) )
.attr("class","line");
// Add an axis:
var axis = d3.axisLeft()
.scale(scale)
.tickValues(ticks)
svg.append("g").call(axis);
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
修改k值
O <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
,关于k
的最新消息。需要设置零值。 k
也会更改图形的形状。如果显示规则间隔的刻度,将k
增加十倍会使最小幅度刻度(不为零)的幅度增加十倍。在上面的例子中,k
乘以10会推动刻度线,其幅度比零刻度高1。将其除以十会为0.1个刻度创建空间(当然,需要修改刻度生成器以显示该刻度)。 k
很难解释,所以我希望在那里做得很好。
我将演示尝试更好地进行交流。让我们使用上述方法将最小震级刻度设置为0.1,我们将要修改刻度功能和k:
k
var margin = {left: 40, top: 10, bottom: 10}
var width = 500;
var height = 300;
var svg = d3.select("body")
.append("svg")
.attr("width",width+margin.left)
.attr("height",height+margin.top+margin.bottom)
.append("g").attr("transform","translate("+[margin.left,margin.top]+")");
var data = d3.range(width).map(function(d) {
return (d - 250) * 4;
})
// Interpolate an output value:
var interpolator = function(a,b) {
var y0 = a;
var y1 = b;
var yd = b-a;
var k = 0.00001;
var positive = d3.scaleLog()
.domain([k,1])
.range([(y0 + y1)/2 ,y1])
var negative = d3.scaleLog()
.domain([-1,-k])
.range([y0, (y1 + y0)/2])
return function(t) {
t = (t - 0.5) * 2; // for an easy range of -1 to 1.
if (t > k) {return positive(t)};
if (t < -1 + k) return y0;
if (t < -k) return negative(t);
else return (y0 + y1) /2 //yd;
}
}
// Create a scale using it:
var scale = d3.scaleLinear()
.range([height,0])
.domain([-1000,1000])
.interpolate(interpolator);
// Set the ticks:
var ticks = [0];
scale.domain().forEach(function(d) {
while (Math.abs(d) >= 0.1) {
ticks.push(d); d /= 10;
}
})
// Apply the scale:
var line = d3.line()
.y(function(d) { return scale(d) })
.x(function(d,i) { return (i); })
// Draw a line:
svg.append("path")
.attr("d", line(data) )
.attr("class","line");
// Add an axis:
var axis = d3.axisLeft()
.scale(scale)
.tickValues(ticks)
.ticks(10,".1f")
svg.append("g").call(axis);
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
如果您的域为+/- 1000,并且您希望最小幅度刻度为1(不包括零),则您希望<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
为0.0001或0.1 / 1000。
如果域的正极限和负极限不同,那么我们将需要两个k
值,一个为负截止值,一个为正值。
最后,
k
设置为零的值,在我的示例中,介于-k和+ k之间的t值设置为相同的-0。理想情况下,数据集中不会有很多值,但是如果是这样,您可能会得到诸如以下的行:
每个输入值都不同,但是有许多零输出值,由于我认为是零的边界,产生了视觉伪像。如果在零位范围内只有一个值,如上面的示例(但没有上面的图片),我们会得到更好的结果: