ChartJS:用不同的颜色填充高于最大值、介于最大值/最小值之间以及低于最小值限制的曲线数据集

问题描述 投票:0回答:1

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,但这就是结果。我需要将棕色区域变成红色:)

  1. 我尝试了 CoPilot,他提到 ChartJS 中的填充对象有一个“Between”属性,但我找不到有关该属性的任何文档,并且它的示例代码从未工作过。
jquery chart.js ruby-on-rails-5 chartkick
1个回答
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>

© www.soinside.com 2019 - 2024. All rights reserved.