diff --git a/app/charts/good_job/performance_index_chart.rb b/app/charts/good_job/performance_index_chart.rb index df50ab9c..3d891cbc 100644 --- a/app/charts/good_job/performance_index_chart.rb +++ b/app/charts/good_job/performance_index_chart.rb @@ -56,6 +56,9 @@ def data display: true, text: I18n.t("good_job.performance.index.chart_title"), }, + legend: { + vertical: true, + }, }, scales: { y: { diff --git a/app/frontend/good_job/modules/charts.js b/app/frontend/good_job/modules/charts.js index 2804fdb5..f33e444a 100644 --- a/app/frontend/good_job/modules/charts.js +++ b/app/frontend/good_job/modules/charts.js @@ -1,3 +1,5 @@ +import htmlLegendPlugin from "html_legend_plugin"; + function renderCharts(animate) { const charts = document.querySelectorAll('.chart'); @@ -5,6 +7,16 @@ function renderCharts(animate) { const chartEl = charts[i]; const chartData = JSON.parse(chartEl.dataset.json); chartData.options ||= {}; + + if (chartData.options.plugins?.legend?.vertical) { + chartData.plugins = [htmlLegendPlugin]; + chartData.options.plugins = { + ...chartData.options.plugins, + legend: { + display: false, + } + } + } chartData.options.animation = animate; chartData.options.responsive = true; chartData.options.maintainAspectRatio = false; diff --git a/app/frontend/good_job/modules/html_legend_plugin.js b/app/frontend/good_job/modules/html_legend_plugin.js new file mode 100644 index 00000000..5d6f831d --- /dev/null +++ b/app/frontend/good_job/modules/html_legend_plugin.js @@ -0,0 +1,56 @@ +const generateListItem = (item) => { + const li = document.createElement('li'); + li.className = 'd-flex align-items-center text-nowrap mb-2'; + + const boxSpan = document.createElement('span'); + boxSpan.className = 'legend-item-color-box'; + boxSpan.style.background = item.fillStyle; + boxSpan.style.borderColor = item.strokeStyle; + boxSpan.style.borderWidth = item.lineWidth + 'px'; + + const textContainer = document.createElement('p'); + textContainer.className = 'item-text m-0 small'; + textContainer.style.color = item.fontColor; + textContainer.style.textDecoration = item.hidden ? 'line-through' : ''; + + const text = document.createTextNode(item.text); + textContainer.appendChild(text); + + li.appendChild(boxSpan); + li.appendChild(textContainer); + + return li; +} + +const htmlLegendPlugin = { + id: 'htmlLegend', + afterUpdate(chart, _args, _options) { + const {type} = chart.config; + const ul = document.getElementById('chart-legend-ul'); + + // Remove old legend items + while (ul.firstChild) { + ul.firstChild.remove(); + } + + // Reuse the built-in legendItems generator + const items = chart.options.plugins.legend.labels.generateLabels(chart); + + items.forEach(item => { + const li = generateListItem(item); + ul.appendChild(li); + + li.onclick = () => { + if (type === 'pie' || type === 'doughnut') { + // Pie and doughnut charts only have a single dataset and visibility is per item + chart.toggleDataVisibility(item.index); + } else { + chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex)); + } + chart.update(); + }; + }); + } +}; + +export { htmlLegendPlugin as default }; diff --git a/app/frontend/good_job/style.css b/app/frontend/good_job/style.css index 92f1d798..f3606269 100644 --- a/app/frontend/good_job/style.css +++ b/app/frontend/good_job/style.css @@ -24,6 +24,19 @@ height: 200px; } +.legend-item-color-box { + display: inline-block; + flex-shrink: 0; + height: 20px; + width: 20px; + border-style: solid; + margin-right: 3px; +} + +#chart-legend-container { + height: 200px; +} + /* Break out of a container */ .break-out { width:100vw; diff --git a/app/views/good_job/jobs/index.html.erb b/app/views/good_job/jobs/index.html.erb index 157e255a..120cf10d 100644 --- a/app/views/good_job/jobs/index.html.erb +++ b/app/views/good_job/jobs/index.html.erb @@ -1,5 +1,5 @@ <%= render 'good_job/shared/filter', title: t("good_job.shared.navbar.jobs"), filter: @filter %> -<%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %> +<%= render 'good_job/shared/chart_container', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>