From eb0b60f9f06c25e6c75f2f8ff3f2740f4124344c Mon Sep 17 00:00:00 2001
From: Michael Behrisch <m.behrisch@uu.nl>
Date: Thu, 6 Jul 2023 17:28:15 +0200
Subject: [PATCH] fix: :sparkles: adds multiple attributes per node to
 dotplotvis

Once more than one attribute will be pulled into the querypanel
the dotplot vis needs to show also multiple (coordinated) dotplots
---
 .../lib/vis/dotplotsvis/dotplotsvis.tsx       | 521 ++++++++++++------
 1 file changed, 358 insertions(+), 163 deletions(-)

diff --git a/libs/shared/lib/vis/dotplotsvis/dotplotsvis.tsx b/libs/shared/lib/vis/dotplotsvis/dotplotsvis.tsx
index 6a2a1c2d3..cdd45d4dc 100644
--- a/libs/shared/lib/vis/dotplotsvis/dotplotsvis.tsx
+++ b/libs/shared/lib/vis/dotplotsvis/dotplotsvis.tsx
@@ -15,182 +15,374 @@ export const DotPlotsVis = React.memo((props: DotPlotsVisProps) => {
   const dispatch = useAppDispatch();
   const theme = useTheme();
 
-  let [oneRowPoints, setOneRowPoints] = useState([]);
-  let [minMax, setMinMax] = useState([]);
-  let [attributeName, setAttributeName] = useState('');
+  const [dotPlotSpec, setDotPlotSpec] = useState<any>([]);
+
+  // let [oneRowPoints, setOneRowPoints] = useState<number[]>([]);
+  // let [minMax, setMinMax] = useState<number[]>([]);
+  // let [attributeName, setAttributeName] = useState<string>('');
 
   // let [spec, setSpec] = useState({});
-  let spec = {
-    $schema: 'https://vega.github.io/schema/vega/v5.json',
-    description:
-      'A dot plot example depicting the distribution of animal sleep times.',
-    width: 500,
-    padding: 5,
-    autosize: 'pad',
-
-    signals: [
-      {
-        name: 'step',
-        value: (minMax[1] - minMax[0]) / 40, // default 40 bins
-        bind: {
-          input: 'range',
-          min: (minMax[1] - minMax[0]) / 100, // 4 bins - 100 bins
-          max: (minMax[1] - minMax[0]) / 10, // max 100 bins
-          step: 1,
-        },
-      },
-      // {
-      //   name: 'offset',
-      //   value: 'zero',
-      //   bind: { input: 'radio', options: ['zero', 'center'] },
-      // },
-      // {
-      //   name: 'smooth',
-      //   value: true,
-      //   bind: { input: 'checkbox' },
-      // },
-      { name: 'size', update: "scale('x', step) - scale('x', 0)" },
-      { name: 'area', update: 'size * size' },
-      { name: 'ddh', update: '(span(ddext) + 1) * size' },
-      { name: 'hdh', update: '(span(hdext) + 1) * size' },
-      { name: 'height', update: 'max(ddh, hdh)' },
-    ],
-
-    data: [
-      {
-        name: 'points',
-        values: oneRowPoints,
-        transform: [
-          {
-            type: 'dotbin',
-            field: 'data',
-            // smooth: { signal: 'smooth' },
-            smooth: 'true',
-            step: { signal: 'step' },
-          },
-          {
-            type: 'stack',
-            groupby: ['bin'],
-            // offset: { signal: 'offset' },
-            offset: 'zero',
-            as: ['d0', 'd1'],
-          },
-          {
-            type: 'extent',
-            field: 'd1',
-            signal: 'ddext',
-          },
-          {
-            type: 'extent',
-            field: 'data',
-            signal: 'ext',
-          },
-          {
-            type: 'bin',
-            field: 'data',
-            step: { signal: 'step' },
-            extent: { signal: 'ext' },
-          },
-          {
-            type: 'stack',
-            // offset: { signal: 'offset' },
-            offset: 'zero',
+  // let spec = {
+  //   $schema: 'https://vega.github.io/schema/vega/v5.json',
+  //   description:
+  //     'A dot plot example depicting the distribution of animal sleep times.',
+  //   width: 500,
+  //   padding: 5,
+  //   autosize: 'pad',
 
-            groupby: ['bin0'],
-          },
-          {
-            type: 'extent',
-            field: 'y0',
-            signal: 'hdext',
-          },
-        ],
-      },
-    ],
-
-    scales: [
-      {
-        name: 'x',
-        domain: [minMax[0], minMax[1]],
-        range: 'width',
-      },
-      {
-        name: 'ddy',
-        domain: { signal: '[0, ddh / size]' },
-        range: { signal: '[height, height - ddh]' },
-      },
-      {
-        name: 'hdy',
-        domain: { signal: '[0, hdh / size]' },
-        range: { signal: '[height, height - hdh]' },
-      },
-    ],
-
-    marks: [
-      {
-        type: 'group',
-        encode: {
-          update: {
-            width: { signal: 'width' },
-            height: { signal: 'height' },
-          },
-        },
-        axes: [
-          {
-            scale: 'x',
-            orient: 'bottom',
-            tickCount: 5,
-            title: 'Histogram (' + attributeName + ')',
-          },
-        ],
-        marks: [
-          {
-            type: 'symbol',
-            from: { data: 'points' },
-            encode: {
-              update: {
-                x: { scale: 'x', signal: '(datum.bin0 + datum.bin1) / 2' },
-                y: { scale: 'hdy', signal: 'datum.y0 + 0.5' },
-                size: { signal: 'area' },
-                // fill: { value: 'steelblue' },
-                fill: { value: theme.palette.primary.light },
-              },
-              enter: {
-                tooltip: {
-                  signal:
-                    "{'Value range': format(datum.bin0, '0.1f') + ' - ' + format(datum.bin1, '0.1f')}",
-                },
-              },
-              hover: { fill: { value: theme.palette.primary.dark } },
-            },
-          },
-        ],
-      },
-    ],
-  };
+  //   signals: [
+  //     {
+  //       name: 'step',
+  //       value: (minMax[1] - minMax[0]) / 40, // default 40 bins
+  //       bind: {
+  //         input: 'range',
+  //         min: (minMax[1] - minMax[0]) / 100, // 4 bins - 100 bins
+  //         max: (minMax[1] - minMax[0]) / 10, // max 100 bins
+  //         step: 1,
+  //       },
+  //     },
+  //     // {
+  //     //   name: 'offset',
+  //     //   value: 'zero',
+  //     //   bind: { input: 'radio', options: ['zero', 'center'] },
+  //     // },
+  //     // {
+  //     //   name: 'smooth',
+  //     //   value: true,
+  //     //   bind: { input: 'checkbox' },
+  //     // },
+  //     { name: 'size', update: "scale('x', step) - scale('x', 0)" },
+  //     { name: 'area', update: 'size * size' },
+  //     { name: 'ddh', update: '(span(ddext) + 1) * size' },
+  //     { name: 'hdh', update: '(span(hdext) + 1) * size' },
+  //     { name: 'height', update: 'max(ddh, hdh)' },
+  //   ],
+
+  //   data: [
+  //     {
+  //       name: 'points',
+  //       values: oneRowPoints,
+  //       transform: [
+  //         {
+  //           type: 'dotbin',
+  //           field: 'data',
+  //           // smooth: { signal: 'smooth' },
+  //           smooth: 'true',
+  //           step: { signal: 'step' },
+  //         },
+  //         {
+  //           type: 'stack',
+  //           groupby: ['bin'],
+  //           // offset: { signal: 'offset' },
+  //           offset: 'zero',
+  //           as: ['d0', 'd1'],
+  //         },
+  //         {
+  //           type: 'extent',
+  //           field: 'd1',
+  //           signal: 'ddext',
+  //         },
+  //         {
+  //           type: 'extent',
+  //           field: 'data',
+  //           signal: 'ext',
+  //         },
+  //         {
+  //           type: 'bin',
+  //           field: 'data',
+  //           step: { signal: 'step' },
+  //           extent: { signal: 'ext' },
+  //         },
+  //         {
+  //           type: 'stack',
+  //           // offset: { signal: 'offset' },
+  //           offset: 'zero',
+
+  //           groupby: ['bin0'],
+  //         },
+  //         {
+  //           type: 'extent',
+  //           field: 'y0',
+  //           signal: 'hdext',
+  //         },
+  //       ],
+  //     },
+  //   ],
+
+  //   scales: [
+  //     {
+  //       name: 'x',
+  //       domain: [minMax[0], minMax[1]],
+  //       range: 'width',
+  //     },
+  //     {
+  //       name: 'ddy',
+  //       domain: { signal: '[0, ddh / size]' },
+  //       range: { signal: '[height, height - ddh]' },
+  //     },
+  //     {
+  //       name: 'hdy',
+  //       domain: { signal: '[0, hdh / size]' },
+  //       range: { signal: '[height, height - hdh]' },
+  //     },
+  //   ],
+
+  //   marks: [
+  //     {
+  //       type: 'group',
+  //       encode: {
+  //         update: {
+  //           width: { signal: 'width' },
+  //           height: { signal: 'height' },
+  //         },
+  //       },
+  //       axes: [
+  //         {
+  //           scale: 'x',
+  //           orient: 'bottom',
+  //           tickCount: 5,
+  //           tickRound: true,
+  //           tickOpacity: 0.5,
+  //           tickCap: 'round',
+  //           tickExtra: true,
+  //           // tickWidth: 2,
+
+  //           // TODO: adapt the tick according to theme/new design https://vega.github.io/vega/docs/axes/
+  //           labelColor: theme.palette.text.secondary,
+  //           // labelFont: theme.typography.body2.fontFamily,
+  //           // labelFontSize: theme.typography.body2.fontSize,
+  //           // labelFontWeight: theme.typography.body2.fontWeight,
+  //           // labelFontStyle: theme.typography.body2.fontStyle,
+  //           title: 'Histogram (' + attributeName + ')',
+  //         },
+  //       ],
+  //       marks: [
+  //         {
+  //           type: 'symbol',
+  //           from: { data: 'points' },
+  //           encode: {
+  //             update: {
+  //               x: { scale: 'x', signal: '(datum.bin0 + datum.bin1) / 2' },
+  //               y: { scale: 'hdy', signal: 'datum.y0 + 0.5' },
+  //               size: { signal: 'area' },
+  //               // fill: { value: 'steelblue' },
+  //               fill: { value: theme.palette.primary.light },
+  //             },
+  //             enter: {
+  //               tooltip: {
+  //                 signal:
+  //                   "{'Value range': format(datum.bin0, '0.1f') + ' - ' + format(datum.bin1, '0.1f')}",
+  //               },
+  //             },
+  //             hover: { fill: { value: theme.palette.primary.dark } },
+  //           },
+  //         },
+  //       ],
+  //     },
+  //   ],
+  // };
 
   useEffect(() => {
-    console.log('update dotplotvis useEffect', graphQueryResult);
-
-    if (graphQueryResult) {
-      const rowPoints: number[] = [];
-      for (let i = 0; i < graphQueryResult.nodes.length; i++) {
-        // const attributes = graphQueryResult.nodes[i].attributes
-        const value = graphQueryResult.nodes[i].attributes.attribute0;
-        rowPoints.push(value);
-      }
+    console.log(
+      'update dotplotvis useEffect',
+      graphQueryResult,
+      graphQueryResult.nodes[0].attributes
+    );
+
+    if (graphQueryResult && graphQueryResult.nodes.length > 0) {
+      const node = graphQueryResult.nodes[0];
+      const attributes = node.attributes;
+
+      for (const [key, value] of Object.entries(attributes)) {
+        // console.log(`${key}: ${value}`);
 
-      setMinMax([Math.min(...rowPoints), Math.max(...rowPoints)]);
-      setOneRowPoints(rowPoints);
-      setAttributeName('attribute0'); //fake for simple version. needs to be determined from query result
+        if (typeof value == 'number') {
+          // console.log('number', value, key);
+          const rowPoints: number[] = [];
+          for (let i = 0; i < graphQueryResult.nodes.length; i++) {
+            const value = graphQueryResult.nodes[i].attributes[key];
+            rowPoints.push(value);
+          }
+          const dotPlotSpec = generateNewDotPlotSpec(rowPoints, key);
+          setDotPlotSpec((oldArray: any) => [...oldArray, dotPlotSpec]);
+        } else {
+          console.log('attribute is not number: TBD', key);
+        }
+      }
     }
   }, [graphQueryResult]);
 
   useEffect(() => {
     return () => {
+      setDotPlotSpec([]);
       console.log('unloaded dotplotvis');
     };
   }, []);
 
+  function generateNewDotPlotSpec(rowPoints: number[], attributeName: string) {
+    const minMax = [Math.min(...rowPoints), Math.max(...rowPoints)];
+
+    return {
+      $schema: 'https://vega.github.io/schema/vega/v5.json',
+      description:
+        'A dot plot example depicting the distribution of animal sleep times.',
+      width: 500,
+      padding: 5,
+      autosize: 'pad',
+
+      signals: [
+        {
+          name: 'step',
+          value: (minMax[1] - minMax[0]) / 40, // default 40 bins
+          bind: {
+            input: 'range',
+            min: (minMax[1] - minMax[0]) / 100, // 4 bins - 100 bins
+            max: (minMax[1] - minMax[0]) / 10, // max 100 bins
+            step: 1,
+          },
+        },
+        // {
+        //   name: 'offset',
+        //   value: 'zero',
+        //   bind: { input: 'radio', options: ['zero', 'center'] },
+        // },
+        // {
+        //   name: 'smooth',
+        //   value: true,
+        //   bind: { input: 'checkbox' },
+        // },
+        { name: 'size', update: "scale('x', step) - scale('x', 0)" },
+        { name: 'area', update: 'size * size' },
+        { name: 'ddh', update: '(span(ddext) + 1) * size' },
+        { name: 'hdh', update: '(span(hdext) + 1) * size' },
+        { name: 'height', update: 'max(ddh, hdh)' },
+      ],
+
+      data: [
+        {
+          name: 'points',
+          values: rowPoints,
+          transform: [
+            {
+              type: 'dotbin',
+              field: 'data',
+              // smooth: { signal: 'smooth' },
+              smooth: 'true',
+              step: { signal: 'step' },
+            },
+            {
+              type: 'stack',
+              groupby: ['bin'],
+              // offset: { signal: 'offset' },
+              offset: 'zero',
+              as: ['d0', 'd1'],
+            },
+            {
+              type: 'extent',
+              field: 'd1',
+              signal: 'ddext',
+            },
+            {
+              type: 'extent',
+              field: 'data',
+              signal: 'ext',
+            },
+            {
+              type: 'bin',
+              field: 'data',
+              step: { signal: 'step' },
+              extent: { signal: 'ext' },
+            },
+            {
+              type: 'stack',
+              // offset: { signal: 'offset' },
+              offset: 'zero',
+
+              groupby: ['bin0'],
+            },
+            {
+              type: 'extent',
+              field: 'y0',
+              signal: 'hdext',
+            },
+          ],
+        },
+      ],
+
+      scales: [
+        {
+          name: 'x',
+          domain: [minMax[0], minMax[1]],
+          range: 'width',
+        },
+        {
+          name: 'ddy',
+          domain: { signal: '[0, ddh / size]' },
+          range: { signal: '[height, height - ddh]' },
+        },
+        {
+          name: 'hdy',
+          domain: { signal: '[0, hdh / size]' },
+          range: { signal: '[height, height - hdh]' },
+        },
+      ],
+
+      marks: [
+        {
+          type: 'group',
+          encode: {
+            update: {
+              width: { signal: 'width' },
+              height: { signal: 'height' },
+            },
+          },
+          axes: [
+            {
+              scale: 'x',
+              orient: 'bottom',
+              tickCount: 5,
+              tickRound: true,
+              tickOpacity: 0.5,
+              tickCap: 'round',
+              tickExtra: true,
+              // tickWidth: 2,
+
+              // TODO: adapt the tick according to theme/new design https://vega.github.io/vega/docs/axes/
+              labelColor: theme.palette.text.secondary,
+              // labelFont: theme.typography.body2.fontFamily,
+              // labelFontSize: theme.typography.body2.fontSize,
+              // labelFontWeight: theme.typography.body2.fontWeight,
+              // labelFontStyle: theme.typography.body2.fontStyle,
+              title: 'Histogram (' + attributeName + ')',
+            },
+          ],
+          marks: [
+            {
+              type: 'symbol',
+              from: { data: 'points' },
+              encode: {
+                update: {
+                  x: { scale: 'x', signal: '(datum.bin0 + datum.bin1) / 2' },
+                  y: { scale: 'hdy', signal: 'datum.y0 + 0.5' },
+                  size: { signal: 'area' },
+                  // fill: { value: 'steelblue' },
+                  fill: { value: theme.palette.primary.light },
+                },
+                enter: {
+                  tooltip: {
+                    signal:
+                      "{'Value range': format(datum.bin0, '0.1f') + ' - ' + format(datum.bin1, '0.1f')}",
+                  },
+                },
+                hover: { fill: { value: theme.palette.primary.dark } },
+              },
+            },
+          ],
+        },
+      ],
+    };
+  }
+
   const loading = props.loading;
 
   return (
@@ -204,7 +396,10 @@ export const DotPlotsVis = React.memo((props: DotPlotsVisProps) => {
             }}
           >
             <h1>DotPlotsVis</h1>
-            <Vega spec={spec} />
+            {/* <Vega spec={spec} /> */}
+            {dotPlotSpec.map((spec: any, i: number) => (
+              <Vega key={i} spec={spec} />
+            ))}
             <div id="vis"></div>
           </div>
         </div>
@@ -230,4 +425,4 @@ export const DotPlotsVis = React.memo((props: DotPlotsVisProps) => {
   );
 });
 
-DotPlotsVis.displayName = 'RawJSONVis';
+DotPlotsVis.displayName = 'DotPlotVis';
-- 
GitLab