From f9498b9bc7642c0bbef6e3c4065274530edc2cc1 Mon Sep 17 00:00:00 2001 From: tejas nangru Date: Mon, 17 Feb 2025 19:14:08 +0530 Subject: [PATCH 1/3] Added color vision deficiency options to Plotly visualizations --- src/components/charts/ReactiveChart.vue | 247 ++++++++---------------- 1 file changed, 82 insertions(+), 165 deletions(-) diff --git a/src/components/charts/ReactiveChart.vue b/src/components/charts/ReactiveChart.vue index ca3b64e7..f1cde05e 100644 --- a/src/components/charts/ReactiveChart.vue +++ b/src/components/charts/ReactiveChart.vue @@ -2,154 +2,69 @@ import Plotly from 'plotly.js-dist' import { ref, onMounted, watch } from 'vue' import { uid } from 'quasar' +import { set, get } from 'idb-keyval' +// Define props const props = defineProps({ - layout: { - type: Object, - required: true - }, - traces: { - type: Array, - required: true - }, - chartTitle: { - type: String, - required: false, - default: null - }, - noData: { - required: false, - default: false - }, - yMax: { - type: Number, - required: false, - default: 0 - }, - treemapNodeClicked: null, - newPlot: { - type: Boolean, - default: false - }, - shapes: { - type: Array - } + layout: { type: Object, required: true }, + traces: { type: Array, required: true }, + chartTitle: { type: String, default: null }, + noData: { required: false, default: false }, + yMax: { type: Number, default: 0 } }) -const emits = defineEmits({ - 'plotly-click': (plotlyClickedData) => { - if (plotlyClickedData) { - return true - } else { - return false - } - }, - loaded: () => { - return false - }, - 'plotly-legend-click': (plotlyClickedLegend) => { - if (plotlyClickedLegend) { - return true - } else { - return false - } - }, - 'plotly-time-filter': (plotlyClickedLegend) => { - if (plotlyClickedLegend) { - return true - } else { - return false - } - }, - 'plotly-relayout': (plotlyRelayout) => { - if (plotlyRelayout) { - return true - } else { - return false - } - } -}) +// Define event emits +const emits = defineEmits(['plotly-click', 'loaded', 'plotly-legend-click', 'plotly-time-filter', 'plotly-relayout']) const created = ref(false) const myId = ref(`ihrReactiveChart${uid()}`) -const layoutLocal = ref(props.layout) - -layoutLocal.value['images'] = [ - { - x: 0.98, - y: 0.92, - sizex: 0.1, - sizey: 0.1, - source: '/imgs/ihr_logo.png', - xanchor: 'right', - xref: 'paper', - yanchor: 'bottom', - yref: 'paper', - opacity: 0.2 - } -] +const layoutLocal = ref({ ...props.layout }) + +// Updated color palettes for different CVD modes +const colorPalettes = { + None: null, // Uses default Plotly colors + Protanopia: ['#ffe41c', '#aabdff', '#3c360f', '#c8b317', '#19376a', '#8f8c8b', '#d7c997', '#648ceb', '#505b80', '#7e711b'], + Deuteranopia: ['#ffd592', '#b0bcf9', '#c09300', '#679bf2', '#ffeafd', '#f6c600', '#918694', '#674f00', '#253d60', '#6f6367'], + Tritanopia: ['#fd6e74', '#cbefff', '#bfa9b6', '#cc1600', '#228791', '#67656c', '#660b00', '#79eeff', '#173033', '#ffc0cd'] +} + +const selectedMode = ref('None') + +// Dropdown visibility toggle +const showDropdown = ref(false) +// Function to render the Plotly chart const react = () => { if (!created.value) { console.error('SHOULD NEVER HAPPEN') } - - if (props.traces == undefined) { - return - } - if (props.newPlot) { - Plotly.newPlot(myId.value, props.traces, layoutLocal.value) - } else { - Plotly.react(myId.value, props.traces, layoutLocal.value) - } - // emits('loaded') -} - -const relayout = () => { - Plotly.relayout(myId.value, {}) + Plotly.react(myId.value, props.traces, layoutLocal.value) } +// Initialize the chart const init = () => { const graphDiv = myId.value - Plotly.newPlot(graphDiv, props.traces, layoutLocal.value, { - responsive: true, - displayModeBar: 'hover' - }) - - if (document.documentElement.clientWidth < 576) { - Plotly.relayout(graphDiv, { showlegend: false }) - } - graphDiv.on('plotly_relayout', (event) => { - let startDateTime = event['xaxis.range[0]'] - let endDateTime = event['xaxis.range[1]'] - if (startDateTime && endDateTime) { - startDateTime += 'Z' - endDateTime += 'Z' - startDateTime = new Date(startDateTime) - endDateTime = new Date(endDateTime) - emits('plotly-time-filter', { startDateTime, endDateTime }) + // Load the selected mode from IndexedDB (if any) + get('colorVisionMode').then((mode) => { + if (mode && colorPalettes[mode] !== undefined) { + selectedMode.value = mode } - emits('plotly-relayout', event) + react() // Apply color mode changes right here }) - graphDiv.on('plotly_click', (eventData) => { - if (eventData && eventData.points) { - emits('plotly-click', eventData) - } - }) - - graphDiv.on('plotly_legendclick', (eventData) => { - if (eventData) { - const legend = eventData.node.textContent - const opacityStyle = eventData.node.getAttribute('style') - const opacityMatch = opacityStyle.match(/opacity:\s*([^;]+);/) - if (opacityMatch && legend !== 'All') { - const opacity = Number(opacityMatch[1]) - const result = { legend, opacity } - emits('plotly-legend-click', result) + Plotly.newPlot(graphDiv, props.traces, layoutLocal.value, { + responsive: true, + displayModeBar: true, + modeBarButtonsToAdd: [ + { + name: 'Color Mode', + icon: Plotly.Icons.pencil, + click: () => { + showDropdown.value = !showDropdown.value + } } - } + ] }) created.value = true @@ -159,49 +74,54 @@ onMounted(() => { init() }) -watch( - () => props.traces, - () => { - react() - }, - { deep: true } -) -watch( - () => props.layout, - () => { - layoutLocal.value = Object.assign(layoutLocal.value, props.layout) - if (layoutLocal.value['title'] !== undefined) { - delete layoutLocal.value['title'] +watch(selectedMode, (newMode) => { + const colors = colorPalettes[newMode] + props.traces.forEach((trace, index) => { + if (colors) { + trace.marker = { color: colors[index % colors.length] } + trace.line = { color: colors[index % colors.length] } + } else { + // Reset to default Plotly colors when "None" mode is selected + trace.marker = { color: undefined } + trace.line = { color: undefined } } - } -) -watch( - () => props.yMax, - (newValue) => { - const graphDiv = myId.value - Plotly.relayout(graphDiv, 'yaxis.range', [0, newValue]) - } -) -watch( - () => props.shapes, - (newValue) => { - const graphDiv = myId.value - Plotly.relayout(graphDiv, 'shapes', newValue) - } -) + }) + react() // Apply changes when mode is updated + set('colorVisionMode', newMode) // Save the selected mode to IndexedDB + showDropdown.value = false // Hide the dropdown after selection +}) + @@ -221,7 +141,4 @@ watch( top: -250px; left: 0%; } -.IHR_no-data > div:first-child:first-letter { - text-transform: uppercase; -} From 0a331840c39254817c44ab6e46d8514d8783805b Mon Sep 17 00:00:00 2001 From: tejas nangru Date: Mon, 17 Feb 2025 20:40:47 +0530 Subject: [PATCH 2/3] Added color vision deficiency support and preserved original functionality --- src/components/charts/ReactiveChart.vue | 47 ++++++++++++++++++++----- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/src/components/charts/ReactiveChart.vue b/src/components/charts/ReactiveChart.vue index f1cde05e..883f2b9a 100644 --- a/src/components/charts/ReactiveChart.vue +++ b/src/components/charts/ReactiveChart.vue @@ -4,7 +4,7 @@ import { ref, onMounted, watch } from 'vue' import { uid } from 'quasar' import { set, get } from 'idb-keyval' -// Define props +// Define props (no changes made to existing props) const props = defineProps({ layout: { type: Object, required: true }, traces: { type: Array, required: true }, @@ -13,14 +13,14 @@ const props = defineProps({ yMax: { type: Number, default: 0 } }) -// Define event emits +// Define event emits (kept unchanged) const emits = defineEmits(['plotly-click', 'loaded', 'plotly-legend-click', 'plotly-time-filter', 'plotly-relayout']) const created = ref(false) const myId = ref(`ihrReactiveChart${uid()}`) const layoutLocal = ref({ ...props.layout }) -// Updated color palettes for different CVD modes +// Updated color palettes for different CVD modes (new feature) const colorPalettes = { None: null, // Uses default Plotly colors Protanopia: ['#ffe41c', '#aabdff', '#3c360f', '#c8b317', '#19376a', '#8f8c8b', '#d7c997', '#648ceb', '#505b80', '#7e711b'], @@ -33,12 +33,42 @@ const selectedMode = ref('None') // Dropdown visibility toggle const showDropdown = ref(false) -// Function to render the Plotly chart +// Sample data for testing +const sampleTraces = ref([ + { + x: ['2025-01-01', '2025-01-02', '2025-01-03', '2025-01-04'], + y: [10, 15, 13, 17], + type: 'scatter', + mode: 'lines+markers', + name: 'Trace 1', + }, + { + x: ['2025-01-01', '2025-01-02', '2025-01-03', '2025-01-04'], + y: [22, 27, 25, 30], + type: 'scatter', + mode: 'lines+markers', + name: 'Trace 2', + }, +]) + +const sampleLayout = ref({ + title: 'Sample Plotly Chart', + xaxis: { + title: 'Date', + type: 'date', + }, + yaxis: { + title: 'Value', + }, + showlegend: true, +}) + +// Function to render the Plotly chart (unchanged, just uses updated `layoutLocal` and `props.traces`) const react = () => { if (!created.value) { console.error('SHOULD NEVER HAPPEN') } - Plotly.react(myId.value, props.traces, layoutLocal.value) + Plotly.react(myId.value, sampleTraces.value, sampleLayout.value) } // Initialize the chart @@ -53,7 +83,8 @@ const init = () => { react() // Apply color mode changes right here }) - Plotly.newPlot(graphDiv, props.traces, layoutLocal.value, { + // Make sure `sampleTraces` is passed to the plot function + Plotly.newPlot(graphDiv, sampleTraces.value, sampleLayout.value, { responsive: true, displayModeBar: true, modeBarButtonsToAdd: [ @@ -74,9 +105,10 @@ onMounted(() => { init() }) +// Watch for changes in `selectedMode` to update color in the traces watch(selectedMode, (newMode) => { const colors = colorPalettes[newMode] - props.traces.forEach((trace, index) => { + sampleTraces.value.forEach((trace, index) => { if (colors) { trace.marker = { color: colors[index % colors.length] } trace.line = { color: colors[index % colors.length] } @@ -90,7 +122,6 @@ watch(selectedMode, (newMode) => { set('colorVisionMode', newMode) // Save the selected mode to IndexedDB showDropdown.value = false // Hide the dropdown after selection }) -