如何使用chart.js和Bootstrap选项卡模拟Apple Health的Cardio Fitness文章中的堆叠条形图?

kokeuurv  于 2023-06-29  发布在  Chart.js
关注(0)|答案(1)|浏览(117)

在Apple Health中,有一篇名为“了解有氧健身”的文章(可以在浏览选项卡(底部)>活动中找到,然后向下滚动)。我偶然发现了这个图表,我真的想用HTML来模拟它。
The chart I'm talking about...
我的问题是

  • stacked bar chart in official docs中,我发现了一个“随机化”按钮,可以在数据之间无缝切换。我如何实现Bootstrap标签,允许像上面的Apple Health文章那样在数据之间切换?
  • 如何在栏中添加数字标签?关掉悬停功能?
  • 额外提示:如何将“VO 2 max”标签旋转到水平位置?我尝试了这里的代码(https://stackoverflow.com/a/75432153/22078763),但不幸的是它不起作用(可能是因为我使用的是chart.js 4.3.0)。

以下是我目前为止的代码和数据(我忘记添加Male/Female/All选项卡 facepalm):

function createChart(id, data) {
  const ctx = document.getElementById(id).getContext('2d');
  new Chart(ctx, {
    type: 'bar',
    data: {
      labels: ['20-29', '30-39', '40-49', '50-59', '60+'],
      datasets: data
    },
    options: {
      scales: {
        x: {
          stacked: true,
          title: {
            display: true,
            text: 'Age Ranges',
            align: 'start'
          }
        },
        y: {
          stacked: true,
          title: {
            display: true,
            text: 'VO2 max',
            align: 'end'
          },
          position: 'right'
        }
      },
      plugins: {
        legend: {
          display: false
        }
      }
    }
  });
}

const lowData = [{
  label: 'Low',
  data: {
    "20-29": [29, 38],
    "30-39": [27, 34],
    "40-49": [24, 31],
    "50-59": [21, 26],
    "60+": [17, 18]
  },
}];

const belowAverageData = [{
  label: 'Below Average',
  data: {
    "20-29": [38, 48],
    "30-39": [34, 43],
    "40-49": [31, 38],
    "50-59": [26, 33],
    "60+": [18, 28]
  },
}];

const aboveAverageData = [{
  label: 'Above Average',
  data: {
    "20-29": [48, 57],
    "30-39": [43, 52],
    "40-49": [38, 47],
    "50-59": [33, 41],
    "60+": [28, 36]
  },
}];

const highData = [{
  label: 'High',
  data: {
    "20-29": [57, 66],
    "30-39": [52, 60],
    "40-49": [47, 56],
    "50-59": [41, 51],
    "60+": [36, 42]
  },
}];

createChart('lowChart', lowData);
createChart('belowAverageChart', belowAverageData);
createChart('aboveAverageChart', aboveAverageData);
createChart('highChart', highData);

const triggerTabList = document.querySelectorAll('#chartTabs button')
triggerTabList.forEach(triggerEl => {
  const tabTrigger = new bootstrap.Tab(triggerEl)

  triggerEl.addEventListener('click', event => {
    event.preventDefault()
    tabTrigger.show()
  })
})
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<ul class="nav nav-pills" id="chartTabs" role="tablist">
  <li class="nav-item" role="presentation">
    <button class="nav-link active" id="low-tab" data-bs-toggle="tab" data-bs-target="#low" type="button" role="tab" aria-controls="low" aria-selected="true">Low</button>
  </li>
  <li class="nav-item" role="presentation">
    <button class="nav-link" id="below-average-tab" data-bs-toggle="tab" data-bs-target="#below-average" type="button" role="tab" aria-controls="below-average" aria-selected="false">Below Average</button>
  </li>
  <li class="nav-item" role="presentation">
    <button class="nav-link" id="above-average-tab" data-bs-toggle="tab" data-bs-target="#above-average" type="button" role="tab" aria-controls="above-average" aria-selected="false">Above Average</button>
  </li>
  <li class="nav-item" role="presentation">
    <button class="nav-link" id="high-tab" data-bs-toggle="tab" data-bs-target="#high" type="button" role="tab" aria-controls="high" aria-selected="false">High</button>
  </li>
</ul>

<div class="tab-content" id="chartTabsContent">
  <div class="tab-pane show active" id="low" role="tabpanel" aria-labelledby="low-tab">
    <div>
      <canvas id="lowChart"></canvas>
    </div>
  </div>
  <div class="tab-pane" id="below-average" role="tabpanel" aria-labelledby="below-average-tab">
    <div>
      <canvas id="belowAverageChart"></canvas>
    </div>
  </div>
  <div class="tab-pane" id="above-average" role="tabpanel" aria-labelledby="above-average-tab">
    <div>
      <canvas id="aboveAverageChart"></canvas>
    </div>
  </div>
  <div class="tab-pane" id="high" role="tabpanel" aria-labelledby="high-tab">
    <div>
      <canvas id="highChart"></canvas>
    </div>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
mitkmikd

mitkmikd1#

首先,您必须将轴的限制设置为相同的,对于您的数据,我将ymax比例设置为70。您可以根据数据计算此值。
此外,禁用动画options.animation = false
悬停时显示的框是工具提示。您可以通过设置options.plugins.tooltip.enable = false来禁用它。
我想您希望静态地显示值。这是通过chart-plugin-datalabels实现的,参见this example
有了这些,你会得到这样的东西:

function createChart(id, data) {
    new Chart(document.getElementById(id), {
        type: 'bar',
        data: {
            labels: ['20-29', '30-39', '40-49', '50-59', '60+'],
            datasets: data
        },
        plugins: [
            ChartDataLabels,
            {
                afterDraw: chart => {
                    var ctx = chart.ctx;
                    ctx.save();
                    ctx.font = "bold 14px Arial";
                    ctx.fillStyle = "gray";
                    console.log(chart.ctx.height)
                    var y = 50;

                    ctx.textAlign = 'left';
                    ctx.fillText('CO2', 5, y);
                    ctx.fillText('°C', 46, y);

                    ctx.textAlign = 'right';
                    ctx.fillText('%', chart.width - 10, y);
                    ctx.restore();
                }
            }
        ],
        options: {
            animation: false,
            scales: {
                x: {
                    stacked: true,
                    title: {
                        display: true,
                        text: 'Age Ranges',
                        align: 'start'
                    }
                },
                y: {
                    stacked: true,
                    title: {
                        display: true,
                        text: 'VO2 max',
                        align: 'end'
                    },
                    position: 'right',
                    max: 70
                }
            },
            plugins: {
                legend: {
                    display: false
                },
                tooltip:{
                    enabled: false
                },
                datalabels: {
                    color: 'black',
                    display: function(context) {
                        // don't show for small values
                        const interval = context.dataset.data[context.chart.config.data.labels[context.dataIndex]];
                        return interval[1] - interval[0] > 1;
                    },
                    font: {
                        size: '14pt',
                        weight: 'bold'
                    },
                    formatter(_, context){
                        const interval = context.dataset.data[
                            context.chart.config.data.labels[context.dataIndex]];
                        return interval[1] - interval[0]; // or interval[0] + ' - ' + interval[1]
                    }
                }
            }
        }
    });
}

const lowData = [{
    label: 'Low',
    data: {
        "20-29": [29, 38],
        "30-39": [27, 34],
        "40-49": [24, 31],
        "50-59": [21, 26],
        "60+": [17, 18]
    },
}];

const belowAverageData = [{
    label: 'Below Average',
    data: {
        "20-29": [38, 48],
        "30-39": [34, 43],
        "40-49": [31, 38],
        "50-59": [26, 33],
        "60+": [18, 28]
    },
}];

const aboveAverageData = [{
    label: 'Above Average',
    data: {
        "20-29": [48, 57],
        "30-39": [43, 52],
        "40-49": [38, 47],
        "50-59": [33, 41],
        "60+": [28, 36]
    },
}];

const highData = [{
    label: 'High',
    data: {
        "20-29": [57, 66],
        "30-39": [52, 60],
        "40-49": [47, 56],
        "50-59": [41, 51],
        "60+": [36, 42]
    },
}];

createChart('lowChart', lowData);
createChart('belowAverageChart', belowAverageData);
createChart('aboveAverageChart', aboveAverageData);
createChart('highChart', highData);

const triggerTabList = document.querySelectorAll('#chartTabs button')
triggerTabList.forEach(triggerEl => {
    const tabTrigger = new bootstrap.Tab(triggerEl)

    triggerEl.addEventListener('click', event => {
        event.preventDefault()
        tabTrigger.show()
    })
})
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<ul class="nav nav-pills" id="chartTabs" role="tablist">
    <li class="nav-item" role="presentation">
        <button class="nav-link active" id="low-tab" data-bs-toggle="tab" data-bs-target="#low" type="button" role="tab" aria-controls="low" aria-selected="true">Low</button>
    </li>
    <li class="nav-item" role="presentation">
        <button class="nav-link" id="below-average-tab" data-bs-toggle="tab" data-bs-target="#below-average" type="button" role="tab" aria-controls="below-average" aria-selected="false">Below Average</button>
    </li>
    <li class="nav-item" role="presentation">
        <button class="nav-link" id="above-average-tab" data-bs-toggle="tab" data-bs-target="#above-average" type="button" role="tab" aria-controls="above-average" aria-selected="false">Above Average</button>
    </li>
    <li class="nav-item" role="presentation">
        <button class="nav-link" id="high-tab" data-bs-toggle="tab" data-bs-target="#high" type="button" role="tab" aria-controls="high" aria-selected="false">High</button>
    </li>
</ul>

<div class="tab-content" id="chartTabsContent">
    <div class="tab-pane show active" id="low" role="tabpanel" aria-labelledby="low-tab">
        <div>
            <canvas id="lowChart"></canvas>
        </div>
    </div>
    <div class="tab-pane" id="below-average" role="tabpanel" aria-labelledby="below-average-tab">
        <div>
            <canvas id="belowAverageChart"></canvas>
        </div>
    </div>
    <div class="tab-pane" id="above-average" role="tabpanel" aria-labelledby="above-average-tab">
        <div>
            <canvas id="aboveAverageChart"></canvas>
        </div>
    </div>
    <div class="tab-pane" id="high" role="tabpanel" aria-labelledby="high-tab">
        <div>
            <canvas id="highChart"></canvas>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.2.0/chartjs-plugin-datalabels.min.js" integrity="sha512-JPcRR8yFa8mmCsfrw4TNte1ZvF1e3+1SdGMslZvmrzDYxS69J7J49vkFL8u6u8PlPJK+H3voElBtUCzaXj+6ig==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

我发现当你第一次激活一个标签时,这仍然会显示一个 Flink ,而不是随后的激活;这是因为chart.js知道画布是隐藏的,只有当标签首次可见时才会呈现图表。
您可以使用OffscreenCanvas或三个隐藏图表来消除 Flink 。你会得到类似这样的东西:

function createChart(id, data, id2) {
    // id2 - use a memory canvas the same size as id2
    const canvas = document.getElementById(id);
    let targetCanvas = canvas;
    if(id2){
        const canvas2 = document.getElementById(id2);
        const memCanvas = new OffscreenCanvas(canvas2.offsetWidth, canvas2.offsetHeight);
        canvas.height = canvas2.offsetHeight;
        canvas.width = canvas2.offsetWidth;
        targetCanvas = memCanvas;
    }
    new Chart(targetCanvas, {
        type: 'bar',
        data: {
            labels: ['20-29', '30-39', '40-49', '50-59', '60+'],
            datasets: data
        },
        plugins: [
            ChartDataLabels,
            {
                afterDraw: chart => {
                    var ctx = chart.ctx;
                    ctx.save();
                    ctx.font = "bold 14px Arial";
                    ctx.fillStyle = "gray";
                    var y = 50;
                    
                    ctx.textAlign = 'left';
                    ctx.fillText('CO2', 5, y);
                    ctx.fillText('°C', 46, y);
                    
                    ctx.textAlign = 'right';
                    ctx.fillText('%', chart.width - 10, y);
                    ctx.restore();
                }
            }
        ],
        options: {
            animation: false,
            scales: {
                x: {
                    stacked: true,
                    title: {
                        display: true,
                        text: 'Age Ranges',
                        align: 'start'
                    }
                },
                y: {
                    stacked: true,
                    title: {
                        display: true,
                        text: 'VO2 max',
                        align: 'end'
                    },
                    position: 'right',
                    max: 70
                }
            },
            plugins: {
                legend: {
                    display: false
                },
                tooltip: {
                    enabled: false
                },
                datalabels: {
                    color: 'black',
                    display: function(context){
                        // don't show for small values
                        const interval = context.dataset.data[context.chart.config.data.labels[context.dataIndex]];
                        return interval[1] - interval[0] > 1;
                    },
                    font: {
                        size: '14pt',
                        weight: 'bold'
                    },
                    formatter(_, context){
                        const interval = context.dataset.data[
                            context.chart.config.data.labels[context.dataIndex]];
                        return interval[1] - interval[0]; // or interval[0] + ' - ' + interval[1]
                    }
                }
            }
        }
    });
    if(id2){
        canvas.getContext('2d').drawImage(targetCanvas, 0, 0);
    }
}

const lowData = [{
    label: 'Low',
    data: {
        "20-29": [29, 38],
        "30-39": [27, 34],
        "40-49": [24, 31],
        "50-59": [21, 26],
        "60+": [17, 18]
    },
}];

const belowAverageData = [{
    label: 'Below Average',
    data: {
        "20-29": [38, 48],
        "30-39": [34, 43],
        "40-49": [31, 38],
        "50-59": [26, 33],
        "60+": [18, 28]
    },
}];

const aboveAverageData = [{
    label: 'Above Average',
    data: {
        "20-29": [48, 57],
        "30-39": [43, 52],
        "40-49": [38, 47],
        "50-59": [33, 41],
        "60+": [28, 36]
    },
}];

const highData = [{
    label: 'High',
    data: {
        "20-29": [57, 66],
        "30-39": [52, 60],
        "40-49": [47, 56],
        "50-59": [41, 51],
        "60+": [36, 42]
    },
}];

createChart('lowChart', lowData);
createChart('belowAverageChart', belowAverageData, 'lowChart');
createChart('aboveAverageChart', aboveAverageData, 'lowChart');
createChart('highChart', highData, 'lowChart');

const triggerTabList = document.querySelectorAll('#chartTabs button')
triggerTabList.forEach(triggerEl => {
    const tabTrigger = new bootstrap.Tab(triggerEl)

    triggerEl.addEventListener('click', event => {
        event.preventDefault()
        tabTrigger.show()
    })
})
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<ul class="nav nav-pills" id="chartTabs" role="tablist">
    <li class="nav-item" role="presentation">
        <button class="nav-link active" id="low-tab" data-bs-toggle="tab" data-bs-target="#low" type="button" role="tab" aria-controls="low" aria-selected="true">Low</button>
    </li>
    <li class="nav-item" role="presentation">
        <button class="nav-link" id="below-average-tab" data-bs-toggle="tab" data-bs-target="#below-average" type="button" role="tab" aria-controls="below-average" aria-selected="false">Below Average</button>
    </li>
    <li class="nav-item" role="presentation">
        <button class="nav-link" id="above-average-tab" data-bs-toggle="tab" data-bs-target="#above-average" type="button" role="tab" aria-controls="above-average" aria-selected="false">Above Average</button>
    </li>
    <li class="nav-item" role="presentation">
        <button class="nav-link" id="high-tab" data-bs-toggle="tab" data-bs-target="#high" type="button" role="tab" aria-controls="high" aria-selected="false">High</button>
    </li>
</ul>

<div class="tab-content" id="chartTabsContent">
    <div class="tab-pane show active" id="low" role="tabpanel" aria-labelledby="low-tab">
        <div>
            <canvas id="lowChart"></canvas>
        </div>
    </div>
    <div class="tab-pane" id="below-average" role="tabpanel" aria-labelledby="below-average-tab">
        <div>
            <canvas id="belowAverageChart"></canvas>
        </div>
    </div>
    <div class="tab-pane" id="above-average" role="tabpanel" aria-labelledby="above-average-tab">
        <div>
            <canvas id="aboveAverageChart"></canvas>
        </div>
    </div>
    <div class="tab-pane" id="high" role="tabpanel" aria-labelledby="high-tab">
        <div>
            <canvas id="highChart"></canvas>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.2.0/chartjs-plugin-datalabels.min.js" integrity="sha512-JPcRR8yFa8mmCsfrw4TNte1ZvF1e3+1SdGMslZvmrzDYxS69J7J49vkFL8u6u8PlPJK+H3voElBtUCzaXj+6ig==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

尽管如此,我认为这种方法并不完全安全;我不能100%确定标签页的内容会完全重叠,就像移动的应用程序的屏幕视图一样。调整窗口大小时已经出现问题。这可以用更多的代码来解决。但我建议你把它看成一个有按钮的图表。除了更安全之外,这种方法还提供了更多的可能性,例如平滑的过渡动画。

相关问题