From 602da8479a23b199b2829b38730220ce14d67cbb Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Wed, 28 Feb 2024 15:20:59 -0500 Subject: [PATCH 1/9] Basically working version --- src/clue/app-config.json | 1 + src/models/data/data-set.ts | 3 +- .../graph/assets/add-points-by-hand-icon.svg | 6 +++ .../components/graph-toolbar-registration.tsx | 34 +++++++++++++++ src/plugins/graph/models/graph-layer-model.ts | 4 +- src/plugins/graph/models/graph-model.ts | 42 ++++++++++++++++++- .../graph/utilities/graph-utils.test.ts | 1 + src/public/demo/units/qa/content.json | 1 + 8 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 src/plugins/graph/assets/add-points-by-hand-icon.svg diff --git a/src/clue/app-config.json b/src/clue/app-config.json index 95e3211ba3..95ba81332c 100644 --- a/src/clue/app-config.json +++ b/src/clue/app-config.json @@ -269,6 +269,7 @@ "disableAttributeDnD": true, "tools": [ "link-tile", + "add-points-by-hand", "|", "fit-all", "toggle-lock" diff --git a/src/models/data/data-set.ts b/src/models/data/data-set.ts index a69a48e0d6..f504f6fbe8 100644 --- a/src/models/data/data-set.ts +++ b/src/models/data/data-set.ts @@ -639,6 +639,7 @@ export const DataSet = types.model("DataSet", { for (let i = attribute.values.length; i < self.cases.length; ++i) { attribute.values.push(""); } + return attribute; }, setAttributeName(attributeID: string, name: string) { @@ -882,7 +883,7 @@ export function addAttributeToDataSet(dataset: IDataSet, snapshot: IAttributeSna if (!snapshot.id) { snapshot.id = uniqueId(); } - dataset.addAttributeWithID(snapshot, beforeID); + return dataset.addAttributeWithID(snapshot, beforeID); } export function addCasesToDataSet(dataset: IDataSet, cases: ICaseCreation[], beforeID?: string | string[]) { diff --git a/src/plugins/graph/assets/add-points-by-hand-icon.svg b/src/plugins/graph/assets/add-points-by-hand-icon.svg new file mode 100644 index 0000000000..748eb9719e --- /dev/null +++ b/src/plugins/graph/assets/add-points-by-hand-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/plugins/graph/components/graph-toolbar-registration.tsx b/src/plugins/graph/components/graph-toolbar-registration.tsx index a04e5ab9dc..d05274cbb8 100644 --- a/src/plugins/graph/components/graph-toolbar-registration.tsx +++ b/src/plugins/graph/components/graph-toolbar-registration.tsx @@ -13,6 +13,7 @@ import AddIcon from "../../../assets/icons/add-data-graph-icon.svg"; import FitAllIcon from "../assets/fit-all-icon.svg"; import LockAxesIcon from "../assets/lock-axes-icon.svg"; import UnlockAxesIcon from "../assets/unlock-axes-icon.svg"; +import AddPointsByHandIcon from "../assets/add-points-by-hand-icon.svg"; function LinkTileButton(name: string, title: string, allowMultiple: boolean) { @@ -88,6 +89,35 @@ const ToggleLockAxesButton = observer(function ToggleLockAxesButton({name}: IToo ); }); +const AddPointsByHandButton = observer(function AddPointsByHandButton({name}: IToolbarButtonComponentProps) { + const graph = useGraphModelContext(); + + // const hasEditableLayers = graph.getEditableLayers().length > 0; + + // Enable button if axes are numeric or undefined. + const isNumeric = (graph.attributeType("x")||"numeric") === "numeric" + && (graph.attributeType("y")||"numeric") === "numeric"; + if (!isNumeric) { + console.log("Attribute types:", graph.attributeType("x"), graph.attributeType("y")); + } + + function handleClick() { + graph.createEditableLayer(); + } + + return ( + + + + ); + +}); + registerTileToolbarButtons("graph", [ { @@ -105,5 +135,9 @@ registerTileToolbarButtons("graph", { name: 'toggle-lock', component: ToggleLockAxesButton + }, + { + name: 'add-points-by-hand', + component: AddPointsByHandButton } ]); diff --git a/src/plugins/graph/models/graph-layer-model.ts b/src/plugins/graph/models/graph-layer-model.ts index 3de1a785c3..d4180c42a1 100644 --- a/src/plugins/graph/models/graph-layer-model.ts +++ b/src/plugins/graph/models/graph-layer-model.ts @@ -15,7 +15,9 @@ export const GraphLayerModel = types .model('GraphLayerModel') .props({ id: types.optional(types.identifier, () => typedId("LAYR")), - config: types.optional(DataConfigurationModel, () => DataConfigurationModel.create()) + config: types.optional(DataConfigurationModel, () => DataConfigurationModel.create()), + // Whether this layer contains "points by hand" that can be edited in the graph + editable: false }) .volatile(self => ({ autoAssignedAttributes: [] as Array<{ place: GraphPlace, role: GraphAttrRole, dataSetID: string, attrID: string }>, diff --git a/src/plugins/graph/models/graph-model.ts b/src/plugins/graph/models/graph-model.ts index 8bed28d17d..575eefe76b 100644 --- a/src/plugins/graph/models/graph-model.ts +++ b/src/plugins/graph/models/graph-model.ts @@ -32,6 +32,8 @@ import { isSharedDataSet, SharedDataSet } from "../../../models/shared/shared-da import { DataConfigurationModel, RoleAttrIDPair } from "./data-configuration-model"; import { ISharedModelManager } from "../../../models/shared/shared-model-manager"; import { multiLegendParts } from "../components/legend/legend-registration"; +import { addAttributeToDataSet, DataSet } from "../../../models/data/data-set"; +import { getDocumentContentFromNode } from "../../../utilities/mst-utils"; export interface GraphProperties { axes: Record @@ -221,6 +223,12 @@ export const GraphModel = TileContentModel } return undefined; }, + /** + * Return a list of layers that can be edited. + */ + getEditableLayers() { + return self.layers.filter(l => l.editable); + }, /** * Find all tooltip-related attributes from all layers. * Returned as a list of { role, attribute } pairs. @@ -321,6 +329,38 @@ export const GraphModel = TileContentModel initialLayer.configureUnlinkedLayer(); } }, + createEditableLayer() { + const smm = getSharedModelManager(self); + const doc = getDocumentContentFromNode(self); + if (doc && smm && smm.isReady) { + const datasetName = doc.getUniqueDataSetName("Added by hand"); + const + xName = "X Variable", + yName = "Y Variable 1"; + const dataset = DataSet.create({ name: datasetName }); + const xAttr = addAttributeToDataSet(dataset, { name: xName }); + const yAttr = addAttributeToDataSet(dataset, { name: yName }); + const sharedDataSet = SharedDataSet.create({ dataSet: dataset }); + smm.addTileSharedModel(self, sharedDataSet, true); + + const metadata = SharedCaseMetadata.create(); + metadata.setData(dataset); + smm.addTileSharedModel(self, metadata); + + const layer = GraphLayerModel.create({ editable: true }); + self.layers.push(layer); + // Remove default layer if there was one + if (!self.layers[0].isLinked) { + self.layers.splice(0, 1); + } + + const dataConfiguration = DataConfigurationModel.create(); + layer.setDataConfiguration(dataConfiguration); + dataConfiguration.setDataset(dataset, metadata); + dataConfiguration.setAttributeForRole("x", { attributeID: xAttr.id, type: "numeric" }); + dataConfiguration.setAttributeForRole("y", { attributeID: yAttr.id, type: "numeric" }); + } + }, setXAttributeLabel(label: string) { self.xAttributeLabel = label; }, @@ -598,7 +638,7 @@ export const GraphModel = TileContentModel (ids) => { ids.forEach(id => { if (!self._idColors.has(id)) { - self.setColorForIdWithoutUndo(id, self.nextColor); + self.setColorForId(id, self.nextColor); } }); } diff --git a/src/plugins/graph/utilities/graph-utils.test.ts b/src/plugins/graph/utilities/graph-utils.test.ts index d4f41a1535..f8b1d2965a 100644 --- a/src/plugins/graph/utilities/graph-utils.test.ts +++ b/src/plugins/graph/utilities/graph-utils.test.ts @@ -105,6 +105,7 @@ describe("updateGraphContentWithNewSharedModelIds", () => { "lockAxes": false, "plotType": "scatterPlot", "layers": [{ "id": "LAYRLybDWmk6IEI-", + "editable": false, "config": { "id": "DCON3uYgNhsq_4tk", "dataset": "UL53mvolYBJ5hIVr", "metadata": "7U0DJ-WxB83noPMK", "primaryRole": "x", diff --git a/src/public/demo/units/qa/content.json b/src/public/demo/units/qa/content.json index aab3c3d309..975f93ac20 100644 --- a/src/public/demo/units/qa/content.json +++ b/src/public/demo/units/qa/content.json @@ -57,6 +57,7 @@ "connectPointsByDefault": true, "tools": [ "link-tile-multiple", + "add-points-by-hand", "fit-all", "toggle-lock" ] From 33435be078217872c42b3684b3f285a1f397f8b5 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Thu, 29 Feb 2024 12:19:19 -0500 Subject: [PATCH 2/9] Only one editable layer. Update tests --- .../components/graph-toolbar-registration.tsx | 9 ++++----- src/plugins/graph/models/graph-layer-model.ts | 1 + src/plugins/graph/models/graph-model.test.ts | 17 +++++++++++++++++ src/plugins/graph/models/graph-model.ts | 4 ++++ src/plugins/graph/utilities/graph-utils.test.ts | 2 +- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/plugins/graph/components/graph-toolbar-registration.tsx b/src/plugins/graph/components/graph-toolbar-registration.tsx index d05274cbb8..bac3a7901a 100644 --- a/src/plugins/graph/components/graph-toolbar-registration.tsx +++ b/src/plugins/graph/components/graph-toolbar-registration.tsx @@ -92,14 +92,13 @@ const ToggleLockAxesButton = observer(function ToggleLockAxesButton({name}: IToo const AddPointsByHandButton = observer(function AddPointsByHandButton({name}: IToolbarButtonComponentProps) { const graph = useGraphModelContext(); - // const hasEditableLayers = graph.getEditableLayers().length > 0; + const hasEditableLayers = graph.getEditableLayers().length > 0; // Enable button if axes are numeric or undefined. const isNumeric = (graph.attributeType("x")||"numeric") === "numeric" && (graph.attributeType("y")||"numeric") === "numeric"; - if (!isNumeric) { - console.log("Attribute types:", graph.attributeType("x"), graph.attributeType("y")); - } + + const enabled = isNumeric && !hasEditableLayers; function handleClick() { graph.createEditableLayer(); @@ -110,7 +109,7 @@ const AddPointsByHandButton = observer(function AddPointsByHandButton({name}: IT name={name} title="Add points by hand" onClick={handleClick} - disabled={!isNumeric} + disabled={!enabled} > diff --git a/src/plugins/graph/models/graph-layer-model.ts b/src/plugins/graph/models/graph-layer-model.ts index d4180c42a1..37128bb9c3 100644 --- a/src/plugins/graph/models/graph-layer-model.ts +++ b/src/plugins/graph/models/graph-layer-model.ts @@ -92,6 +92,7 @@ export const GraphLayerModel = types configureUnlinkedLayer() { if (!self.config.isEmpty) { self.config.clearAttributes(); + self.editable = false; } }, setDataSetListener() { diff --git a/src/plugins/graph/models/graph-model.test.ts b/src/plugins/graph/models/graph-model.test.ts index 1d1cc4f11f..91c25a0025 100644 --- a/src/plugins/graph/models/graph-model.test.ts +++ b/src/plugins/graph/models/graph-model.test.ts @@ -126,6 +126,23 @@ describe('GraphModel', () => { expect(graphModel.layers[1].config.dataset).toEqual(sharedDataSet2.dataSet); }); + it('supports adding an editable layer', () => { + document = createDefaultDocument(); + const { tileId } = document.content?.addTileContentInNewRow(getSnapshot(GraphModel.create())) || {}; + graphModel = document.content?.getTile(tileId!)?.content as IGraphModel; + + expect(graphModel.layers.length).toBe(1); + expect(graphModel.layers[0].editable).toBe(false); + graphModel.createEditableLayer(); + expect(graphModel.layers.length).toBe(1); + const layer = graphModel.layers[0]; + expect(layer.editable).toBe(true); + expect(layer.config.attributeDescriptions.x.type).toEqual("numeric"); + expect(layer.config.attributeDescriptions.y.type).toEqual("numeric"); + expect(layer.config.dataset?.name).toEqual("Added by hand"); + expect(layer.config.dataset?.attributes.map(a => a.name)).toEqual(["X Variable", "Y Variable 1"]); + }); + it('supports removing layers', () => { if (!graphModel) fail('No graph model'); const smm = getSharedModelManager(graphModel); diff --git a/src/plugins/graph/models/graph-model.ts b/src/plugins/graph/models/graph-model.ts index 575eefe76b..8b318e32a3 100644 --- a/src/plugins/graph/models/graph-model.ts +++ b/src/plugins/graph/models/graph-model.ts @@ -329,6 +329,10 @@ export const GraphModel = TileContentModel initialLayer.configureUnlinkedLayer(); } }, + /** + * Creates an "added by hand" dataset and attaches it as a layer to the graph. + * The layer is marked as editable so that the user can add and edit points. + */ createEditableLayer() { const smm = getSharedModelManager(self); const doc = getDocumentContentFromNode(self); diff --git a/src/plugins/graph/utilities/graph-utils.test.ts b/src/plugins/graph/utilities/graph-utils.test.ts index f8b1d2965a..4952b7fe30 100644 --- a/src/plugins/graph/utilities/graph-utils.test.ts +++ b/src/plugins/graph/utilities/graph-utils.test.ts @@ -139,7 +139,7 @@ describe("updateGraphContentWithNewSharedModelIds", () => { expect(result).not.toContain("CTZ8N5wGpbsgFPDr"); expect(result).not.toContain("t_Yigae_ENpSAaNJ"); // eslint-disable-next-line max-len - expect(result).toMatchInlineSnapshot(`"{\\"type\\":\\"Graph\\",\\"adornments\\":[{\\"id\\":\\"ADRNxHvLKiH_ntmG\\",\\"type\\":\\"Connecting Lines\\",\\"isVisible\\":true}],\\"axes\\":{\\"bottom\\":{\\"type\\":\\"numeric\\",\\"place\\":\\"bottom\\",\\"scale\\":\\"linear\\",\\"min\\":-4.5,\\"max\\":7.5},\\"left\\":{\\"type\\":\\"numeric\\",\\"place\\":\\"left\\",\\"scale\\":\\"linear\\",\\"min\\":-3.5,\\"max\\":8.5}},\\"lockAxes\\":false,\\"plotType\\":\\"scatterPlot\\",\\"layers\\":[{\\"id\\":\\"LAYRLybDWmk6IEI-\\",\\"config\\":{\\"id\\":\\"DCON3uYgNhsq_4tk\\",\\"dataset\\":\\"dset1\\",\\"metadata\\":\\"7U0DJ-WxB83noPMK\\",\\"primaryRole\\":\\"x\\",\\"_attributeDescriptions\\":{\\"x\\":{\\"type\\":\\"numeric\\",\\"attributeID\\":\\"att1\\"}},\\"_yAttributeDescriptions\\":[{\\"type\\":\\"numeric\\",\\"attributeID\\":\\"att2\\"}]}}],\\"_idColors\\":{\\"att2\\":0},\\"_pointColors\\":[\\"#E6805B\\"],\\"_pointStrokeColor\\":\\"#FFFFFF\\",\\"pointStrokeSameAsFill\\":false,\\"pointSizeMultiplier\\":1,\\"plotBackgroundColor\\":\\"#FFFFFF\\",\\"isTransparent\\":false,\\"plotBackgroundImageID\\":\\"\\",\\"showParentToggles\\":false,\\"showMeasuresForSelection\\":false,\\"xAttributeLabel\\":\\"time\\",\\"yAttributeLabel\\":\\"Signal\\"}"`); + expect(result).toMatchInlineSnapshot(`"{\\"type\\":\\"Graph\\",\\"adornments\\":[{\\"id\\":\\"ADRNxHvLKiH_ntmG\\",\\"type\\":\\"Connecting Lines\\",\\"isVisible\\":true}],\\"axes\\":{\\"bottom\\":{\\"type\\":\\"numeric\\",\\"place\\":\\"bottom\\",\\"scale\\":\\"linear\\",\\"min\\":-4.5,\\"max\\":7.5},\\"left\\":{\\"type\\":\\"numeric\\",\\"place\\":\\"left\\",\\"scale\\":\\"linear\\",\\"min\\":-3.5,\\"max\\":8.5}},\\"lockAxes\\":false,\\"plotType\\":\\"scatterPlot\\",\\"layers\\":[{\\"id\\":\\"LAYRLybDWmk6IEI-\\",\\"editable\\":false,\\"config\\":{\\"id\\":\\"DCON3uYgNhsq_4tk\\",\\"dataset\\":\\"dset1\\",\\"metadata\\":\\"7U0DJ-WxB83noPMK\\",\\"primaryRole\\":\\"x\\",\\"_attributeDescriptions\\":{\\"x\\":{\\"type\\":\\"numeric\\",\\"attributeID\\":\\"att1\\"}},\\"_yAttributeDescriptions\\":[{\\"type\\":\\"numeric\\",\\"attributeID\\":\\"att2\\"}]}}],\\"_idColors\\":{\\"att2\\":0},\\"_pointColors\\":[\\"#E6805B\\"],\\"_pointStrokeColor\\":\\"#FFFFFF\\",\\"pointStrokeSameAsFill\\":false,\\"pointSizeMultiplier\\":1,\\"plotBackgroundColor\\":\\"#FFFFFF\\",\\"isTransparent\\":false,\\"plotBackgroundImageID\\":\\"\\",\\"showParentToggles\\":false,\\"showMeasuresForSelection\\":false,\\"xAttributeLabel\\":\\"time\\",\\"yAttributeLabel\\":\\"Signal\\"}"`); }); }); From 669be0136f28e2a5f3522680d30a8724d8651339 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Thu, 29 Feb 2024 13:06:18 -0500 Subject: [PATCH 3/9] Update test --- src/plugins/graph/models/graph-model.test.ts | 29 ++++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/plugins/graph/models/graph-model.test.ts b/src/plugins/graph/models/graph-model.test.ts index 91c25a0025..c55b85ab2a 100644 --- a/src/plugins/graph/models/graph-model.test.ts +++ b/src/plugins/graph/models/graph-model.test.ts @@ -127,15 +127,12 @@ describe('GraphModel', () => { }); it('supports adding an editable layer', () => { - document = createDefaultDocument(); - const { tileId } = document.content?.addTileContentInNewRow(getSnapshot(GraphModel.create())) || {}; - graphModel = document.content?.getTile(tileId!)?.content as IGraphModel; - - expect(graphModel.layers.length).toBe(1); + if (!graphModel) fail('No graph model'); // reuses data from previous test + expect(graphModel.layers.length).toBe(2); expect(graphModel.layers[0].editable).toBe(false); graphModel.createEditableLayer(); - expect(graphModel.layers.length).toBe(1); - const layer = graphModel.layers[0]; + expect(graphModel.layers.length).toBe(3); + const layer = graphModel.layers[2]; expect(layer.editable).toBe(true); expect(layer.config.attributeDescriptions.x.type).toEqual("numeric"); expect(layer.config.attributeDescriptions.y.type).toEqual("numeric"); @@ -144,33 +141,35 @@ describe('GraphModel', () => { }); it('supports removing layers', () => { - if (!graphModel) fail('No graph model'); + if (!graphModel) fail('No graph model'); // reuses data from previous test const smm = getSharedModelManager(graphModel); smm?.removeTileSharedModel(graphModel, sharedDataSet2); graphModel.updateAfterSharedModelChanges(sharedDataSet); // Currently Metadata remains attached - doesn't seem like correct behavior longer term though - expect(getTileSharedModels(graphModel)).toHaveLength(3); - expect(graphModel.layers.length).toBe(1); + expect(getTileSharedModels(graphModel)).toHaveLength(5); + expect(graphModel.layers.length).toBe(2); expect(graphModel.layers[0].isLinked).toBe(true); expect(graphModel.layers[0].config.dataset).toEqual(sharedDataSet.dataSet); }); it("re-uses existing metadata if present", () => { - if (!graphModel) fail('No graph model'); + if (!graphModel) fail('No graph model'); // reuses data from previous test const smm = getSharedModelManager(graphModel); smm?.addSharedModel(sharedDataSet2); smm?.addTileSharedModel(graphModel, sharedDataSet2); graphModel.updateAfterSharedModelChanges(sharedDataSet2); - expect(getTileSharedModels(graphModel)).toHaveLength(4); - expect(graphModel.layers.length).toBe(2); + expect(getTileSharedModels(graphModel)).toHaveLength(6); + expect(graphModel.layers.length).toBe(3); expect(graphModel.layers[0].isLinked).toBe(true); expect(graphModel.layers[0].config.dataset).toEqual(sharedDataSet.dataSet); expect(graphModel.layers[1].isLinked).toBe(true); - expect(graphModel.layers[1].config.dataset).toEqual(sharedDataSet2.dataSet); + expect(graphModel.layers[1].editable).toEqual(true); + expect(graphModel.layers[2].isLinked).toBe(true); + expect(graphModel.layers[2].config.dataset).toEqual(sharedDataSet2.dataSet); }); it("cycles through colors properly", () => { - if (!graphModel) fail("No graph model"); + if (!graphModel) fail("No graph model"); // reuses data from previous test function getUniqueColorIndices() { const uniqueColorIndices: number[] = []; graphModel._idColors.forEach(colorIndex => { From 2d47972b16c47ac1b4766885b78aa2338a04d85d Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Fri, 1 Mar 2024 10:52:48 -0500 Subject: [PATCH 4/9] Editable dataset name --- .../graph/assets/edit-legend-name-icon.svg | 3 ++ .../legend/editable-dataset-name.tsx | 44 +++++++++++++++++++ .../graph/components/legend/layer-legend.tsx | 10 +++-- .../graph/components/legend/multi-legend.scss | 29 ++++++++++++ 4 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 src/plugins/graph/assets/edit-legend-name-icon.svg create mode 100644 src/plugins/graph/components/legend/editable-dataset-name.tsx diff --git a/src/plugins/graph/assets/edit-legend-name-icon.svg b/src/plugins/graph/assets/edit-legend-name-icon.svg new file mode 100644 index 0000000000..ece95cfae5 --- /dev/null +++ b/src/plugins/graph/assets/edit-legend-name-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/plugins/graph/components/legend/editable-dataset-name.tsx b/src/plugins/graph/components/legend/editable-dataset-name.tsx new file mode 100644 index 0000000000..a040bc4210 --- /dev/null +++ b/src/plugins/graph/components/legend/editable-dataset-name.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { Editable, EditableInput, EditablePreview, useEditableControls } from "@chakra-ui/react"; +import { observer } from "mobx-react"; +import { IDataConfigurationModel } from "../../models/data-configuration-model"; + +import EditIcon from "../../assets/edit-legend-name-icon.svg"; + +interface IProps { + dataConfiguration: IDataConfigurationModel; +} + +export const EditableDataSetName = observer(function EditableDataSetName({dataConfiguration}: IProps) { + + function EditButton() { + const { isEditing, getEditButtonProps } = useEditableControls(); + if (!isEditing) { + return ( + + ); + } else { + return null; + } + } + + function handleOnSubmit(val: string) { + if (val) { + dataConfiguration.dataset?.setName(val); + } + } + + return ( + + + + + + ); +}); diff --git a/src/plugins/graph/components/legend/layer-legend.tsx b/src/plugins/graph/components/legend/layer-legend.tsx index 5bd7fcfdb9..6ef48a2dfc 100644 --- a/src/plugins/graph/components/legend/layer-legend.tsx +++ b/src/plugins/graph/components/legend/layer-legend.tsx @@ -12,13 +12,14 @@ import { DataConfigurationContext, useDataConfigurationContext } from "../../hoo import { IGraphLayerModel } from "../../models/graph-layer-model"; import { LegendDropdown } from "./legend-dropdown"; import { LegendIdListFunction, ILegendHeightFunctionProps, ILegendPartProps } from "./legend-types"; -import RemoveDataIcon from "../../assets/remove-data-icon.svg"; -import XAxisIcon from "../../assets/x-axis-icon.svg"; -import YAxisIcon from "../../assets/y-axis-icon.svg"; import { logSharedModelDocEvent } from "../../../../models/document/log-shared-model-document-event"; import { LogEventName } from "../../../../lib/logger-types"; import { useTileModelContext } from "../../../../components/tiles/hooks/use-tile-model-context"; +import { EditableDataSetName } from "./editable-dataset-name"; +import RemoveDataIcon from "../../assets/remove-data-icon.svg"; +import XAxisIcon from "../../assets/x-axis-icon.svg"; +import YAxisIcon from "../../assets/y-axis-icon.svg"; export const layerLegendType = "layer-legend"; @@ -136,7 +137,8 @@ const SingleLayerLegend = observer(function SingleLayerLegend(props: ILegendPart }
- Data from: {dataConfiguration.dataset.name || "Unknown"}  + Data from: +
diff --git a/src/plugins/graph/components/legend/multi-legend.scss b/src/plugins/graph/components/legend/multi-legend.scss index 98aad001b7..41f12fba94 100644 --- a/src/plugins/graph/components/legend/multi-legend.scss +++ b/src/plugins/graph/components/legend/multi-legend.scss @@ -25,6 +25,35 @@ .legend-title { padding-left: 6px; + + .chakra-editable { + display: inline-block; + margin-left: 6px; + } + + .chakra-editable__preview { + font-weight: bold; + } + + .chakra-editable__preview[hidden] { + display: none; + } + + button { + background: inherit; + border: none; + cursor: pointer; + + &:hover { + background-color: $workspace-teal-light-6; + } + + svg { + height: 14px; + width: 14px; + vertical-align: baseline; + } + } } } From 67414bef80f508daf627c095777c83cee9a8ca01 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Fri, 1 Mar 2024 14:10:00 -0500 Subject: [PATCH 5/9] Generalize editor component --- ...name.tsx => editable-label-with-button.tsx} | 18 ++++++------------ .../graph/components/legend/layer-legend.tsx | 13 +++++++++++-- 2 files changed, 17 insertions(+), 14 deletions(-) rename src/plugins/graph/components/legend/{editable-dataset-name.tsx => editable-label-with-button.tsx} (61%) diff --git a/src/plugins/graph/components/legend/editable-dataset-name.tsx b/src/plugins/graph/components/legend/editable-label-with-button.tsx similarity index 61% rename from src/plugins/graph/components/legend/editable-dataset-name.tsx rename to src/plugins/graph/components/legend/editable-label-with-button.tsx index a040bc4210..62c99f1d6e 100644 --- a/src/plugins/graph/components/legend/editable-dataset-name.tsx +++ b/src/plugins/graph/components/legend/editable-label-with-button.tsx @@ -1,15 +1,15 @@ import React from "react"; -import { Editable, EditableInput, EditablePreview, useEditableControls } from "@chakra-ui/react"; import { observer } from "mobx-react"; -import { IDataConfigurationModel } from "../../models/data-configuration-model"; +import { Editable, EditableInput, EditablePreview, useEditableControls } from "@chakra-ui/react"; import EditIcon from "../../assets/edit-legend-name-icon.svg"; interface IProps { - dataConfiguration: IDataConfigurationModel; + defaultValue: string|undefined; + onSubmit: (value:string) => void; } -export const EditableDataSetName = observer(function EditableDataSetName({dataConfiguration}: IProps) { +export const EditableLabelWithButton = observer(function EditableDataSetName({defaultValue, onSubmit}: IProps) { function EditButton() { const { isEditing, getEditButtonProps } = useEditableControls(); @@ -24,17 +24,11 @@ export const EditableDataSetName = observer(function EditableDataSetName({dataCo } } - function handleOnSubmit(val: string) { - if (val) { - dataConfiguration.dataset?.setName(val); - } - } - return ( diff --git a/src/plugins/graph/components/legend/layer-legend.tsx b/src/plugins/graph/components/legend/layer-legend.tsx index 6ef48a2dfc..880c1cc8f3 100644 --- a/src/plugins/graph/components/legend/layer-legend.tsx +++ b/src/plugins/graph/components/legend/layer-legend.tsx @@ -15,7 +15,7 @@ import { LegendIdListFunction, ILegendHeightFunctionProps, ILegendPartProps } fr import { logSharedModelDocEvent } from "../../../../models/document/log-shared-model-document-event"; import { LogEventName } from "../../../../lib/logger-types"; import { useTileModelContext } from "../../../../components/tiles/hooks/use-tile-model-context"; -import { EditableDataSetName } from "./editable-dataset-name"; +import { EditableLabelWithButton } from "./editable-label-with-button"; import RemoveDataIcon from "../../assets/remove-data-icon.svg"; import XAxisIcon from "../../assets/x-axis-icon.svg"; @@ -73,6 +73,15 @@ const SingleLayerLegend = observer(function SingleLayerLegend(props: ILegendPart } } + + const dataSetName = dataConfiguration?.dataset?.name || "Unknown"; + + function handleSetDataSetName (value: string) { + if (value) { + dataConfiguration?.dataset?.setName(value); + } + } + if (dataConfiguration) { const yAttributes = dataConfiguration.yAttributeDescriptions; @@ -138,7 +147,7 @@ const SingleLayerLegend = observer(function SingleLayerLegend(props: ILegendPart }
Data from: - +
From 19571e761f68f322c473987803e91e4d484f68d9 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Fri, 1 Mar 2024 15:00:39 -0500 Subject: [PATCH 6/9] Cypress test --- .../tile_tests/xy_plot_tool_spec.js | 23 +++++++++++++++++++ .../support/elements/tile/XYPlotToolTile.js | 9 ++++++++ .../demo/units/qa-config-subtabs/content.json | 1 + 3 files changed, 33 insertions(+) diff --git a/cypress/e2e/functional/tile_tests/xy_plot_tool_spec.js b/cypress/e2e/functional/tile_tests/xy_plot_tool_spec.js index 7c84ca4ccb..341903b6c7 100644 --- a/cypress/e2e/functional/tile_tests/xy_plot_tool_spec.js +++ b/cypress/e2e/functional/tile_tests/xy_plot_tool_spec.js @@ -432,5 +432,28 @@ context('XYPlot Tool Tile', function () { // Only the unlink remove button should remain xyTile.getRemoveVariablesButtons().should("have.length", 1); }); + + it("Test points by hand", () => { + beforeTest(queryParamsMultiDataset); + cy.log("Add XY Plot Tile"); + cy.collapseResourceTabs(); + clueCanvas.addTile("graph"); + xyTile.getTile().should('be.visible'); + + clueCanvas.clickToolbarButton("graph", "add-points-by-hand"); + xyTile.getXAttributesLabel().should('have.length', 1).should("contain.text", "X Variable"); + xyTile.getYAttributesLabel().should('have.length', 1).should("contain.text", "Y Variable 1"); + xyTile.getLayerName().should('have.length', 1).should("contain.text", "Added by hand"); + xyTile.getLayerNameInput().should('not.be.visible'); + + xyTile.getLayerNameEditButton().click(); + xyTile.getLayerNameEditButton().should('not.be.visible'); + xyTile.getLayerNameInput().should('be.visible').type('Renamed{enter}'); + xyTile.getLayerNameInput().should('not.be.visible'); + xyTile.getLayerName().should('have.length', 1).should("contain.text", "Renamed"); + }); + }); + + }); diff --git a/cypress/support/elements/tile/XYPlotToolTile.js b/cypress/support/elements/tile/XYPlotToolTile.js index 7f7f6f5083..b9b78e88c8 100644 --- a/cypress/support/elements/tile/XYPlotToolTile.js +++ b/cypress/support/elements/tile/XYPlotToolTile.js @@ -47,6 +47,15 @@ class XYPlotToolTile { getYAxisInput(workspaceClass) { return this.getAxisInput("left", workspaceClass); } + getLayerName(workspaceClass) { + return cy.get(`${wsClass(workspaceClass)} .canvas-area .multi-legend .legend-row .layer-name`); + } + getLayerNameEditButton(workspaceClass) { + return cy.get(`${wsClass(workspaceClass)} .canvas-area .multi-legend .legend-row .layer-name button`); + } + getLayerNameInput(workspaceClass) { + return cy.get(`${wsClass(workspaceClass)} .canvas-area .multi-legend .legend-row .layer-name input`); + } getXAttributesLabel(workspaceClass) { return cy.get(`${wsClass(workspaceClass)} .canvas-area .multi-legend .legend-row .bottom .simple-attribute-label`); } diff --git a/src/public/demo/units/qa-config-subtabs/content.json b/src/public/demo/units/qa-config-subtabs/content.json index c317aaf2f2..9b5bb048a0 100644 --- a/src/public/demo/units/qa-config-subtabs/content.json +++ b/src/public/demo/units/qa-config-subtabs/content.json @@ -38,6 +38,7 @@ "connectPointsByDefault": true, "tools": [ "link-tile-multiple", + "add-points-by-hand", "fit-all", "toggle-lock" ] From eb62ed5577b8c28f1b344b9e097c97e5242edfb4 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Fri, 1 Mar 2024 15:14:05 -0500 Subject: [PATCH 7/9] Generalize editor component; cleanups --- .../tile_tests/xy_plot_tool_spec.js | 2 +- .../edit.svg} | 0 .../utilities/editable-label-with-button.scss | 22 ++++++++++++++ .../utilities}/editable-label-with-button.tsx | 4 ++- .../graph/components/legend/layer-legend.tsx | 27 ++++++++++++----- .../graph/components/legend/multi-legend.scss | 29 +++++-------------- src/plugins/graph/models/graph-model.ts | 2 +- 7 files changed, 53 insertions(+), 33 deletions(-) rename src/{plugins/graph/assets/edit-legend-name-icon.svg => assets/edit.svg} (100%) create mode 100644 src/components/utilities/editable-label-with-button.scss rename src/{plugins/graph/components/legend => components/utilities}/editable-label-with-button.tsx (90%) diff --git a/cypress/e2e/functional/tile_tests/xy_plot_tool_spec.js b/cypress/e2e/functional/tile_tests/xy_plot_tool_spec.js index 341903b6c7..e35a2a2edf 100644 --- a/cypress/e2e/functional/tile_tests/xy_plot_tool_spec.js +++ b/cypress/e2e/functional/tile_tests/xy_plot_tool_spec.js @@ -447,7 +447,7 @@ context('XYPlot Tool Tile', function () { xyTile.getLayerNameInput().should('not.be.visible'); xyTile.getLayerNameEditButton().click(); - xyTile.getLayerNameEditButton().should('not.be.visible'); + xyTile.getLayerNameEditButton().should('have.length', 0); xyTile.getLayerNameInput().should('be.visible').type('Renamed{enter}'); xyTile.getLayerNameInput().should('not.be.visible'); xyTile.getLayerName().should('have.length', 1).should("contain.text", "Renamed"); diff --git a/src/plugins/graph/assets/edit-legend-name-icon.svg b/src/assets/edit.svg similarity index 100% rename from src/plugins/graph/assets/edit-legend-name-icon.svg rename to src/assets/edit.svg diff --git a/src/components/utilities/editable-label-with-button.scss b/src/components/utilities/editable-label-with-button.scss new file mode 100644 index 0000000000..879c30d44d --- /dev/null +++ b/src/components/utilities/editable-label-with-button.scss @@ -0,0 +1,22 @@ +@import "../vars.sass"; + + +.chakra-editable__preview[hidden] { + display: none; +} + +button { + background: inherit; + border: none; + cursor: pointer; + + &:hover { + background-color: $workspace-teal-light-6; + } + + svg { + height: 1em; + width: 1em; + vertical-align: baseline; + } +} diff --git a/src/plugins/graph/components/legend/editable-label-with-button.tsx b/src/components/utilities/editable-label-with-button.tsx similarity index 90% rename from src/plugins/graph/components/legend/editable-label-with-button.tsx rename to src/components/utilities/editable-label-with-button.tsx index 62c99f1d6e..5b107da8b8 100644 --- a/src/plugins/graph/components/legend/editable-label-with-button.tsx +++ b/src/components/utilities/editable-label-with-button.tsx @@ -2,7 +2,9 @@ import React from "react"; import { observer } from "mobx-react"; import { Editable, EditableInput, EditablePreview, useEditableControls } from "@chakra-ui/react"; -import EditIcon from "../../assets/edit-legend-name-icon.svg"; +import EditIcon from "../../assets/edit.svg"; + +import "./editable-label-with-button.scss"; interface IProps { defaultValue: string|undefined; diff --git a/src/plugins/graph/components/legend/layer-legend.tsx b/src/plugins/graph/components/legend/layer-legend.tsx index 880c1cc8f3..15f87b1ad0 100644 --- a/src/plugins/graph/components/legend/layer-legend.tsx +++ b/src/plugins/graph/components/legend/layer-legend.tsx @@ -15,7 +15,8 @@ import { LegendIdListFunction, ILegendHeightFunctionProps, ILegendPartProps } fr import { logSharedModelDocEvent } from "../../../../models/document/log-shared-model-document-event"; import { LogEventName } from "../../../../lib/logger-types"; import { useTileModelContext } from "../../../../components/tiles/hooks/use-tile-model-context"; -import { EditableLabelWithButton } from "./editable-label-with-button"; +import { EditableLabelWithButton } from "../../../../components/utilities/editable-label-with-button"; +import { GraphLayerContext, useGraphLayerContext } from "../../hooks/use-graph-layer-context"; import RemoveDataIcon from "../../assets/remove-data-icon.svg"; import XAxisIcon from "../../assets/x-axis-icon.svg"; @@ -47,6 +48,7 @@ function ColorKey({ color }: IColorKeyProps) { const SingleLayerLegend = observer(function SingleLayerLegend(props: ILegendPartProps) { let legendItems = [] as React.ReactNode[]; const graphModel = useGraphModelContext(); + const layer = useGraphLayerContext(); const dataConfiguration = useDataConfigurationContext(); const readOnly = useReadOnlyContext(); const { tile } = useTileModelContext(); @@ -82,6 +84,14 @@ const SingleLayerLegend = observer(function SingleLayerLegend(props: ILegendPart } } + const layerName = + + {layer.editable + ? + : dataSetName + } + ; + if (dataConfiguration) { const yAttributes = dataConfiguration.yAttributeDescriptions; @@ -146,8 +156,7 @@ const SingleLayerLegend = observer(function SingleLayerLegend(props: ILegendPart
}
- Data from: - + Data from: {layerName}
@@ -181,11 +190,13 @@ export const LayerLegend = observer(function LayerLegend(props: ILegendPartProps { graphModel.layers.map((layer) => { return ( - - - ); - } - ) + + + + + + ); + }) } ); diff --git a/src/plugins/graph/components/legend/multi-legend.scss b/src/plugins/graph/components/legend/multi-legend.scss index 41f12fba94..8d32bf70df 100644 --- a/src/plugins/graph/components/legend/multi-legend.scss +++ b/src/plugins/graph/components/legend/multi-legend.scss @@ -26,34 +26,19 @@ .legend-title { padding-left: 6px; - .chakra-editable { - display: inline-block; - margin-left: 6px; - } - - .chakra-editable__preview { + .layer-name { font-weight: bold; } - .chakra-editable__preview[hidden] { - display: none; - } - - button { - background: inherit; - border: none; - cursor: pointer; - - &:hover { - background-color: $workspace-teal-light-6; - } + .chakra-editable { + display: inline-block; + margin-left: 6px; - svg { - height: 14px; - width: 14px; - vertical-align: baseline; + input { + font-weight: normal; } } + } } diff --git a/src/plugins/graph/models/graph-model.ts b/src/plugins/graph/models/graph-model.ts index 8b318e32a3..d465e3846c 100644 --- a/src/plugins/graph/models/graph-model.ts +++ b/src/plugins/graph/models/graph-model.ts @@ -337,7 +337,7 @@ export const GraphModel = TileContentModel const smm = getSharedModelManager(self); const doc = getDocumentContentFromNode(self); if (doc && smm && smm.isReady) { - const datasetName = doc.getUniqueDataSetName("Added by hand"); + const datasetName = doc.getUniqueSharedModelName("Added by hand"); const xName = "X Variable", yName = "Y Variable 1"; From 31f2232f1772d313f84d510ef5d7b36a5eedd07b Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Fri, 1 Mar 2024 15:17:07 -0500 Subject: [PATCH 8/9] Fix CSS --- .../utilities/editable-label-with-button.scss | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/components/utilities/editable-label-with-button.scss b/src/components/utilities/editable-label-with-button.scss index 879c30d44d..31e2038138 100644 --- a/src/components/utilities/editable-label-with-button.scss +++ b/src/components/utilities/editable-label-with-button.scss @@ -1,22 +1,23 @@ @import "../vars.sass"; +.chakra-editable { + .chakra-editable__preview[hidden] { + display: none; + } -.chakra-editable__preview[hidden] { - display: none; -} - -button { - background: inherit; - border: none; - cursor: pointer; + button { + background: inherit; + border: none; + cursor: pointer; - &:hover { - background-color: $workspace-teal-light-6; - } + &:hover { + background-color: $workspace-teal-light-6; + } - svg { - height: 1em; - width: 1em; - vertical-align: baseline; + svg { + height: 1em; + width: 1em; + vertical-align: baseline; + } } } From 31af0a92c7b63163a3ca5eaef7da8d2d5983bb66 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Tue, 5 Mar 2024 13:15:48 -0500 Subject: [PATCH 9/9] withoutUndo tweak --- src/plugins/graph/models/graph-model.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/graph/models/graph-model.ts b/src/plugins/graph/models/graph-model.ts index d465e3846c..9d0dc5f4e0 100644 --- a/src/plugins/graph/models/graph-model.ts +++ b/src/plugins/graph/models/graph-model.ts @@ -500,7 +500,7 @@ export const GraphModel = TileContentModel } }, setColorForIdWithoutUndo(id: string, colorIndex: number) { - withoutUndo(); + withoutUndo({unlessChildAction: true}); self.setColorForId(id, colorIndex); } })) @@ -642,7 +642,7 @@ export const GraphModel = TileContentModel (ids) => { ids.forEach(id => { if (!self._idColors.has(id)) { - self.setColorForId(id, self.nextColor); + self.setColorForIdWithoutUndo(id, self.nextColor); } }); }