From c03b7fb5c881104ec3aca450031edfa3b5adc0c0 Mon Sep 17 00:00:00 2001 From: George Dang <53052793+gtdang@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:13:24 -0500 Subject: [PATCH 1/5] feat: added bar chart of counts by year --- src/components/BarChart.js | 149 +++++++++++++++++++++++++++++++++ src/components/ContentPage.jsx | 4 + 2 files changed, 153 insertions(+) create mode 100644 src/components/BarChart.js diff --git a/src/components/BarChart.js b/src/components/BarChart.js new file mode 100644 index 0000000..a9a9b66 --- /dev/null +++ b/src/components/BarChart.js @@ -0,0 +1,149 @@ +import React from 'react'; +import { Vega } from 'react-vega'; + +const bar_color = '#00c398'; // ccv green +const bar_hover_color = '#ffc72c'; // ccv yellow + +function generateBarPlot({ data = {}, xLabel = '', yLabel = '' }) { + // Validate input JSON structure + if (!Array.isArray(data) || data.some((item) => !('label' in item && 'count' in item))) { + throw new Error("Input must be an array of objects with 'label' and 'count' properties"); + } + + // Transform the input JSON to match Vega's expected data format + const dataMap = data.map((item) => ({ + bin: item.label, // Use 'label' for the x-axis + count: item.count, // Use 'count' for the y-axis + })); + + // Define the Vega specification + const spec = { + $schema: 'https://vega.github.io/schema/vega/v5.json', + width: 800, + height: 400, + padding: 5, + data: [ + { + name: 'table', + values: dataMap, + }, + ], + + signals: [ + { + name: 'tooltip', + value: {}, + on: [ + { events: 'rect:pointerover', update: 'datum' }, + { events: 'rect:pointerout', update: '{}' }, + ], + }, + ], + + scales: [ + { + name: 'xscale', + type: 'band', + domain: { data: 'table', field: 'bin' }, + range: 'width', + padding: 0.1, + }, + { + name: 'yscale', + type: 'linear', + domain: { data: 'table', field: 'count' }, + range: 'height', + nice: true, + }, + ], + + axes: [ + { + orient: 'bottom', + scale: 'xscale', + title: xLabel, + labelAngle: -50, + labelAlign: 'right', + labelBaseline: 'top', + }, + { orient: 'left', scale: 'yscale', title: yLabel }, + ], + + marks: [ + { + type: 'rect', + from: { data: 'table' }, + encode: { + enter: { + x: { scale: 'xscale', field: 'bin' }, + width: { scale: 'xscale', band: 1 }, + y: { scale: 'yscale', field: 'count' }, + y2: { scale: 'yscale', value: 0 }, + }, + update: { + fill: { value: bar_color }, + }, + hover: { + fill: { value: bar_hover_color }, + }, + }, + }, + { + type: 'text', + encode: { + enter: { + align: { value: 'center' }, + baseline: { value: 'bottom' }, + fill: { value: '#333' }, + fontSize: { value: 15 }, + }, + update: { + x: { scale: 'xscale', signal: `tooltip.bin`, band: 0.5 }, + y: { scale: 'yscale', signal: 'tooltip.count', offset: -2 }, + text: { signal: 'tooltip.count' }, + fillOpacity: [{ test: 'datum === tooltip', value: 0 }, { value: 1 }], + }, + }, + }, + ], + + config: { + title: { + fontSize: 24, + }, + axis: { + labelFontSize: 16, + titleFontSize: 20, + }, + }, + }; + + return spec; +} + +const inputJson = [ + { label: '2008', count: 1 }, + { label: '2009', count: 2 }, + { label: '2010', count: 6 }, + { label: '2011', count: 3 }, + { label: '2012', count: 8 }, + { label: '2013', count: 18 }, + { label: '2014', count: 35 }, + { label: '2015', count: 41 }, + { label: '2016', count: 35 }, + { label: '2017', count: 60 }, + { label: '2018', count: 70 }, + { label: '2019', count: 60 }, + { label: '2020', count: 70 }, + { label: '2021', count: 58 }, + { label: '2022', count: 62 }, + { label: '2023', count: 80 }, + { label: '2024', count: 5 }, +]; + +export function YearBarPlot() { + // Generate the Vega spec + const vegaSpec = generateBarPlot({ data: inputJson, xLabel: 'Year', yLabel: 'Publications' }); + + return ; +} diff --git a/src/components/ContentPage.jsx b/src/components/ContentPage.jsx index 9300b0e..398792c 100755 --- a/src/components/ContentPage.jsx +++ b/src/components/ContentPage.jsx @@ -5,6 +5,7 @@ import { useSelector } from 'react-redux'; import { selectUser } from '../store/slice/appState'; import { PublicationsTable } from './PublicationsTable.tsx'; import { AddPublicationModal } from './AddPublicationModal.tsx'; +import { YearBarPlot, YearBarPlotCumu, YearLinePlotCumu } from './BarChart.js'; export function ContentPage() { const user = useSelector(selectUser); @@ -29,6 +30,9 @@ export function ContentPage() { {/*
*/} {/* */} {/**/} + + {/* */} + {/* */} {/*
*/} ); From 0e419a45a0547384f5913f01086d3b2a3fcd6f14 Mon Sep 17 00:00:00 2001 From: George Dang <53052793+gtdang@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:16:32 -0500 Subject: [PATCH 2/5] feat: added line plot with cumulative publications sum --- src/components/BarChart.js | 149 +++++++++++++++++++++++++++++++++ src/components/ContentPage.jsx | 2 +- 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/src/components/BarChart.js b/src/components/BarChart.js index a9a9b66..20f1fb9 100644 --- a/src/components/BarChart.js +++ b/src/components/BarChart.js @@ -121,6 +121,144 @@ function generateBarPlot({ data = {}, xLabel = '', yLabel = '' }) { return spec; } +function generateCumuSumPlot({ data = {}, xLabel = '', yLabel = '' }) { + // Validate input JSON structure + if (!Array.isArray(data) || data.some((item) => !('label' in item && 'count' in item))) { + throw new Error("Input must be an array of objects with 'label' and 'count' properties"); + } + + // Transform the input JSON to match Vega's expected data format + const dataMap = data.map((item) => ({ + bin: item.label, // Use 'label' for the x-axis + count: item.count, // Use 'count' for the y-axis + })); + + // Define the Vega specification + const spec = { + $schema: 'https://vega.github.io/schema/vega/v5.json', + width: 800, + height: 400, + padding: 5, + data: [ + { + name: 'table', + values: dataMap, + transform: [ + { + type: 'window', + ops: ['sum'], + fields: ['count'], + as: ['cumulativeSum'], + }, + ], + }, + ], + signals: [ + { + name: 'hoveredPoint', + value: null, + on: [ + { events: 'symbol:mouseover', update: 'datum' }, + { events: 'symbol:mouseout', update: 'null' }, + ], + }, + ], + + scales: [ + { + name: 'xscale', + type: 'band', + domain: { data: 'table', field: 'bin' }, + range: 'width', + padding: 0.1, + }, + { + name: 'yscale', + type: 'linear', + domain: { data: 'table', field: 'cumulativeSum' }, + range: 'height', + nice: true, + }, + ], + + axes: [ + { + orient: 'bottom', + scale: 'xscale', + title: xLabel, + labelAngle: -50, + labelAlign: 'right', + labelBaseline: 'top', + }, + { orient: 'left', scale: 'yscale', title: yLabel }, + ], + + marks: [ + { + type: 'line', + from: { data: 'table' }, + encode: { + enter: { + x: { scale: 'xscale', field: 'bin', band: 0.5 }, + y: { scale: 'yscale', field: 'cumulativeSum' }, + stroke: { value: 'steelblue' }, + strokeWidth: { value: 2 }, + }, + }, + }, + { + type: 'symbol', + from: { data: 'table' }, + encode: { + enter: { + x: { scale: 'xscale', field: 'bin', band: 0.5 }, + y: { scale: 'yscale', field: 'cumulativeSum' }, + size: { value: 60 }, + fill: { value: 'steelblue' }, + }, + update: { + fill: { value: 'steelblue' }, + size: { value: 60 }, + }, + hover: { + fill: { value: bar_hover_color }, + size: { value: 100 }, + }, + }, + }, + { + type: 'rule', + encode: { + update: { + x: { value: 0 }, + x2: { + signal: + "hoveredPoint ? scale('xscale', hoveredPoint.bin) + (bandwidth('xscale') / 2) : 0", + }, + y: { signal: "hoveredPoint ? scale('yscale', hoveredPoint.cumulativeSum) : 0" }, + stroke: { value: 'dimgray' }, + strokeDash: { value: [4, 4] }, + strokeWidth: { value: 1.2 }, + opacity: { signal: 'hoveredPoint ? 1 : 0' }, + }, + }, + }, + ], + + config: { + title: { + fontSize: 24, + }, + axis: { + labelFontSize: 16, + titleFontSize: 20, + }, + }, + }; + + return spec; +} + const inputJson = [ { label: '2008', count: 1 }, { label: '2009', count: 2 }, @@ -147,3 +285,14 @@ export function YearBarPlot() { return ; } + +export function YearLinePlotCumu() { + // Generate the Vega spec + const vegaSpec = generateCumuSumPlot({ + data: inputJson, + xLabel: 'Year', + yLabel: 'Cumulative Publications', + }); + + return ; +} diff --git a/src/components/ContentPage.jsx b/src/components/ContentPage.jsx index 398792c..c9bff27 100755 --- a/src/components/ContentPage.jsx +++ b/src/components/ContentPage.jsx @@ -31,7 +31,7 @@ export function ContentPage() { {/* */} {/**/} - {/* */} + {/* */} {/**/} From 8921b684d6ea07ca44023a0a5c9140fd3e645929 Mon Sep 17 00:00:00 2001 From: George Dang <53052793+gtdang@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:18:43 -0500 Subject: [PATCH 3/5] feat: added barplot and cumulative lineplot duel axis figure --- src/components/BarChart.js | 179 +++++++++++++++++++++++++++++++++ src/components/ContentPage.jsx | 2 +- 2 files changed, 180 insertions(+), 1 deletion(-) diff --git a/src/components/BarChart.js b/src/components/BarChart.js index 20f1fb9..f6d6471 100644 --- a/src/components/BarChart.js +++ b/src/components/BarChart.js @@ -259,6 +259,178 @@ function generateCumuSumPlot({ data = {}, xLabel = '', yLabel = '' }) { return spec; } +const generateBarPlotWithCumuSum = (dataJson, xLabel) => { + // Validate input JSON structure + if (!Array.isArray(dataJson) || dataJson.some((item) => !('label' in item && 'count' in item))) { + throw new Error("Input must be an array of objects with 'label' and 'count' properties"); + } + + // Transform the input JSON to match Vega's expected data format + const data = dataJson.map((item) => ({ + bin: item.label, // Use 'label' for the x-axis + count: item.count, // Use 'count' for the y-axis + })); + + // Define the Vega specification + const spec = { + $schema: 'https://vega.github.io/schema/vega/v5.json', + width: 800, + height: 400, + padding: 5, + data: [ + { + name: 'table', + values: data, + transform: [ + { + type: 'window', + ops: ['sum'], + fields: ['count'], + as: ['cumulativeSum'], + }, + ], + }, + ], + + signals: [ + { + name: 'tooltip', + value: {}, + on: [ + { events: 'rect:pointerover', update: 'datum' }, + { events: 'rect:pointerout', update: '{}' }, + ], + }, + { + name: 'hoveredPoint', + value: null, + on: [ + { events: 'symbol:mouseover', update: 'datum' }, + { events: 'symbol:mouseout', update: 'null' }, + ], + }, + ], + + scales: [ + { + name: 'xscale', + type: 'band', + domain: { data: 'table', field: 'bin' }, + range: 'width', + padding: 0.1, + }, + { + name: 'yscale', + type: 'linear', + domain: { data: 'table', field: 'count' }, + range: 'height', + nice: true, + }, + { + name: 'y2scale', + type: 'linear', + domain: { data: 'table', field: 'cumulativeSum' }, + range: 'height', + nice: true, + }, + ], + + axes: [ + { orient: 'bottom', scale: 'xscale', title: xLabel }, + { orient: 'left', scale: 'yscale', title: 'Publications' }, + { orient: 'right', scale: 'y2scale', title: 'Cumulative', grid: false }, + ], + + marks: [ + { + type: 'rect', + from: { data: 'table' }, + encode: { + enter: { + x: { scale: 'xscale', field: 'bin' }, + width: { scale: 'xscale', band: 1 }, + y: { scale: 'yscale', field: 'count' }, + y2: { scale: 'yscale', value: 0 }, + }, + update: { + fill: { value: bar_color }, + }, + hover: { + fill: { value: bar_hover_color }, + }, + }, + }, + { + type: 'line', + from: { data: 'table' }, + encode: { + enter: { + x: { scale: 'xscale', field: 'bin', band: 0.5 }, + y: { scale: 'y2scale', field: 'cumulativeSum' }, + stroke: { value: 'steelblue' }, + strokeWidth: { value: 2 }, + }, + }, + }, + { + type: 'symbol', + from: { data: 'table' }, + encode: { + enter: { + x: { scale: 'xscale', field: 'bin', band: 0.5 }, + y: { scale: 'y2scale', field: 'cumulativeSum' }, + size: { value: 50 }, + fill: { value: 'steelblue' }, + }, + }, + }, + { + type: 'text', + encode: { + enter: { + align: { value: 'center' }, + baseline: { value: 'bottom' }, + fill: { value: '#333' }, + fontSize: { value: 15 }, + }, + update: { + x: { scale: 'xscale', signal: 'tooltip.bin', band: 0.5 }, + y: { scale: 'yscale', signal: 'tooltip.count', offset: -2 }, + text: { signal: 'tooltip.count' }, + fillOpacity: [{ test: 'datum === tooltip', value: 0 }, { value: 1 }], + }, + }, + }, + { + type: 'rule', + encode: { + update: { + x: { scale: 'xscale', signal: 'tooltip.bin', band: 0.5 }, + x2: { signal: 'width' }, + y: { signal: "tooltip ? scale('y2scale', tooltip.cumulativeSum) : 0" }, + stroke: { value: 'dimgray' }, + strokeDash: { value: [4, 4] }, + strokeWidth: { value: 1.2 }, + opacity: { signal: 'tooltip && tooltip.bin ? 1 : 0' }, + }, + }, + }, + ], + + config: { + title: { + fontSize: 24, + }, + axis: { + labelFontSize: 16, + titleFontSize: 20, + }, + }, + }; + + return spec; +}; + const inputJson = [ { label: '2008', count: 1 }, { label: '2009', count: 2 }, @@ -296,3 +468,10 @@ export function YearLinePlotCumu() { return ; } + +export function YearBarPlotCumu() { + // Generate the Vega spec + const vegaSpec = generateBarPlotWithCumuSum(inputJson, 'Year'); + + return ; +} diff --git a/src/components/ContentPage.jsx b/src/components/ContentPage.jsx index c9bff27..d267cec 100755 --- a/src/components/ContentPage.jsx +++ b/src/components/ContentPage.jsx @@ -32,7 +32,7 @@ export function ContentPage() { {/**/} - {/* */} + {/**/} ); From 27c7cbd3831ebc3cae1e1b6657ff35d3f85fe7b0 Mon Sep 17 00:00:00 2001 From: George Dang <53052793+gtdang@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:02:47 -0500 Subject: [PATCH 4/5] refactor: simplified plot functions to a single function CountsByYearPlot with specification using a type property --- src/components/BarChart.js | 39 ++++++++++++++++------------------ src/components/ContentPage.jsx | 8 +++---- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/components/BarChart.js b/src/components/BarChart.js index f6d6471..b1c5d18 100644 --- a/src/components/BarChart.js +++ b/src/components/BarChart.js @@ -451,27 +451,24 @@ const inputJson = [ { label: '2024', count: 5 }, ]; -export function YearBarPlot() { - // Generate the Vega spec - const vegaSpec = generateBarPlot({ data: inputJson, xLabel: 'Year', yLabel: 'Publications' }); - - return ; -} - -export function YearLinePlotCumu() { - // Generate the Vega spec - const vegaSpec = generateCumuSumPlot({ - data: inputJson, - xLabel: 'Year', - yLabel: 'Cumulative Publications', - }); - - return ; -} - -export function YearBarPlotCumu() { - // Generate the Vega spec - const vegaSpec = generateBarPlotWithCumuSum(inputJson, 'Year'); +export function CountsByYearPlot({ type }) { + let vegaSpec = {}; + const xLabel = 'Year'; + if (type === 'bar') { + vegaSpec = generateBarPlot({ + data: inputJson, + xLabel: xLabel, + yLabel: 'Publications', + }); + } else if (type === 'cumu-line') { + vegaSpec = generateCumuSumPlot({ + data: inputJson, + xLabel: xLabel, + yLabel: 'Cumulative Publications', + }); + } else if (type === 'bar-cumu-line') { + vegaSpec = generateBarPlotWithCumuSum(inputJson, 'Year'); + } return ; } diff --git a/src/components/ContentPage.jsx b/src/components/ContentPage.jsx index d267cec..7450e75 100755 --- a/src/components/ContentPage.jsx +++ b/src/components/ContentPage.jsx @@ -5,7 +5,7 @@ import { useSelector } from 'react-redux'; import { selectUser } from '../store/slice/appState'; import { PublicationsTable } from './PublicationsTable.tsx'; import { AddPublicationModal } from './AddPublicationModal.tsx'; -import { YearBarPlot, YearBarPlotCumu, YearLinePlotCumu } from './BarChart.js'; +import { CountsByYearPlot } from './BarChart.js'; export function ContentPage() { const user = useSelector(selectUser); @@ -30,9 +30,9 @@ export function ContentPage() { {/*
*/} {/* */} {/**/} - - - + + + {/*
*/} ); From 1cb857d3dae793926e91e6f948fdfa707d4fbe78 Mon Sep 17 00:00:00 2001 From: George Dang <53052793+gtdang@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:39:37 -0500 Subject: [PATCH 5/5] refactor: replace hardcode input with shared const --- src/components/BarChart.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/BarChart.js b/src/components/BarChart.js index b1c5d18..ce5cd34 100644 --- a/src/components/BarChart.js +++ b/src/components/BarChart.js @@ -467,7 +467,7 @@ export function CountsByYearPlot({ type }) { yLabel: 'Cumulative Publications', }); } else if (type === 'bar-cumu-line') { - vegaSpec = generateBarPlotWithCumuSum(inputJson, 'Year'); + vegaSpec = generateBarPlotWithCumuSum(inputJson, xLabel); } return ;