Repository: jku-vds-lab/loops (language: python)

Issue:
Show information on changed amount for data diff
Number of changed cells
Added/removed rows
added/removed columns


Discussion / hints:

Fix patch:
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index 9288ca8..e15c5f2 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -11,13 +11,13 @@ services:
       [
         'jupyter',
         'lab',
-        '--port=8888',
+        '--port=13013',
         '--no-browser',
         "--ServerApp.token=''",
         "--ServerApp.password=''",
         '/workspaces/loops/notebooks/'
       ]
     ports:
-      - 13013:8888
+      - 13013:13013
     volumes:
       - ..:/workspaces/loops/
diff --git a/src/Detail/TacoDiff.tsx b/src/Detail/DataDiff.tsx
similarity index 77%
rename from src/Detail/TacoDiff.tsx
rename to src/Detail/DataDiff.tsx
index e5cd333..ae6d34c 100644
--- a/src/Detail/TacoDiff.tsx
+++ b/src/Detail/DataDiff.tsx
@@ -3,6 +3,8 @@ import { useStyles } from './DiffDetail';
 import { IHTMLDiffProps } from './HTMLDiff';
 import { tabletojson } from 'tabletojson';
 import * as d3 from 'd3-selection';
+import { makePlural } from '../util';
+import { IconAlertTriangle } from '@tabler/icons-react';
 
 export const TacoDiff = ({ newCell, oldCell }: IHTMLDiffProps) => {
   const { classes, cx } = useStyles();
@@ -13,6 +15,13 @@ export const TacoDiff = ({ newCell, oldCell }: IHTMLDiffProps) => {
     setDiffMode(event.target.value);
   };
 
+  const [totalRowChanges, setTotalRowChanges] = React.useState(0);
+  const [totalColChanges, setTotalColumnChanges] = React.useState(0);
+
+  const [rowChanges, setRowChanges] = React.useState(0);
+  const [colChanges, setColumnChanges] = React.useState(0);
+  const [cellChanges, setCellChanges] = React.useState(0);
+
   const unifiedParent = useRef<HTMLDivElement>(null);
   const sideOldParent = useRef<HTMLDivElement>(null);
   const sideNewParent = useRef<HTMLDivElement>(null);
@@ -24,9 +33,33 @@ export const TacoDiff = ({ newCell, oldCell }: IHTMLDiffProps) => {
       const newTable = tabletojson.convert(newOutput, { useFirstRowForHeadings: true });
       const oldTable = tabletojson.convert(oldOutput, { useFirstRowForHeadings: true });
 
-      createSummaryVisualization(unifiedParent.current, newTable[0], oldTable[0], true, true, '#F05268', '#66C2A5');
+      //unified
+      const summary = createSummaryVisualization(
+        unifiedParent.current,
+        oldTable[0],
+        newTable[0],
+        true,
+        true,
+        '#66C2A5',
+        '#F05268'
+      );
+      setRowChanges(summary.rowChanges);
+      setColumnChanges(summary.colChanges);
+      setCellChanges(summary.cellChanges);
+
+      //side-by-side
       createSummaryVisualization(sideNewParent.current, newTable[0], oldTable[0], false, true, '#F05268', '#66C2A5');
       createSummaryVisualization(sideOldParent.current, oldTable[0], newTable[0], false, true);
+
+      const rowChange = oldOutput
+        ? Number(newOutput.match(/(\d+) rows/)?.[1] ?? 0) - Number(oldOutput.match(/(\d+) rows/)?.[1] ?? 0)
+        : 0;
+      setTotalRowChanges(rowChange);
+
+      const colChange = oldOutput
+        ? Number(newOutput.match(/(\d+) columns/)?.[1] ?? 0) - Number(oldOutput.match(/(\d+) columns/)?.[1] ?? 0)
+        : 0;
+      setTotalColumnChanges(colChange);
     });
   }, []);
 
@@ -84,6 +117,38 @@ export const TacoDiff = ({ newCell, oldCell }: IHTMLDiffProps) => {
           <input type="radio" value="unified" checked={diffMode === 'unified'} onChange={handleDiffModeChange} />
           Unified
         </label>
+        <span style={{ fontWeight: 600, marginTop: '1em' }}>Visible Changes</span>
+        <span style={{ background: rowChanges === 0 ? 'inherit' : rowChanges > 0 ? '#66C2A5' : '#F05268' }}>
+          {(rowChanges < 0 ? '' : '+') + rowChanges + makePlural(' row', rowChanges)}
+        </span>
+        <span style={{ background: colChanges === 0 ? 'inherit' : colChanges > 0 ? '#66C2A5' : '#F05268' }}>
+          {(colChanges < 0 ? '' : '+') + colChanges + makePlural(' column', colChanges)}
+        </span>
+        <span style={{ background: cellChanges !== 0 ? '#FBE156' : 'inherit' }}>
+          {cellChanges + makePlural(' changed cell', cellChanges)}
+        </span>
+
+        {totalRowChanges || totalColChanges ? (
+          <span style={{ fontWeight: 600, marginTop: '1em' }}>
+            <IconAlertTriangle size={15}></IconAlertTriangle> Total Changes
+          </span>
+        ) : (
+          <></>
+        )}
+        {totalRowChanges ? (
+          <span style={{ color: totalRowChanges > 0 ? '#66C2A5' : '#F05268' }}>
+            {(totalRowChanges < 0 ? '' : '+') + totalRowChanges + makePlural(' row', totalRowChanges)}
+          </span>
+        ) : (
+          <> </>
+        )}
+        {totalColChanges ? (
+          <span style={{ color: totalColChanges > 0 ? '#66C2A5' : '#F05268' }}>
+            {(totalColChanges < 0 ? '' : '+') + totalColChanges + makePlural(' column', totalColChanges)}
+          </span>
+        ) : (
+          <> </>
+        )}
       </div>
       <div className={cx(classes.monacoWrapper)}>
         <div className={cx(classes.monacoHeader)}>
@@ -142,7 +207,7 @@ export function createSummaryVisualizationFromHTML(
     addColor,
     removeColor,
     showContent
-  );
+  ).node;
 
   if (rowChange !== 0 || colChange !== 0) {
     const pTag = document.createElement('p');
@@ -173,14 +238,13 @@ export function createSummaryVisualization(
   addColor = '#66C2A5',
   removeColor = '#F05268',
   showContent = true
-) {
+): { node: HTMLDivElement; rowChanges: number; colChanges: number; cellChanges: number } {
   const columns = Object.keys(data[0]);
   const referenceColumns = referenceData.length > 0 ? Object.keys(referenceData[0]) : [];
 
   // Determine added and removed columns
   const addedColumns = referenceColumns.filter(column => !columns.includes(column));
   const removedColumns = columns.filter(column => !referenceColumns.includes(column));
-
   // Determine added and removed rows
   const addedRows = showAdded ? referenceData.slice(data.length) : [];
   // const removedRows = showRemoved ? data.slice(referenceData.length) : []; //checked via added rows (= referenceData has more rows )
@@ -204,6 +268,13 @@ export function createSummaryVisualization(
 
   const summaryGrid = wrapper.append('div');
 
+  const summary = {
+    node: wrapper.node(),
+    rowChanges: addedRows.length,
+    colChanges: -removedColumns.length + addedColumns.length,
+    cellChanges: 0
+  };
+
   // Create an div element
   summaryGrid
     .style('display', 'grid')
@@ -294,12 +365,13 @@ export function createSummaryVisualization(
       } else if (d.rowIndex >= referenceData.length) {
         return removeColor; // removed row
       } else if (d.value !== referenceData[d.rowIndex]?.[d.column]) {
+        summary.cellChanges++;
         return '#FBE156'; // changed cell
       }
       return '#F5F5F5'; // Unchanged cells
     });
 
-  return wrapper.node();
+  return summary;
 }
 
 // check if the output is a pandas dataframe
diff --git a/src/Detail/DiffDetail.tsx b/src/Detail/DiffDetail.tsx
index 3c710e5..9c522df 100644
--- a/src/Detail/DiffDetail.tsx
+++ b/src/Detail/DiffDetail.tsx
@@ -13,7 +13,7 @@ import {
 import { TextDiff } from './TextDiff';
 import { ImgDetailDiff } from './ImgDetailDiff';
 import { HTMLDiff } from './HTMLDiff';
-import { TacoDiff, hasDataframe } from './TacoDiff';
+import { TacoDiff, hasDataframe } from './DataDiff';
 
 export const useStyles = createStyles((theme, _params, getRef) => ({
   diffDetail: {
@@ -99,6 +99,8 @@ export class DiffDetail extends ReactWidget {
   render(): JSX.Element {
     const diffTools: { tab: JSX.Element; panel: JSX.Element }[] = [];
 
+    let defaulTab = 'input';
+
     // input diff for code and raw cells
     if (['code', 'raw', 'markdown'].includes(this.current.cell.type)) {
       //adapt syntax highlight to cell type
@@ -154,6 +156,7 @@ export class DiffDetail extends ReactWidget {
           </Tabs.Panel>
         )
       });
+      defaulTab = 'output-md';
     }
 
     // output diff for code cells
@@ -249,6 +252,7 @@ export class DiffDetail extends ReactWidget {
                   </Tabs.Panel>
                 )
               });
+              defaulTab = `output-${outputIndex}-${key}`;
             } else if (key.includes('html')) {
               if (hasDataframe(value)) {
                 //add dataframe diff
@@ -277,6 +281,7 @@ export class DiffDetail extends ReactWidget {
                     </Tabs.Panel>
                   )
                 });
+                defaulTab = `output-${outputIndex}-DataFrame`;
               }
 
               // add html diff
@@ -327,7 +332,7 @@ export class DiffDetail extends ReactWidget {
     // return <>{...diffTools}</>;
     return (
       // Default Value == Tab that is opened on startup
-      <Tabs color="teal" defaultValue="input" orientation="vertical" style={{ height: '100%' }}>
+      <Tabs color="teal" defaultValue={defaulTab} orientation="vertical" style={{ height: '100%' }}>
         <Tabs.List style={{ flexWrap: 'nowrap' }}>
           {/* Order does matter */}
           {...diffTools.map(tool => tool.tab)}
diff --git a/src/Overview/State.tsx b/src/Overview/State.tsx
index 9033996..a7e91b5 100644
--- a/src/Overview/State.tsx
+++ b/src/Overview/State.tsx
@@ -13,7 +13,7 @@ import { ExecutionBadge } from './ExecutionBadge';
 import '@github/relative-time-element';
 import { INotebookTracker } from '@jupyterlab/notebook';
 import { CompareBadge } from './CompareBadge';
-import { createSummaryVisualizationFromHTML, hasDataframe } from '../Detail/TacoDiff';
+import { createSummaryVisualizationFromHTML, hasDataframe } from '../Detail/DataDiff';
 import { createUnifedDiff, hasImage } from '../Detail/ImgDetailDiff';
 import { TypeIcon } from './TypeIcon';
 
@@ -684,13 +684,15 @@ export function State({
           {cell.outputHTML.map((output, j) => {
             let stateOutput = output;
             if (hasDataframe(output)) {
+              const cellColor: string =
+                previousCell && hasDataframe(previousCell.outputHTML[j]) ? '#F5F5F5' : '#66C2A5';
               const tableSummary: HTMLDivElement = createSummaryVisualizationFromHTML(
                 output,
                 undefined,
                 true,
                 true,
-                '#66C2A5',
-                '#66C2A5',
+                cellColor,
+                cellColor,
                 false
               );
 


Test patch:
diff --git a/ui-tests/playwright.config.js b/ui-tests/playwright.config.js
index 9ece6fa..ad08470 100644
--- a/ui-tests/playwright.config.js
+++ b/ui-tests/playwright.config.js
@@ -3,6 +3,7 @@
  */
 const baseConfig = require('@jupyterlab/galata/lib/playwright-config');
 
+console.log('💩🔥 Running Playwright tests with JupyterLab extension');
 module.exports = {
   ...baseConfig,
   webServer: {