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 5938f7526e..c120361061 100644
--- a/cypress/e2e/functional/tile_tests/xy_plot_tool_spec.js
+++ b/cypress/e2e/functional/tile_tests/xy_plot_tool_spec.js
@@ -468,5 +468,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('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/cypress/support/elements/tile/XYPlotToolTile.js b/cypress/support/elements/tile/XYPlotToolTile.js
index f43aed3872..aa97dd1a87 100644
--- a/cypress/support/elements/tile/XYPlotToolTile.js
+++ b/cypress/support/elements/tile/XYPlotToolTile.js
@@ -51,6 +51,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/assets/edit.svg b/src/assets/edit.svg
new file mode 100644
index 0000000000..ece95cfae5
--- /dev/null
+++ b/src/assets/edit.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/clue/app-config.json b/src/clue/app-config.json
index 8360c85a63..170ff46412 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/components/utilities/editable-label-with-button.scss b/src/components/utilities/editable-label-with-button.scss
new file mode 100644
index 0000000000..31e2038138
--- /dev/null
+++ b/src/components/utilities/editable-label-with-button.scss
@@ -0,0 +1,23 @@
+@import "../vars.sass";
+
+.chakra-editable {
+ .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/components/utilities/editable-label-with-button.tsx b/src/components/utilities/editable-label-with-button.tsx
new file mode 100644
index 0000000000..5b107da8b8
--- /dev/null
+++ b/src/components/utilities/editable-label-with-button.tsx
@@ -0,0 +1,40 @@
+import React from "react";
+import { observer } from "mobx-react";
+import { Editable, EditableInput, EditablePreview, useEditableControls } from "@chakra-ui/react";
+
+import EditIcon from "../../assets/edit.svg";
+
+import "./editable-label-with-button.scss";
+
+interface IProps {
+ defaultValue: string|undefined;
+ onSubmit: (value:string) => void;
+}
+
+export const EditableLabelWithButton = observer(function EditableDataSetName({defaultValue, onSubmit}: IProps) {
+
+ function EditButton() {
+ const { isEditing, getEditButtonProps } = useEditableControls();
+ if (!isEditing) {
+ return (
+
+ );
+ } else {
+ return null;
+ }
+ }
+
+ return (
+
+
+
+
+
+ );
+});
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..bac3a7901a 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,34 @@ 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";
+
+ const enabled = isNumeric && !hasEditableLayers;
+
+ function handleClick() {
+ graph.createEditableLayer();
+ }
+
+ return (
+
+
+
+ );
+
+});
+
registerTileToolbarButtons("graph",
[
{
@@ -105,5 +134,9 @@ registerTileToolbarButtons("graph",
{
name: 'toggle-lock',
component: ToggleLockAxesButton
+ },
+ {
+ name: 'add-points-by-hand',
+ component: AddPointsByHandButton
}
]);
diff --git a/src/plugins/graph/components/legend/layer-legend.tsx b/src/plugins/graph/components/legend/layer-legend.tsx
index 5bd7fcfdb9..15f87b1ad0 100644
--- a/src/plugins/graph/components/legend/layer-legend.tsx
+++ b/src/plugins/graph/components/legend/layer-legend.tsx
@@ -12,13 +12,15 @@ 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 { 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";
+import YAxisIcon from "../../assets/y-axis-icon.svg";
export const layerLegendType = "layer-legend";
@@ -46,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();
@@ -72,6 +75,23 @@ const SingleLayerLegend = observer(function SingleLayerLegend(props: ILegendPart
}
}
+
+ const dataSetName = dataConfiguration?.dataset?.name || "Unknown";
+
+ function handleSetDataSetName (value: string) {
+ if (value) {
+ dataConfiguration?.dataset?.setName(value);
+ }
+ }
+
+ const layerName =
+
+ {layer.editable
+ ?
+ : dataSetName
+ }
+ ;
+
if (dataConfiguration) {
const yAttributes = dataConfiguration.yAttributeDescriptions;
@@ -136,7 +156,7 @@ const SingleLayerLegend = observer(function SingleLayerLegend(props: ILegendPart
}
- Data from: {dataConfiguration.dataset.name || "Unknown"}
+ Data from: {layerName}
@@ -170,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 98aad001b7..8d32bf70df 100644
--- a/src/plugins/graph/components/legend/multi-legend.scss
+++ b/src/plugins/graph/components/legend/multi-legend.scss
@@ -25,6 +25,20 @@
.legend-title {
padding-left: 6px;
+
+ .layer-name {
+ font-weight: bold;
+ }
+
+ .chakra-editable {
+ display: inline-block;
+ margin-left: 6px;
+
+ input {
+ font-weight: normal;
+ }
+ }
+
}
}
diff --git a/src/plugins/graph/models/graph-layer-model.ts b/src/plugins/graph/models/graph-layer-model.ts
index 3de1a785c3..37128bb9c3 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 }>,
@@ -90,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..c55b85ab2a 100644
--- a/src/plugins/graph/models/graph-model.test.ts
+++ b/src/plugins/graph/models/graph-model.test.ts
@@ -126,34 +126,50 @@ describe('GraphModel', () => {
expect(graphModel.layers[1].config.dataset).toEqual(sharedDataSet2.dataSet);
});
+ it('supports adding an editable layer', () => {
+ 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(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");
+ 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');
+ 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 => {
diff --git a/src/plugins/graph/models/graph-model.ts b/src/plugins/graph/models/graph-model.ts
index 8bed28d17d..9d0dc5f4e0 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,42 @@ 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);
+ if (doc && smm && smm.isReady) {
+ const datasetName = doc.getUniqueSharedModelName("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;
},
@@ -456,7 +500,7 @@ export const GraphModel = TileContentModel
}
},
setColorForIdWithoutUndo(id: string, colorIndex: number) {
- withoutUndo();
+ withoutUndo({unlessChildAction: true});
self.setColorForId(id, colorIndex);
}
}))
diff --git a/src/plugins/graph/utilities/graph-utils.test.ts b/src/plugins/graph/utilities/graph-utils.test.ts
index d4f41a1535..4952b7fe30 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",
@@ -138,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\\"}"`);
});
});
diff --git a/src/public/demo/units/qa-config-subtabs/content.json b/src/public/demo/units/qa-config-subtabs/content.json
index bc1c48bc2e..5694d5ad91 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"
]
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"
]