From 7041be447d37e9e5139bc8f3b34b844e123de248 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Tue, 17 Sep 2024 10:06:29 -0400 Subject: [PATCH 1/4] Add selected highlight to bars --- src/plugins/bar-graph/bar-graph-content.ts | 33 +++++++---- src/plugins/bar-graph/bar-graph-types.ts | 2 + src/plugins/bar-graph/chart-area.tsx | 68 +++++++++++++++++----- 3 files changed, 77 insertions(+), 26 deletions(-) diff --git a/src/plugins/bar-graph/bar-graph-content.ts b/src/plugins/bar-graph/bar-graph-content.ts index 60a157198..bbbacfc2d 100644 --- a/src/plugins/bar-graph/bar-graph-content.ts +++ b/src/plugins/bar-graph/bar-graph-content.ts @@ -1,7 +1,7 @@ import { types, Instance } from "mobx-state-tree"; -import { isNumber } from "lodash"; +import { isObject } from "lodash"; import { ITileContentModel, TileContentModel } from "../../models/tiles/tile-content"; -import { kBarGraphTileType, kBarGraphContentType } from "./bar-graph-types"; +import { kBarGraphTileType, kBarGraphContentType, BarInfo } from "./bar-graph-types"; import { getSharedModelManager } from "../../models/tiles/tile-environment"; import { SharedDataSet, SharedDataSetType } from "../../models/shared/shared-data-set"; import { clueDataColorInfo } from "../../utilities/color-utils"; @@ -72,30 +72,38 @@ export const BarGraphContentModel = TileContentModel return cases.reduce((acc, caseID) => { const cat = displayValue(dataSet.getStrValue(caseID.__id__, primary)); const subCat = displayValue(dataSet.getStrValue(caseID.__id__, secondary)); + const selected = dataSet.isCaseSelected(caseID.__id__); const index = acc.findIndex(r => r[primary] === cat); if (index >= 0) { const cur = acc[index][subCat]; - acc[index][subCat] = (isNumber(cur) ? cur : 0) + 1; + if (isObject(cur)) { + acc[index][subCat] = { count: cur.count + 1, selected: cur.selected || selected }; + } else { + acc[index][subCat] = { count: 1, selected }; + } } else { - const newRow = { [primary]: cat, [subCat]: 1 }; + const newRow = { [primary]: cat, [subCat]: { count: 1, selected } }; acc.push(newRow); } return acc; - }, [] as { [key: string]: number | string }[]); + }, [] as { [key: string]: BarInfo | string }[]); } else { // One-dimensional data return cases.reduce((acc, caseID) => { const cat = displayValue(dataSet.getStrValue(caseID.__id__, primary)); + const selected = dataSet.isCaseSelected(caseID.__id__); const index = acc.findIndex(r => r[primary] === cat); if (index >= 0) { const cur = acc[index].value; - acc[index].value = isNumber(cur) ? cur + 1 : 1; + if (isObject(cur)) { + acc[index].value = { count: cur.count + 1, selected: cur.selected || selected }; + } } else { - const newRow = { [primary]: cat, value: 1 }; + const newRow = { [primary]: cat, value: { count: 1, selected } }; acc.push(newRow); } return acc; - }, [] as { [key: string]: number | string }[]); + }, [] as { [key: string]: BarInfo | string }[]); } } })) @@ -106,13 +114,14 @@ export const BarGraphContentModel = TileContentModel return self.dataArray.map(d => d[primary] as string); }, get secondaryKeys() { - const primary = self.primaryAttribute; - if (!primary) return []; - return Array.from(new Set(self.dataArray.flatMap(d => Object.keys(d)).filter(k => k !== primary))); + const dataSet = self.sharedModel?.dataSet; + const secondary = self.secondaryAttribute; + if (!secondary || !dataSet || !self.cases) return []; + return Array.from(new Set(self.cases.map(caseID => displayValue(dataSet.getStrValue(caseID.__id__, secondary))))); }, get maxDataValue(): number { return self.dataArray.reduce((acc, row) => { - const rowValues = Object.values(row).filter(v => isNumber(v)) as number[]; + const rowValues = Object.values(row).map(v => isObject(v) ? v.count : 0); const maxInRow = Math.max(...rowValues); return Math.max(maxInRow, acc); }, 0); diff --git a/src/plugins/bar-graph/bar-graph-types.ts b/src/plugins/bar-graph/bar-graph-types.ts index 9bbbf9cc4..4a6f2eb12 100644 --- a/src/plugins/bar-graph/bar-graph-types.ts +++ b/src/plugins/bar-graph/bar-graph-types.ts @@ -3,3 +3,5 @@ export const kBarGraphTileType = "BarGraph"; export const kBarGraphContentType = "BarGraphContentModel"; export const kBarGraphDefaultHeight = 320; + +export type BarInfo = { count: number, selected: boolean }; diff --git a/src/plugins/bar-graph/chart-area.tsx b/src/plugins/bar-graph/chart-area.tsx index f25099718..cc7768771 100644 --- a/src/plugins/bar-graph/chart-area.tsx +++ b/src/plugins/bar-graph/chart-area.tsx @@ -5,10 +5,12 @@ import { GridRows } from "@visx/grid"; import { Group } from "@visx/group"; import { scaleBand, scaleLinear } from "@visx/scale"; import { Bar, BarGroup } from "@visx/shape"; +import { PositionScale } from "@visx/shape/lib/types"; import { useBarGraphModelContext } from "./bar-graph-content-context"; import { CategoryPulldown } from "./category-pulldown"; import EditableAxisLabel from "./editable-axis-label"; import { logBarGraphEvent, roundTo5 } from "./bar-graph-utils"; +import { BarInfo } from "./bar-graph-types"; const margin = { top: 7, @@ -96,16 +98,13 @@ export const ChartArea = observer(function BarGraphChart({ width, height }: IPro {data.map((d) => { const key = d[primary] as string; - const val = d.value as number; + const info = d.value as BarInfo; + const x = primaryScale(key) || 0; + const y = countScale(info.count); + const w = primaryScale.bandwidth(); + const h = yMax - countScale(info.count); return ( - + ); })} @@ -122,21 +121,25 @@ export const ChartArea = observer(function BarGraphChart({ width, height }: IPro x0={(d) => d[primary] as string} x0Scale={primaryScale} x1Scale={secondaryScale} - yScale={countScale} + yScale={((info: BarInfo) => countScale(info?.count||0)) as PositionScale} > {(barGroups) => {barGroups.map((barGroup) => ( - + {barGroup.bars.map((bar) => { if (!bar.value) return null; - return ; })} @@ -192,3 +195,40 @@ export const ChartArea = observer(function BarGraphChart({ width, height }: IPro ); }); + +interface IBarHighlightProps { + x: number; + y: number; + width: number; + height: number; +} + +function BarHighlight({ x, y, width, height }: IBarHighlightProps) { + return( + + ); +} + +interface IBarWithHighlightProps { + x: number; + y: number; + width: number; + height: number; + color: string; + selected: boolean; +} + +function BarWithHighlight({ x, y, width, height, color, selected }: IBarWithHighlightProps) { + return ( + + {selected && } + + + ); +} From a39f2794ed5f5e28a5476943fae94a51ddbe9139 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Tue, 17 Sep 2024 11:28:22 -0400 Subject: [PATCH 2/4] Click on bars to select --- .../bar-graph/bar-graph-content.test.ts | 26 ++++---- src/plugins/bar-graph/bar-graph-content.ts | 16 +++++ src/plugins/bar-graph/bar-graph-utils.ts | 5 +- src/plugins/bar-graph/chart-area.tsx | 65 ++++++++++++------- 4 files changed, 73 insertions(+), 39 deletions(-) diff --git a/src/plugins/bar-graph/bar-graph-content.test.ts b/src/plugins/bar-graph/bar-graph-content.test.ts index 565a7f56f..79c70942b 100644 --- a/src/plugins/bar-graph/bar-graph-content.test.ts +++ b/src/plugins/bar-graph/bar-graph-content.test.ts @@ -107,14 +107,14 @@ Object { content.setSharedModel(sharedSampleDataSet()); content.setPrimaryAttribute("att-s"); expect(content.dataArray).toEqual([ - { "att-s": "cat", "value": 2 }, - { "att-s": "owl","value": 2} + { "att-s": "cat", "value": { count: 2, selected: false }}, + { "att-s": "owl","value": { count: 2, selected: false }} ]); content.setPrimaryAttribute("att-l"); expect(content.dataArray).toEqual([ - { "att-l": "yard", "value": 3 }, - { "att-l": "forest", "value": 1 } + { "att-l": "yard", "value": { count: 3, selected: false }}, + { "att-l": "forest", "value": { count: 1, selected: false }} ]); }); @@ -122,8 +122,8 @@ Object { const content = TestingBarGraphContentModel.create({ }); content.setSharedModel(sharedSampleDataSet()); expect(content.dataArray).toEqual([ - { "att-s": "cat", "value": 2 }, - { "att-s": "owl","value": 2} + { "att-s": "cat", "value": { count: 2, selected: false }}, + { "att-s": "owl","value": { count: 2, selected: false }} ]); }); @@ -133,8 +133,8 @@ Object { content.setPrimaryAttribute("att-s"); content.setSecondaryAttribute("att-l"); expect(content.dataArray).toEqual([ - { "att-s": "cat", "yard": 2 }, - { "att-s": "owl", "yard": 1, "forest": 1 } + { "att-s": "cat", "yard": { count: 2, selected: false }}, + { "att-s": "owl", "yard": { count: 1, selected: false }, "forest": { count: 1, selected: false }} ]); }); @@ -146,15 +146,15 @@ Object { content.setPrimaryAttribute("att-s"); content.setSecondaryAttribute("att-l"); expect(content.dataArray).toEqual([ - { "att-s": "cat", "yard": 2 }, - { "att-s": "owl", "yard": 1, "(no value)": 1 } + { "att-s": "cat", "yard": { count: 2, selected: false }}, + { "att-s": "owl", "yard": { count: 1, selected: false}, "(no value)": { count: 1, selected: false }} ]); dataSet.dataSet?.attributes[0].setValue(3, undefined); // hide that owl entirely expect(content.dataArray).toEqual([ - { "att-s": "cat", "yard": 2 }, - { "att-s": "owl", "yard": 1 }, - { "att-s": "(no value)", "(no value)": 1 } + { "att-s": "cat", "yard": { count: 2, selected: false }}, + { "att-s": "owl", "yard": { count: 1, selected: false }}, + { "att-s": "(no value)", "(no value)": { count: 1, selected: false }} ]); }); diff --git a/src/plugins/bar-graph/bar-graph-content.ts b/src/plugins/bar-graph/bar-graph-content.ts index bbbacfc2d..7ef2a9a53 100644 --- a/src/plugins/bar-graph/bar-graph-content.ts +++ b/src/plugins/bar-graph/bar-graph-content.ts @@ -145,6 +145,22 @@ export const BarGraphContentModel = TileContentModel }, setSecondaryAttribute(attrId: string|undefined) { self.secondaryAttribute = attrId; + }, + selectCasesByValues(primaryVal: string, secondaryVal?: string) { + const dataSet = self.sharedModel?.dataSet; + const cases = self.cases; + const primaryAttribute = self.primaryAttribute; + if (!dataSet || !cases || !primaryAttribute) return; + const secondaryAttribute = self.secondaryAttribute; + if (!secondaryAttribute && secondaryVal) return; + let matchingCases = cases + .filter(caseID => displayValue(dataSet.getStrValue(caseID.__id__, primaryAttribute)) === primaryVal); + if (secondaryAttribute && secondaryVal) { + matchingCases = matchingCases + .filter(caseID => displayValue(dataSet.getStrValue(caseID.__id__, secondaryAttribute)) === secondaryVal); + } + const caseIds = matchingCases.map(caseID => caseID.__id__); + dataSet.setSelectedCases(caseIds); } })) .actions(self => ({ diff --git a/src/plugins/bar-graph/bar-graph-utils.ts b/src/plugins/bar-graph/bar-graph-utils.ts index e5a1d8681..a742c6c91 100644 --- a/src/plugins/bar-graph/bar-graph-utils.ts +++ b/src/plugins/bar-graph/bar-graph-utils.ts @@ -37,9 +37,10 @@ export function updateBarGraphContentWithNewSharedModelIds( } // Define types here to document all possible values that this tile logs -type LoggableOperation = "setPrimaryAttribute" | "setSecondaryAttribute" | "setYAxisLabel"; +type LoggableOperation = "setPrimaryAttribute" | "setSecondaryAttribute" | "setYAxisLabel" | "selectCases"; type LoggableChange = { - attributeId?: string; + attributeId?: string | string[]; + attributeValue?: string | string[]; text?: string; }; diff --git a/src/plugins/bar-graph/chart-area.tsx b/src/plugins/bar-graph/chart-area.tsx index cc7768771..d0e9d89f2 100644 --- a/src/plugins/bar-graph/chart-area.tsx +++ b/src/plugins/bar-graph/chart-area.tsx @@ -92,6 +92,16 @@ export const ChartArea = observer(function BarGraphChart({ width, height }: IPro const labelWidth = (xMax/primaryKeys.length)-10; // setting width will wrap lines when needed + function handleClick(primaryValue: string, secondaryValue?: string) { + if (!model || !model.primaryAttribute) return; + model.selectCasesByValues(primaryValue, secondaryValue); + logBarGraphEvent(model, "selectCases", { + attributeId: + model.secondaryAttribute ? [model.primaryAttribute, model.secondaryAttribute] : model.primaryAttribute, + attributeValue: secondaryValue ? [primaryValue, secondaryValue] : primaryValue + }); + } + function simpleBars() { const color = barColor(primary); return ( @@ -104,7 +114,8 @@ export const ChartArea = observer(function BarGraphChart({ width, height }: IPro const w = primaryScale.bandwidth(); const h = yMax - countScale(info.count); return ( - + handleClick(key)} /> ); })} @@ -123,29 +134,34 @@ export const ChartArea = observer(function BarGraphChart({ width, height }: IPro x1Scale={secondaryScale} yScale={((info: BarInfo) => countScale(info?.count||0)) as PositionScale} > - {(barGroups) => - - {barGroups.map((barGroup) => ( - - {barGroup.bars.map((bar) => { - if (!bar.value) return null; - // BarGroup really expects the values to be pure numeric, but we're using objects. - // Alternatively, we could drop BarGroup and build the bars manually. - const val = bar.value as unknown as BarInfo; - return ; + {(barGroups) => { + return ( + + {barGroups.map((barGroup) => { + const primaryValue = data[barGroup.index][primary] as string; + return ( + + {barGroup.bars.map((bar) => { + if (!bar.value) return null; + // BarGroup really expects the values to be pure numeric, but we're using objects. + // Alternatively, we could drop BarGroup and build the bars manually. + const val = bar.value as unknown as BarInfo; + return handleClick(primaryValue, bar.key)} />; + })} + + ); })} - ))} - - } + ); + }} ); } @@ -222,13 +238,14 @@ interface IBarWithHighlightProps { height: number; color: string; selected: boolean; + onClick: () => void; } -function BarWithHighlight({ x, y, width, height, color, selected }: IBarWithHighlightProps) { +function BarWithHighlight({ x, y, width, height, color, selected, onClick }: IBarWithHighlightProps) { return ( {selected && } - + ); } From 861db2a029f3ff4d26273ec4f1f46a43e8104c3e Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Tue, 17 Sep 2024 15:42:45 -0400 Subject: [PATCH 3/4] Add tests --- .../tile_tests/bar_graph_tile_spec.js | 156 ++++++++++++++++++ cypress/support/elements/tile/BarGraphTile.js | 4 + .../support/elements/tile/TableToolTile.js | 3 + .../bar-graph/bar-graph-content.test.ts | 71 ++++++++ src/plugins/bar-graph/chart-area.tsx | 1 + 5 files changed, 235 insertions(+) diff --git a/cypress/e2e/functional/tile_tests/bar_graph_tile_spec.js b/cypress/e2e/functional/tile_tests/bar_graph_tile_spec.js index 57a15d2fe..530b77c36 100644 --- a/cypress/e2e/functional/tile_tests/bar_graph_tile_spec.js +++ b/cypress/e2e/functional/tile_tests/bar_graph_tile_spec.js @@ -335,4 +335,160 @@ context('Bar Graph Tile', function () { } }); + it.only('Synchronizes selection', function () { + beforeTest(); + + clueCanvas.addTile('bargraph'); + + // Table dataset for testing: + // 4 instances of X / Y / Z + // 2 instances of XX / Y / Z + // 1 instance of X / YY / Z + clueCanvas.addTile('table'); + tableTile.fillTable(tableTile.getTableTile(), [ + ['X', 'Y', 'Z'], + ['XX', 'Y', 'Z'], + ['X', 'YY', 'Z'], + ['X', 'Y', 'Z'], + ['XX', 'Y', 'Z'], + ['X', 'Y', 'Z'], + ['X', 'Y', 'Z'], + ]); + + barGraph.getTile().click(); + clueCanvas.clickToolbarButton('bargraph', 'link-tile'); + cy.get('select').select('Table Data 1'); + cy.get('.modal-button').contains("Graph It!").click(); + + cy.log("Check synchronization of case selection with one attribute"); + + // Selecting cases in the table should highlight the corresponding bars in the bar graph + tableTile.getSelectedRow(workspaces[0]).should('have.length', 0); + for (const workspace of workspaces) { + barGraph.getBarHighlight(workspace).should('have.length', 0); + } + tableTile.getTableIndexColumnCell().eq(0).click(); // first X Y Z case, X bar selected + tableTile.getSelectedRow(workspaces[0]).should('have.length', 1); + barGraph.getBarHighlight(workspaces[0]).should('have.length', 1); + barGraph.getBarHighlight(workspaces[1]).should('have.length', 1); + // Selection is local, volatile state, so remote workspace should not be affected + barGraph.getBarHighlight(workspaces[2]).should('have.length', 0); + tableTile.getTableIndexColumnCell().eq(1).click({shiftKey: true}); // first XX Y Z case, X and XX bars selected + for (const workspace of workspaces.slice(0, 2)) { + barGraph.getBarHighlight(workspace).should('have.length', 2); + } + tableTile.getTableIndexColumnCell().eq(2).click({shiftKey: true}); // first X YY Z case, X and XX bars selected + for (const workspace of workspaces.slice(0, 2)) { + barGraph.getBarHighlight(workspace).should('have.length', 2); + } + tableTile.getTableIndexColumnCell().eq(0).click({shiftKey: true}); + for (const workspace of workspaces.slice(0, 2)) { + barGraph.getBarHighlight(workspace).should('have.length', 2); + } + tableTile.getTableIndexColumnCell().eq(1).click({shiftKey: true}); + for (const workspace of workspaces.slice(0, 2)) { + barGraph.getBarHighlight(workspace).should('have.length', 1); + } + tableTile.getTableIndexColumnCell().eq(2).click({shiftKey: true}); + for (const workspace of workspaces.slice(0, 2)) { + barGraph.getBarHighlight(workspace).should('have.length', 0); + } + + // Clicking on bars should select the corresponding cases in the table + tableTile.getSelectedRow(workspaces[0]).should('have.length', 0); + barGraph.getBar().eq(0).click(); + for (const workspace of workspaces.slice(0, 2)) { + tableTile.getSelectedRow(workspace).should('have.length', 5); // All "X" cases + } + barGraph.getBar().eq(1).click(); + for (const workspace of workspaces.slice(0, 2)) { + tableTile.getSelectedRow(workspace).should('have.length', 2); // All "XX" cases + } + // Unselect the two selected cases, which should be numbers 1 and 4 + tableTile.getTableIndexColumnCell().eq(1).click({shiftKey: true}); + tableTile.getTableIndexColumnCell().eq(4).click({shiftKey: true}); + tableTile.getSelectedRow(workspaces[0]).should('have.length', 0); + + cy.log("Check synchronization of case selection with two attributes"); + barGraph.getSortByMenuButton().click(); + barGraph.getChakraMenuItem().should('have.length', 3); + barGraph.getChakraMenuItem().eq(1).should('have.text', 'y').click(); + barGraph.getBar().should("have.length", 3); + + tableTile.getTableIndexColumnCell().eq(0).click(); // first X Y Z case, X Y bar selected + tableTile.getSelectedRow(workspaces[0]).should('have.length', 1); + barGraph.getBarHighlight(workspaces[0]).should('have.length', 1); + barGraph.getBarHighlight(workspaces[1]).should('have.length', 1); + // Selection is local, volatile state, so remote workspace should not be affected + barGraph.getBarHighlight(workspaces[2]).should('have.length', 0); + + tableTile.getTableIndexColumnCell().eq(1).click({shiftKey: true}); // first XX Y Z case, X Y and XX Y bars selected + for (const workspace of workspaces.slice(0, 2)) { + barGraph.getBarHighlight(workspace).should('have.length', 2); + } + tableTile.getTableIndexColumnCell().eq(2).click({shiftKey: true}); // first X YY Z case, X Y, XX Y, and X YY bars selected + for (const workspace of workspaces.slice(0, 2)) { + barGraph.getBarHighlight(workspace).should('have.length', 3); + } + tableTile.getTableIndexColumnCell().eq(0).click({shiftKey: true}); + for (const workspace of workspaces.slice(0, 2)) { + barGraph.getBarHighlight(workspace).should('have.length', 2); + } + tableTile.getTableIndexColumnCell().eq(1).click({shiftKey: true}); + for (const workspace of workspaces.slice(0, 2)) { + barGraph.getBarHighlight(workspace).should('have.length', 1); + } + tableTile.getTableIndexColumnCell().eq(2).click({shiftKey: true}); + for (const workspace of workspaces.slice(0, 2)) { + barGraph.getBarHighlight(workspace).should('have.length', 0); + } + + // Clicking on bars should select the corresponding cases in the table + barGraph.getBar().eq(0).click(); + for (const workspace of workspaces.slice(0, 2)) { + tableTile.getSelectedRow(workspace).should('have.length', 4); // All "X / Y" cases + } + barGraph.getBar().eq(1).click(); + for (const workspace of workspaces.slice(0, 2)) { + tableTile.getSelectedRow(workspace).should('have.length', 1); // All "X / YY" cases + } + barGraph.getBar().eq(2).click(); + for (const workspace of workspaces.slice(0, 2)) { + tableTile.getSelectedRow(workspace).should('have.length', 2); // All "XX / Y" cases + } + + // Clicking bars in local read-only view also works and changes the main view, but not the remote view. + barGraph.getBar(workspaces[1]).eq(0).click(); + for (const workspace of workspaces.slice(0, 2)) { + barGraph.getBarHighlight(workspace).should('have.length', 1); + tableTile.getSelectedRow(workspace).should('have.length', 4); // All "X / Y" cases + } + barGraph.getBar(workspaces[1]).eq(1).click(); + for (const workspace of workspaces.slice(0, 2)) { + barGraph.getBarHighlight(workspace).should('have.length', 1); + tableTile.getSelectedRow(workspace).should('have.length', 1); // All "X / YY" cases + } + barGraph.getBar(workspaces[1]).eq(2).click(); + for (const workspace of workspaces.slice(0, 2)) { + barGraph.getBarHighlight(workspace).should('have.length', 1); + tableTile.getSelectedRow(workspace).should('have.length', 2); // All "XX / Y" cases + } + // Unselect the two remaining selected cases + tableTile.getTableIndexColumnCell().eq(1).click({shiftKey: true}); + tableTile.getTableIndexColumnCell().eq(4).click({shiftKey: true}); + tableTile.getSelectedRow(workspaces[0]).should('have.length', 0); + + // Clicking bars in remote read-only view does not change the main view, but does change itself. + barGraph.getBarHighlight(workspaces[2]).should('have.length', 0); + tableTile.getSelectedRow(workspaces[2]).should('have.length', 0); + barGraph.getBar(workspaces[2]).eq(0).click(); + barGraph.getBarHighlight(workspaces[2]).should('have.length', 1); + tableTile.getSelectedRow(workspaces[2]).should('have.length', 4); // All "X / Y" cases + for (const workspace of workspaces.slice(0, 2)) { + barGraph.getBarHighlight(workspace).should('have.length', 0); + tableTile.getSelectedRow(workspace).should('have.length', 0); // All "XX / Y" cases + } + + }); + }); diff --git a/cypress/support/elements/tile/BarGraphTile.js b/cypress/support/elements/tile/BarGraphTile.js index d19a3b1d9..fdf85d7b2 100644 --- a/cypress/support/elements/tile/BarGraphTile.js +++ b/cypress/support/elements/tile/BarGraphTile.js @@ -56,6 +56,10 @@ class BarGraphTile { return this.getChartArea(workspaceClass, tileIndex).find(`.visx-bar`); } + getBarHighlight(workspaceClass, tileIndex = 0) { + return this.getChartArea(workspaceClass, tileIndex).find(`.bar-highlight`); + } + getLegendArea(workspaceClass, tileIndex = 0) { return this.getTile(workspaceClass, tileIndex).find(`.bar-graph-legend`); } diff --git a/cypress/support/elements/tile/TableToolTile.js b/cypress/support/elements/tile/TableToolTile.js index 68fb922ff..947a8172d 100644 --- a/cypress/support/elements/tile/TableToolTile.js +++ b/cypress/support/elements/tile/TableToolTile.js @@ -41,6 +41,9 @@ class TableToolTile{ getTableRow(){ return cy.get('.canvas .rdg-row'); } + getSelectedRow(workspaceClass) { + return cy.get(`${wsclass(workspaceClass)} .canvas .rdg-row.highlighted`); + } getColumnHeaderText(i){ return cy.get('.column-header-cell .editable-header-cell .header-name').text(); } diff --git a/src/plugins/bar-graph/bar-graph-content.test.ts b/src/plugins/bar-graph/bar-graph-content.test.ts index 79c70942b..e4788f7ca 100644 --- a/src/plugins/bar-graph/bar-graph-content.test.ts +++ b/src/plugins/bar-graph/bar-graph-content.test.ts @@ -118,6 +118,28 @@ Object { ]); }); + it("returns expected array when a case is selected with primary attribute", () => { + const content = TestingBarGraphContentModel.create({ }); + content.setSharedModel(sharedSampleDataSet()); + content.setPrimaryAttribute("att-s"); + content.sharedModel?.dataSet.setSelectedCases([content.sharedModel?.dataSet.cases[0].__id__]); + expect(content.dataArray).toEqual([ + { "att-s": "cat", "value": { count: 2, selected: true }}, + { "att-s": "owl", "value": { count: 2, selected: false }} + ]); + content.sharedModel?.dataSet.setSelectedCases([content.sharedModel?.dataSet.cases[2].__id__]); + expect(content.dataArray).toEqual([ + { "att-s": "cat", "value": { count: 2, selected: false }}, + { "att-s": "owl","value": { count: 2, selected: true }} + ]); + content.sharedModel?.dataSet.selectAllCases(); + expect(content.dataArray).toEqual([ + { "att-s": "cat", "value": { count: 2, selected: true }}, + { "att-s": "owl","value": { count: 2, selected: true }} + ]); + + }); + it("sets first dataset attribute as the primary attribute by default", () => { const content = TestingBarGraphContentModel.create({ }); content.setSharedModel(sharedSampleDataSet()); @@ -138,6 +160,55 @@ Object { ]); }); + it("returns expected array when a case is selected with primary and secondary attributes", () => { + const content = TestingBarGraphContentModel.create({ }); + content.setSharedModel(sharedSampleDataSet()); + content.setPrimaryAttribute("att-s"); + content.setSecondaryAttribute("att-l"); + content.sharedModel?.dataSet.setSelectedCases([content.sharedModel?.dataSet.cases[0].__id__]); + expect(content.dataArray).toEqual([ + { "att-s": "cat", "yard": { count: 2, selected: true }}, + { "att-s": "owl", "yard": { count: 1, selected: false }, "forest": { count: 1, selected: false }} + ]); + content.sharedModel?.dataSet.setSelectedCases([content.sharedModel?.dataSet.cases[3].__id__]); + expect(content.dataArray).toEqual([ + { "att-s": "cat", "yard": { count: 2, selected: false }}, + { "att-s": "owl", "yard": { count: 1, selected: false }, "forest": { count: 1, selected: true }} + ]); + content.sharedModel?.dataSet.selectAllCases(); + expect(content.dataArray).toEqual([ + { "att-s": "cat", "yard": { count: 2, selected: true }}, + { "att-s": "owl", "yard": { count: 1, selected: true }, "forest": { count: 1, selected: true }} + ]); + }); + + it("selects cases based on primary and secondary attributes", () => { + const content = TestingBarGraphContentModel.create({ }); + content.setSharedModel(sharedSampleDataSet()); + const dataSet = content.sharedModel?.dataSet; + expect(dataSet).toBeDefined(); + content.setPrimaryAttribute("att-s"); + content.setSecondaryAttribute("att-l"); + + content.selectCasesByValues("cat", undefined); + expect(dataSet?.selectedCaseIds.map(c => dataSet?.caseIndexFromID(c))).toEqual([0, 1]); + + content.selectCasesByValues("owl", undefined); + expect(dataSet?.selectedCaseIds.map(c => dataSet?.caseIndexFromID(c))).toEqual([2, 3]); + + content.selectCasesByValues("cat", "yard"); + expect(dataSet?.selectedCaseIds.map(c => dataSet?.caseIndexFromID(c))).toEqual([0, 1]); + + content.selectCasesByValues("owl", "yard"); + expect(dataSet?.selectedCaseIds.map(c => dataSet?.caseIndexFromID(c))).toEqual([2]); + + content.selectCasesByValues("owl", "forest"); + expect(dataSet?.selectedCaseIds.map(c => dataSet?.caseIndexFromID(c))).toEqual([3]); + + content.selectCasesByValues("cat", "forest"); + expect(dataSet?.selectedCaseIds.map(c => dataSet?.caseIndexFromID(c))).toEqual([]); + }); + it("fills in missing values with (no value)", () => { const content = TestingBarGraphContentModel.create({ }); const dataSet = sharedSampleDataSet(); diff --git a/src/plugins/bar-graph/chart-area.tsx b/src/plugins/bar-graph/chart-area.tsx index d0e9d89f2..67364946d 100644 --- a/src/plugins/bar-graph/chart-area.tsx +++ b/src/plugins/bar-graph/chart-area.tsx @@ -227,6 +227,7 @@ function BarHighlight({ x, y, width, height }: IBarHighlightProps) { width={width + 8} height={height + 8} fill="#14F49E" + className="bar-highlight" /> ); } From 8e73250f9601ab88b743d4983f3e37194b73782a Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Tue, 17 Sep 2024 15:48:43 -0400 Subject: [PATCH 4/4] Remove .only --- cypress/e2e/functional/tile_tests/bar_graph_tile_spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/functional/tile_tests/bar_graph_tile_spec.js b/cypress/e2e/functional/tile_tests/bar_graph_tile_spec.js index 530b77c36..b9accfff9 100644 --- a/cypress/e2e/functional/tile_tests/bar_graph_tile_spec.js +++ b/cypress/e2e/functional/tile_tests/bar_graph_tile_spec.js @@ -154,7 +154,7 @@ context('Bar Graph Tile', function () { } }); - it('Can link data ', function () { + it('Linking data', function () { beforeTest(); clueCanvas.addTile('bargraph'); @@ -335,7 +335,7 @@ context('Bar Graph Tile', function () { } }); - it.only('Synchronizes selection', function () { + it('Synchronizing selection', function () { beforeTest(); clueCanvas.addTile('bargraph');