【发布时间】:2022-10-12 23:04:50
【问题描述】:
我想以对数刻度绘制 Y 值,其中值包括负数和正数。
这可能吗?将 min 设置为负值无效,它始终从零开始
【问题讨论】:
标签: chart.js
我想以对数刻度绘制 Y 值,其中值包括负数和正数。
这可能吗?将 min 设置为负值无效,它始终从零开始
【问题讨论】:
标签: chart.js
查看代码,对数刻度将最小值设置为 0(用于最小值和最大值)。
https://github.com/chartjs/Chart.js/blob/master/src/scales/scale.logarithmic.js#L85-L96
【讨论】:
我最近遇到了同样的问题,我最初使用这个Log2 axis implementation,根据这个github issue,他们不打算在对数轴上支持负值。
我有自己的想法,但经过一番研究后找不到类似的解决方案,所以我决定制作自己的 Log2Axis 版本,它在最新版本 v3.9.1 中运行良好(尤其是参数@987654326 @)。我的方法很简单:
base“2”之间的值将转换为[-1,1],以在从log2转换到y = x / 2时保持连续性(阅读下文),因此,零将为0(不是那个"log(0) = 0",但由于使用日志轴的主要原因是同时查看大值和小值,因此受限于负值的日志限制是没有意义的!)。-2 的负值将简单地转换为正值,同时保留它们的符号,如下所示:Math.sign(v) * Math.log2( Math.abs(v) )
这样,我们可以在同一个区域中同时拥有正负、大值和小值,下面是一个活生生的例子:它是如何工作的:
//LogAxis definition
class LogAxis extends Chart.Scale{
constructor(cfg){ super(cfg); this._startValue = undefined; this._valueRange = 0;
const log = cfg.chart.config.options.scales.y.log || [2,Math.log2];
this._base = log[0]; this._log = log[1];}
parse(raw, index){
const value = Chart.LinearScale.prototype.parse.apply(this, [raw, index]);
return isFinite(value) ? value : null;
}
determineDataLimits(){
const {min, max} = this.getMinMax(true);
this.min = isFinite(min) ? min : null; this.max = isFinite(max) ? max : null;
}
buildTicks(){
const ticks = [], aMin=Math.abs(this.min), aMax=Math.abs(this.max);
if(aMin<=this._base){ticks.push({value: this._base*Math.sign(this.min)});}
let v, power = Math.floor( (aMin>this._base ? Math.sign(this.min):1)*this._log( this.options.beginAtZero && this.min>0 ? 1 : (aMin>this._base ? aMin : 1) )),
maxPower = Math.ceil( (aMax>this._base ? Math.sign(this.max):1)*this._log( this.options.beginAtZero && this.max<0 ? 1 : (aMax>this._base ? aMax : 1) ));
while(power <= maxPower){
ticks.push({value: Math.sign(power)*Math.pow(this._base, Math.abs(power)) }); power += 1;
}
if(aMax<=this._base){ticks.push({value: this._base*Math.sign(this.max)});}
v=ticks.map(x=>x.value);
this.min = Math.min(...v); this.max = Math.max(...v);
return ticks;
}
getLogVal(v){ var aV=Math.abs(v); return aV>this._base ? Math.sign(v)*this._log(aV) : v/this._base;}
configure(){/* @protected*/
const start = this.min; super.configure();
this._startValue = this.getLogVal(start);
this._valueRange = this.getLogVal(this.max) - this.getLogVal(start);
}
getPixelForValue(value){
if(value === undefined){value = this.min;}
return this.getPixelForDecimal( (this.getLogVal(value) - this._startValue) / this._valueRange);
}
getValueForPixel(pixel){
const decimal = this.getLogVal(this.getDecimalForPixel(pixel));
return Math.pow(2, this._startValue + decimal * this._valueRange);
}
} LogAxis.id = 'mylog'; LogAxis.defaults = {}; Chart.register(LogAxis);
Chart.register(LogAxis);
//Utils and button handlers
const Utils={
RandomNumbers:function(num,min,max){ var i,nums=[];
for(i=0;i<num;i++){
nums.push( min+Math.round( (max-min)*Math.random() ));
}
return nums;
}, Randomize:function(canvId,params){
var chart = Chart.getChart(canvId), min= params[0], max= params[1];
chart.data.datasets.forEach( (d,i) => {
d.data = Utils.RandomNumbers(d.data.length,min,max);
});
chart.update();
}
};
var maxLog2=10000, log2D0=0;
bRandData1.onclick= function(e){Utils.Randomize('chart',[0,maxLog2]);};
bRandData2.onclick= function(e){Utils.Randomize('chart',[-maxLog2,0]);};
bRandData3.onclick= function(e){Utils.Randomize('chart',[-maxLog2,maxLog2]);};
bToggle1.onclick= function(e){
var chart=Chart.getChart("chart"), d0=chart.data.datasets[0].data[0];
if(d0!=0){log2D0=d0}
chart.data.datasets[0].data[0]= d0==0?log2D0:0;
chart.update();
};
bToggle2.onclick= function(e){
var chart=Chart.getChart("chart");
chart.config.options.scales.y.beginAtZero = !chart.config.options.scales.y.beginAtZero;
chart.update();
};
//Chart config
const data ={
labels:['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
datasets:[ { label:'My Log2 Dataset',
data: Utils.RandomNumbers(12,0,1000),
backgroundColor:'blue',
borderColor:'blue',
borderWidth:2}
]
}, config ={ type:'line', data:data,
options:{ responsive:true,
plugins:{
title: {
display:true,
text:'Log Derived Axis Type',
color:'#bb8899'
}, tooltip: {
interaction: {intersect:false, mode:'nearest', axis:'x'}
}
}, scales:{ x:{display:true},
y:{
display:true,
beginAtZero:false,
type:'mylog', // <-- you have to set the scale id 'mylog' to use LogAxis
log: [2, Math.log2] // <-- a config array to change the log type directly from the chart options, without changing the class definition.
// If omitted, it's [2, Math.log2] by default, but you can change it to [10, Math.log10] to use log10 or other log functions.
}
}
}
};
const ctx = document.getElementById('chart').getContext('2d');
new Chart(ctx,config);
body{white-space:nowrap;}
button{margin:3px 3px 3px 0;}
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<canvas id="chart"></canvas>
<br>
Randomize : <button id="bRandData1">Positive</button>
<button id="bRandData2">Negative</button>
<button id="bRandData3">Both</button> |
Toggle : <button id="bToggle1">data[0]=0</button>
<button id="bToggle2">beginAtZero</button>
编辑我最初的解决方案不适用于 [-base,base] 之间的值(其中 base 是日志基数 2、10 等),但只有经过彻底调查后,我才能通过修改转换函数使其工作:
x => Math.sign(x) * Math.log2(x) , for Math.abs(x) > 2
x => x/2 , for Math.abs(x) <= 2
现在您只需在options > scales > y 中设置“log”参数,而无需触及初始类LogAxis,它的工作方式相同,而且我发现 log2 更方便,因为与 log10 相比,它对小值有更多的可见性。
为了更深入地了解它是如何工作的,您可以使用这个online graph maker,其中一个滑块有助于可视化区间 [-base,base] 以及日志函数和“填充”函数y = x / l。
【讨论】: