Rails 5、ChartKick 5.0.5 => ChartJS 4.4.1
我在 ChartJS 中有一个曲线图,它在 Y 轴上绘制大约 0 的值,并具有单独的正值最大阈值和负值最小阈值(均为线性)。我试图在曲线内用一种颜色填充阈值上方和下方的曲线内,并用不同的颜色填充阈值之间。
我尝试了几种不同的方法将填充应用于多个数据集,以达到我想要得到的结果。
1:我将曲线数据集分成三个不同的数据集。超过最大阈值、低于最大阈值以及介于两者之间。 我已经很接近了,但我不知道当曲线超出阈值时如何让填充在最小/最大之间的曲线内工作。此外,工具提示将分解的曲线显示为三种不同的线条颜色。
2(下面的代码,结果图片):我在覆盖填充上方和下方时尝试了单个曲线数据集。虽然这确实有效,但调色板与我的应用程序的其余部分和其他图表并不相符。
>! Ruby, using ChartKick interface
def chart_datasets
@chart_datasets ||=
[
{
name: station_name,
data: generate_chart_data(&:speed),
dataset: {
fill: {
target: 'origin',
below: 'rgba(0,204,102, 0.2)',
above: 'rgba(0,204,102, 0.2)'
}
}
},
{
name: 'Max',
data: generate_chart_data(max_limit),
dataset: {
fill: {
target: 0,
below: 'rgba(220,53,69, 0.6)',
above: 'rgba(255,255,255,0)'
}
}
},
{
name: 'Min',
data: generate_chart_data(min_limit),
dataset: {
fill: {
target: 0,
above: 'rgba(220,53,69, 0.6)',
below: 'rgba(255,255,255,0)'
}
}
}
]
end
#2 的结果 - 注意,我从上面的代码中删除了第三个阈值 0,但这就是结果。我需要将棕色区域变成红色:)
如果chart.js配置对象中的javascript代码通过您的框架,您可以创建一个 梯度
backgroundColor
无需复制数据集即可工作,
看这个片段:
const stopsAt = [-2, 2];
const config = {
type: 'line',
data: {
labels: Array.from({length: 100}, (_, i)=>new Date(Date.parse('2024-05-25') + 3 * 24 * 3600 * 1000 * i /100)),
datasets:[
{
data: Array.from({length: 100}, (_, i)=>Math.sin(i/5)*(1+0.45*Math.sin(i/40+2))*3),
fill: true,
backgroundColor: function(context){
const yAxis = context.chart.scales.y;
if(!yAxis.max){
return 'rgba(0,0,0,0)';
}
const ygStops = [yAxis.min, ...stopsAt, yAxis.max].map(yAxis.getPixelForValue, yAxis);
const fractions = ygStops.map(y =>
(y-ygStops[0])/(ygStops[ygStops.length-1]-ygStops[0])
);
const gradientFill = context.chart.ctx.createLinearGradient(0, ygStops[0], 0, ygStops[ygStops.length-1]);
gradientFill.addColorStop(0, 'rgba(220, 53, 69, 0.5)');
gradientFill.addColorStop(fractions[1], 'rgba(220, 53, 69, 0.5)');
gradientFill.addColorStop(fractions[1], 'rgba(0, 204, 102, 0.2)');
gradientFill.addColorStop(fractions[2], 'rgba(0, 204, 102, 0.2)');
gradientFill.addColorStop(fractions[2], 'rgba(220, 53, 69, 0.6)');
gradientFill.addColorStop(1, 'rgba(220, 53, 69, 0.6)');
return gradientFill;
},
pointRadius: 0,
borderWidth: 0
},
{
data: Array(100).fill(2),
borderColor: '#f00',
pointRadius: 0
},
{
data: Array(100).fill(0),
borderColor: '#f00',
borderDash: [10, 7],
pointRadius: 0
},
{
data: Array(100).fill(-2),
borderColor: '#f80',
pointRadius: 0
}]
},
options: {
maintainAspectRatio: false,
scales: {
x: {
type: 'time',
time:{
unit: 'day'
}
},
y:{
min: -5,
max: 5
}
},
plugins:{
legend:{
display: false
}
}
},
};
new Chart('myChart', config);
<div style="min-height:70vh">
<canvas id='myChart'></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
您发布的代码中的解决方案也可以使用。首先是颠倒填充的顺序, 这意味着应用颜色的顺序,因为颜色混合不是对称的。如果背景 (应用第一种颜色)是绿色,透明度为
0.2
,结果会更接近第二种
红色,而不是相反:
const config = {
type: 'line',
data: {
labels: Array.from({length: 100}, (_, i)=>new Date(Date.parse('2024-05-25') + 3 * 24 * 3600 * 1000 * i /100)),
datasets:[
{
data: Array(100).fill(2), // line at y = 2
borderColor: '#f00',
fill: {
target: '+2',
below: 'rgba(220,53,69,0.6)',
above: 'rgba(255,255,255,0)'
},
pointRadius: 0
},
{
data: Array(100).fill(-2), // line at y = -2
borderColor: '#f80',
pointRadius: 0,
fill: {
target: '+1',
above: 'rgba(220,53,69, 0.6)',
below: 'rgba(255,255,255,0)'
},
},
{
data: Array.from({length: 100}, (_, i)=>Math.sin(i/5)*(1+0.45*Math.sin(i/40+2))*3),
fill: {
target: 'origin',
below: 'rgba(0,204,102, 0.2)',
above: 'rgba(0,204,102, 0.2)'
},
pointRadius: 0,
borderWidth: 0
},
{
data: Array(100).fill(0),
borderColor: '#f00',
borderDash: [10, 7],
pointRadius: 0
},
]
},
options: {
maintainAspectRatio: false,
scales: {
x: {
type: 'time',
time:{
unit: 'day'
}
},
y:{
min: -5,
max: 5
}
},
plugins:{
legend:{
display: false
}
}
},
};
new Chart('myChart', config);
<div style="min-height:70vh">
<canvas id='myChart'></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
下一步是将填充颜色
rgba(220, 53, 69, 0.6)
替换为另一种颜色(colorX
,将被计算),这样绿色背景与这种新颜色组合的结果就是您最初想要的颜色。我将使用以下符号将其表示为方程:
rgba(0, 204, 102, 0.2) ⨁ colorX = rgba(220, 53, 69, 0.6)
其中 ⨁ 左侧的颜色是背景颜色或应用的第一个颜色,⨁ 右侧的颜色是前景色或应用的第二个颜色。请注意,为了正确性,实际的第一种颜色是 画布的背景;如果它是白色的,上面两个术语前面都会有一个
rgba(255, 255, 255, 1) ⨁
,但这会简化并且不会影响结果。
不幸的是,上面的方程没有解;但是,如果我们将红色的 alpha 增加到
0.65
,我们将得到一个解决方案:
rgba(0, 204, 102, 0.2) ⨁ rgba(254, 30, 64, 0.56) = rgba(220, 53, 69, 0.65)
所以,如果我们用新计算的
rgba(220, 53, 69, 0.65)
替换 rgba(254, 30, 64, 0.56)
,我们将非常接近原来的结果
预期:
const config = {
type: 'line',
data: {
labels: Array.from({length: 100}, (_, i)=>new Date(Date.parse('2024-05-25') + 3 * 24 * 3600 * 1000 * i /100)),
datasets:[
{
data: Array(100).fill(2), // line at y = 2
borderColor: '#f00',
fill: {
target: '+2',
below: 'rgba(254,30,64,0.56)',
above: 'rgba(255,255,255,0)'
},
pointRadius: 0
},
{
data: Array(100).fill(-2), // line at y = -2
borderColor: '#f80',
pointRadius: 0,
fill: {
target: '+1',
above: 'rgba(254,30,64,0.56)',
below: 'rgba(255,255,255,0)'
},
},
{
data: Array.from({length: 100}, (_, i)=>Math.sin(i/5)*(1+0.45*Math.sin(i/40+2))*3),
fill: {
target: 'origin',
below: 'rgba(0,204,102, 0.2)',
above: 'rgba(0,204,102, 0.2)'
},
pointRadius: 0,
borderWidth: 0
},
{
data: Array(100).fill(0),
borderColor: '#f00',
borderDash: [10, 7],
pointRadius: 0
},
]
},
options: {
maintainAspectRatio: false,
scales: {
x: {
type: 'time',
time:{
unit: 'day'
}
},
y:{
min: -5,
max: 5
}
},
plugins:{
legend:{
display: false
}
}
},
};
new Chart('myChart', config);
<div style="min-height:70vh">
<canvas id='myChart'></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
上面颜色代数的计算可以在下面的代码片段中找到:
const colBck = 'rgba(0, 204, 102, 0.2)';
const colTarget = 'rgba(220, 53, 69, 0.65)';
// solve colBck ⨁ colX = colTarget
// in colBck ⨁ colX, colBck is the background, the first color applied, colX is the foreground, the second color applied.
const colBckCompArr = parseRgba(colBck),
colTargetCompArr = parseRgba(colTarget);
try{
const colXCompArr = invMixRgbaForForeground(colTargetCompArr, colBckCompArr);
const colX = `rgba(${colXCompArr.join(', ')})`;
document.querySelector('#formulaLHS').innerText = `${colX} ⨁\n ${colBck}`;
document.querySelector('#formulaRHS').innerText = ` \n${colTarget}`;
const el1 = document.querySelector('#canvas1'),
ctx1 = el1.getContext("2d");
ctx1.fillStyle = colBck; // apply background
ctx1.fillRect(0, 0, 100, 100);
ctx1.fillStyle = colX; // then apply color X
ctx1.fillRect(0, 0, 100, 100);
}
catch(err){
document.querySelector('#error').innerText = err;
}
const el2 = document.querySelector('#canvas2'),
ctx2 = el2.getContext("2d");
ctx2.fillStyle = colTarget;
ctx2.fillRect(0, 0, 100, 100); // compare to: apply just target color
//////////////////////////////////////////////////////////////////////
// functions used, and some possibly needed for other combinations
function parseRgba(s){
return s.replace(/rgba\(([^\)]+)\)/, '$1').split(',').map(s => s.trim()).map(parseFloat);
}
// formulae from e.g., https://stackoverflow.com/a/727339/16466946
function mixRgba([r1, g1, b1, a1], [r2, g2, b2, a2]){ // color1 is background, color2 is foreground
const ar = a2 + a1 * (1 - a2);
return [
Math.round((r2 * a2 + r1 * a1 * (1 - a2)) / ar), // rx * ax = rr * ar - r1 * a1 * (1-ax)
Math.round((g2 * a2 + g1 * a1 * (1 - a2)) / ar),
Math.round((b2 * a2 + b1 * a1 * (1 - a2)) / ar),
Math.round(ar * 100)/100,
];
}
function invMixRgbaForForeground([r3, g3, b3, a3], [r1, g1, b1, a1]){
// find colX from col1 ⨁ colX = col3
const aX = (a3 - a1)/ (1 - a1);
const [rX, gX, bX] = [
Math.round((r3 * a3 - r1 * a1 * (1 - aX)) / aX),
Math.round((g3 * a3 - g1 * a1 * (1 - aX)) / aX),
Math.round((b3 * a3 - b1 * a1 * (1 - aX)) / aX)
];
if(rX < 0 || rX > 255 || gX < 0 || gX > 255 || bX < 0 || bX > 255 || aX < 0 || aX > 1){
throw new Error(`There's no color to produce that mix `+
`[solution = rgba(${rX}, ${gX}, ${bX}, ${Math.round(aX*100)/100})]`);
}
return[rX, gX, bX, Math.round(aX * 100)/100];
}
function invMixRgbaForBackground([r3, g3, b3, a3], [r2, g2, b2, a2]){
// find colX from colX ⨁ col2 = colTarget
const aX = (a3 - a2) / (1 - a2);
const [rX, gX, bX] = [
Math.round((r3 * a3 - r2 * a2) / (1 - a2) / aX),
Math.round((g3 * a3 - g2 * a2) / (1 - a2) / aX),
Math.round((b3 * a3 - b2 * a2) / (1 - a2) / aX)
];
if(rX < 0 || rX > 255 || gX < 0 || gX > 255 || bX < 0 || bX > 255 || aX < 0 || aX > 1){
throw new Error(`There's no color to produce that mix `+
`[solution = rgba(${rX}, ${gX}, ${bX}, ${Math.round(aX*100)/100})]`);
}
return[rX, gX, bX, Math.round(aX * 100)/100];
}
<div style="display: inline-block;text-align: center; border: 1px dashed #888">
<canvas id ="canvas1" width="100" height="100"></canvas>
<div id="formulaLHS" style="font-family: monospace; font-size: smaller"></div>
</div> =
<div style="display: inline-block; text-align: center; border: 1px dashed #888">
<canvas id ="canvas2" width="100" height="100"></canvas>
<div id="formulaRHS" style="font-family: monospace; font-size: smaller"></div>
</div>
<div id="error" style="font-family: monospace; color: red"></div>