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 ;