From 5217997c2e7bfbc5e0a0fc74f5f6763c366c792b Mon Sep 17 00:00:00 2001 From: Abigail Alexander Date: Wed, 24 Jan 2024 11:39:49 +0000 Subject: [PATCH 1/7] Use svg polyline for Line widget --- src/ui/widgets/Line/line.tsx | 58 +++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/src/ui/widgets/Line/line.tsx b/src/ui/widgets/Line/line.tsx index 82af3c4..efd282c 100644 --- a/src/ui/widgets/Line/line.tsx +++ b/src/ui/widgets/Line/line.tsx @@ -6,19 +6,22 @@ import { FloatPropOpt, ColorPropOpt, BoolPropOpt, - FloatProp + FloatProp, + PointsPropOpt } from "../propTypes"; import { registerWidget } from "../register"; -import { ShapeComponent } from "../Shape/shape"; import { Color } from "../../../types/color"; +import { Point } from "../../../types/points"; const LineProps = { width: FloatProp, + height: FloatProp, lineWidth: FloatPropOpt, backgroundColor: ColorPropOpt, visible: BoolPropOpt, transparent: BoolPropOpt, - rotationAngle: FloatPropOpt + rotationAngle: FloatPropOpt, + points: PointsPropOpt }; export type LineComponentProps = InferWidgetProps & @@ -31,27 +34,40 @@ export const LineComponent = (props: LineComponentProps): JSX.Element => { backgroundColor, rotationAngle = 0, width, - lineWidth = 1 + height, + lineWidth = 1, + points } = props; - const styleProps = { - backgroundColor: transparent ? Color.TRANSPARENT : backgroundColor, - visible - }; + const transform = `rotation(${rotationAngle},0,0)`; - const shapeProps = { - shapeWidth: `${width}px`, - shapeHeight: `${lineWidth}px` - }; - - const transform = `rotate(${rotationAngle}deg)`; - - return ( - - ); + let coordinates = ""; + if (points !== undefined && visible) { + points.values.forEach((point: Point) => { + coordinates += `${point.x},${point.y} `; + }); + return ( + + + + ); + } else { + return <>; + } }; const LineWidgetProps = { From 56f9de11820d56145b6b9e61546d0642e79a6cb8 Mon Sep 17 00:00:00 2001 From: Abigail Alexander Date: Wed, 24 Jan 2024 13:44:11 +0000 Subject: [PATCH 2/7] Update line widget to use svg polyline --- .../Line/__snapshots__/line.test.tsx.snap | 16 +++- src/ui/widgets/Line/line.test.tsx | 82 ++++++++++++++----- 2 files changed, 73 insertions(+), 25 deletions(-) diff --git a/src/ui/widgets/Line/__snapshots__/line.test.tsx.snap b/src/ui/widgets/Line/__snapshots__/line.test.tsx.snap index c7a9aa9..ef5e825 100644 --- a/src/ui/widgets/Line/__snapshots__/line.test.tsx.snap +++ b/src/ui/widgets/Line/__snapshots__/line.test.tsx.snap @@ -2,8 +2,18 @@ exports[` matches snapshot 1`] = ` -
+ + + `; diff --git a/src/ui/widgets/Line/line.test.tsx b/src/ui/widgets/Line/line.test.tsx index b7dd56d..3582768 100644 --- a/src/ui/widgets/Line/line.test.tsx +++ b/src/ui/widgets/Line/line.test.tsx @@ -1,12 +1,13 @@ import React from "react"; -import renderer, { ReactTestRenderer } from "react-test-renderer"; +import renderer, { ReactTestRendererJSON } from "react-test-renderer"; import { render } from "@testing-library/react"; import { LineComponent } from "./line"; -import { ShapeComponent } from "../Shape/shape"; import { Color } from "../../../types/color"; -const LineRenderer = (lineProps: any): ReactTestRenderer => { - return renderer.create(); +const LineRenderer = (lineProps: any): ReactTestRendererJSON => { + return renderer + .create() + .toJSON() as ReactTestRendererJSON; }; describe("", (): void => { @@ -15,9 +16,17 @@ describe("", (): void => { ); @@ -28,36 +37,65 @@ describe("", (): void => { test("default properties are added to line component", (): void => { const lineProps = { width: 20, - lineWidth: 4, - backgroundColor: Color.fromRgba(0, 255, 255) + height: 25, + backgroundColor: Color.fromRgba(0, 255, 255), + points: { + values: [ + { x: 1, y: 10 }, + { x: 15, y: 20 }, + { x: 4, y: 15 } + ] + } }; - const testRenderer = LineRenderer(lineProps); + const svg = LineRenderer(lineProps); + expect(svg.props.viewBox).toEqual("0 0 20 25"); - const shapeProps = testRenderer.root.findByType(ShapeComponent).props; + const lines = svg.children as Array; - expect(shapeProps.shapeWidth).toBe("20px"); - expect(shapeProps.shapeHeight).toBe("4px"); - expect(shapeProps.backgroundColor.text).toEqual("rgba(0,255,255,255)"); - expect(shapeProps.visible).toBe(true); + expect(lines[0].props.stroke).toEqual("rgba(0,255,255,255)"); + expect(lines[0].props.strokeWidth).toEqual(1); + expect(lines[0].props.transform).toEqual("rotation(0,0,0)"); + expect(lines[0].props.points).toEqual("1,10 15,20 4,15 "); }); test("props override default properties", (): void => { const lineProps = { - width: 15, + width: 30, + height: 20, lineWidth: 15, - backgroundColor: Color.fromRgba(0, 255, 255), + backgroundColor: Color.fromRgba(0, 254, 250), transparent: true, rotationAngle: 45, - visible: false + visible: true, + points: { + values: [ + { x: 16, y: 4 }, + { x: 25, y: 10 }, + { x: 4, y: 15 } + ] + } }; - const testRenderer = LineRenderer(lineProps); + const svg = LineRenderer(lineProps); + expect(svg.props.viewBox).toEqual("0 0 30 20"); + + const lines = svg.children as Array; + + expect(lines[0].props.stroke).toEqual("rgba(0,0,0,0)"); + expect(lines[0].props.strokeWidth).toEqual(15); + expect(lines[0].props.transform).toEqual("rotation(45,0,0)"); + expect(lines[0].props.points).toEqual("16,4 25,10 4,15 "); + }); - const shapeProps = testRenderer.root.findByType(ShapeComponent).props; + test("line component not created if no points to plot", (): void => { + const lineProps = { + width: 20, + height: 25, + backgroundColor: Color.fromRgba(0, 255, 255) + }; - expect(shapeProps.backgroundColor.text).toBe(Color.TRANSPARENT.toString()); - expect(shapeProps.shapeTransform).toBe("rotate(45deg)"); - expect(shapeProps.visible).toBe(false); + const svg = LineRenderer(lineProps); + expect(svg).toBeNull(); }); }); From 90d1aec0b651cc82eacef1f617a308cab775c389 Mon Sep 17 00:00:00 2001 From: Abigail Alexander Date: Thu, 25 Jan 2024 15:44:06 +0000 Subject: [PATCH 3/7] Add arrowheads to Line widget, update tests --- src/ui/widgets/EmbeddedDisplay/opiParser.ts | 4 +- .../Line/__snapshots__/line.test.tsx.snap | 1 + src/ui/widgets/Line/line.test.tsx | 22 +++++-- src/ui/widgets/Line/line.tsx | 62 +++++++++++++++++-- 4 files changed, 79 insertions(+), 10 deletions(-) diff --git a/src/ui/widgets/EmbeddedDisplay/opiParser.ts b/src/ui/widgets/EmbeddedDisplay/opiParser.ts index 515165b..8550d9c 100644 --- a/src/ui/widgets/EmbeddedDisplay/opiParser.ts +++ b/src/ui/widgets/EmbeddedDisplay/opiParser.ts @@ -658,7 +658,9 @@ export const OPI_SIMPLE_PARSERS: ParserDict = { effect3d: ["effect_3d", opiParseBoolean], showLed: ["show_led", opiParseBoolean], cornerWidth: ["corner_width", opiParseString], - cornerHeight: ["corner_height", opiParseString] + cornerHeight: ["corner_height", opiParseString], + arrows: ["arrows", opiParseNumber], + arrowLength: ["arrow_length", opiParseNumber] }; /** diff --git a/src/ui/widgets/Line/__snapshots__/line.test.tsx.snap b/src/ui/widgets/Line/__snapshots__/line.test.tsx.snap index ef5e825..f62c093 100644 --- a/src/ui/widgets/Line/__snapshots__/line.test.tsx.snap +++ b/src/ui/widgets/Line/__snapshots__/line.test.tsx.snap @@ -3,6 +3,7 @@ exports[` matches snapshot 1`] = ` diff --git a/src/ui/widgets/Line/line.test.tsx b/src/ui/widgets/Line/line.test.tsx index 3582768..4f03a3a 100644 --- a/src/ui/widgets/Line/line.test.tsx +++ b/src/ui/widgets/Line/line.test.tsx @@ -49,9 +49,11 @@ describe("", (): void => { }; const svg = LineRenderer(lineProps); + console.log(svg); expect(svg.props.viewBox).toEqual("0 0 20 25"); const lines = svg.children as Array; + console.log(lines); expect(lines[0].props.stroke).toEqual("rgba(0,255,255,255)"); expect(lines[0].props.strokeWidth).toEqual(1); @@ -74,18 +76,28 @@ describe("", (): void => { { x: 25, y: 10 }, { x: 4, y: 15 } ] - } + }, + arrows: 3, + arrowLength: 10 }; const svg = LineRenderer(lineProps); expect(svg.props.viewBox).toEqual("0 0 30 20"); const lines = svg.children as Array; + const marker = lines[0].children as Array; + + // Check arrowhead definitions were created + expect(marker[0].props.markerWidth).toEqual("10"); + expect(marker[0].props.markerHeight).toEqual("10"); + expect(marker[0].props.orient).toEqual("auto-start-reverse"); - expect(lines[0].props.stroke).toEqual("rgba(0,0,0,0)"); - expect(lines[0].props.strokeWidth).toEqual(15); - expect(lines[0].props.transform).toEqual("rotation(45,0,0)"); - expect(lines[0].props.points).toEqual("16,4 25,10 4,15 "); + expect(lines[1].props).toHaveProperty("markerStart"); + expect(lines[1].props).toHaveProperty("markerEnd"); + expect(lines[1].props.stroke).toEqual("rgba(0,0,0,0)"); + expect(lines[1].props.strokeWidth).toEqual(15); + expect(lines[1].props.transform).toEqual("rotation(45,0,0)"); + expect(lines[1].props.points).toEqual("16,4 25,10 4,15 "); }); test("line component not created if no points to plot", (): void => { diff --git a/src/ui/widgets/Line/line.tsx b/src/ui/widgets/Line/line.tsx index efd282c..a3e914c 100644 --- a/src/ui/widgets/Line/line.tsx +++ b/src/ui/widgets/Line/line.tsx @@ -7,7 +7,7 @@ import { ColorPropOpt, BoolPropOpt, FloatProp, - PointsPropOpt + PointsProp } from "../propTypes"; import { registerWidget } from "../register"; import { Color } from "../../../types/color"; @@ -16,12 +16,14 @@ import { Point } from "../../../types/points"; const LineProps = { width: FloatProp, height: FloatProp, + points: PointsProp, lineWidth: FloatPropOpt, backgroundColor: ColorPropOpt, visible: BoolPropOpt, transparent: BoolPropOpt, rotationAngle: FloatPropOpt, - points: PointsPropOpt + arrows: FloatPropOpt, + arrowLength: FloatPropOpt }; export type LineComponentProps = InferWidgetProps & @@ -31,16 +33,65 @@ export const LineComponent = (props: LineComponentProps): JSX.Element => { const { visible = true, transparent = false, - backgroundColor, + backgroundColor = Color.fromRgba(0, 0, 255), rotationAngle = 0, width, height, lineWidth = 1, - points + points, + arrowLength = 2, + arrows = 0 } = props; const transform = `rotation(${rotationAngle},0,0)`; + // Each marker definition needs a unique ID or colours overlap + // Get random decimal number, take decimal part and truncate + const uid = String(Math.random()).split(".")[1].substring(0, 6); + // Create a marker if arrows set + let arrowSize = ""; + let arrowConfig = {}; + let markerConfig = <>; + if (arrows) { + arrowSize = `M 0 0 L ${arrowLength} ${arrowLength / 4} L 0 ${ + arrowLength / 2 + } z`; + switch (arrows) { + case 1: + arrowConfig = { markerStart: `url(#arrow${uid})` }; + break; + case 2: + arrowConfig = { markerEnd: `url(#arrow${uid})` }; + break; + case 3: + arrowConfig = { + markerStart: `url(#arrow${uid})`, + markerEnd: `url(#arrow${uid})` + }; + break; + } + markerConfig = ( + + + + + + ); + } + // 0 is none, 1 is from (marker start), 2 is to (marker end), 3 is both + let coordinates = ""; if (points !== undefined && visible) { points.values.forEach((point: Point) => { @@ -50,7 +101,9 @@ export const LineComponent = (props: LineComponentProps): JSX.Element => { + {markerConfig} { strokeWidth={lineWidth} transform={transform} points={coordinates} + {...arrowConfig} /> ); From 2bcfd6aef886a4cda68404e2f8b2200edc1cf89d Mon Sep 17 00:00:00 2001 From: Abigail Alexander Date: Thu, 25 Jan 2024 15:46:46 +0000 Subject: [PATCH 4/7] Remove console.log statements --- src/ui/widgets/Line/line.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ui/widgets/Line/line.test.tsx b/src/ui/widgets/Line/line.test.tsx index 4f03a3a..e9c4783 100644 --- a/src/ui/widgets/Line/line.test.tsx +++ b/src/ui/widgets/Line/line.test.tsx @@ -49,11 +49,9 @@ describe("", (): void => { }; const svg = LineRenderer(lineProps); - console.log(svg); expect(svg.props.viewBox).toEqual("0 0 20 25"); const lines = svg.children as Array; - console.log(lines); expect(lines[0].props.stroke).toEqual("rgba(0,255,255,255)"); expect(lines[0].props.strokeWidth).toEqual(1); From 94af4ba78ec9059c11add1c0209ddcb817f7e8a7 Mon Sep 17 00:00:00 2001 From: Abigail Alexander Date: Thu, 1 Feb 2024 13:32:30 +0000 Subject: [PATCH 5/7] Add uuid package --- package-lock.json | 70 +++++++++++++++++++++++++++++++++++++++++------ package.json | 4 ++- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 25b6e3c..2c09d11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,8 @@ "plotly.js-basic-dist": "^2.20.0", "react-plotly.js": "^2.6.0", "redux": "^4.1.2", - "utf-8-validate": "^5.0.10" + "utf-8-validate": "^5.0.10", + "uuid": "^9.0.1" }, "devDependencies": { "@apollo/client": "^3.6.0", @@ -30,6 +31,7 @@ "@types/node": "^17.0.18", "@types/react-plotly.js": "^2.5.2", "@types/react-test-renderer": "^17.0.1", + "@types/uuid": "^9.0.8", "base64-js": "^1.3.1", "clipboard-copy": "^4.0.1", "eslint-config-prettier": "^8.3.0", @@ -4030,6 +4032,12 @@ "node": ">=0.10.0" } }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, "node_modules/@types/webpack": { "version": "4.41.32", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz", @@ -15281,6 +15289,16 @@ "which": "^2.0.2" } }, + "node_modules/node-notifier/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/node-releases": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", @@ -23756,6 +23774,15 @@ "ms": "^2.1.1" } }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -26152,10 +26179,13 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -31497,6 +31527,12 @@ } } }, + "@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, "@types/webpack": { "version": "4.41.32", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz", @@ -40546,6 +40582,15 @@ "shellwords": "^0.1.1", "uuid": "^8.3.0", "which": "^2.0.2" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "optional": true + } } }, "node-releases": { @@ -47056,6 +47101,14 @@ "faye-websocket": "^0.11.3", "uuid": "^8.3.2", "websocket-driver": "^0.7.4" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + } } }, "sockjs-client": { @@ -49027,10 +49080,9 @@ } }, "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, "v8-compile-cache": { "version": "2.3.0", diff --git a/package.json b/package.json index 4bf3283..3ef2345 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "plotly.js-basic-dist": "^2.20.0", "react-plotly.js": "^2.6.0", "redux": "^4.1.2", - "utf-8-validate": "^5.0.10" + "utf-8-validate": "^5.0.10", + "uuid": "^9.0.1" }, "devDependencies": { "@apollo/client": "^3.6.0", @@ -39,6 +40,7 @@ "@types/node": "^17.0.18", "@types/react-plotly.js": "^2.5.2", "@types/react-test-renderer": "^17.0.1", + "@types/uuid": "^9.0.8", "base64-js": "^1.3.1", "clipboard-copy": "^4.0.1", "eslint-config-prettier": "^8.3.0", From 6f998e0db014dd50ac3bfc27cd3d2e667cca391a Mon Sep 17 00:00:00 2001 From: Abigail Alexander Date: Thu, 1 Feb 2024 13:34:09 +0000 Subject: [PATCH 6/7] Add Line widget arrow config, update uuid and update tests --- src/ui/widgets/EmbeddedDisplay/opiParser.ts | 3 +- src/ui/widgets/Line/line.test.tsx | 81 ++++++++++++++-- src/ui/widgets/Line/line.tsx | 100 ++++++++++++++++++-- 3 files changed, 167 insertions(+), 17 deletions(-) diff --git a/src/ui/widgets/EmbeddedDisplay/opiParser.ts b/src/ui/widgets/EmbeddedDisplay/opiParser.ts index 8550d9c..ca72439 100644 --- a/src/ui/widgets/EmbeddedDisplay/opiParser.ts +++ b/src/ui/widgets/EmbeddedDisplay/opiParser.ts @@ -660,7 +660,8 @@ export const OPI_SIMPLE_PARSERS: ParserDict = { cornerWidth: ["corner_width", opiParseString], cornerHeight: ["corner_height", opiParseString], arrows: ["arrows", opiParseNumber], - arrowLength: ["arrow_length", opiParseNumber] + arrowLength: ["arrow_length", opiParseNumber], + fillArrow: ["fill_arrow", opiParseBoolean] }; /** diff --git a/src/ui/widgets/Line/line.test.tsx b/src/ui/widgets/Line/line.test.tsx index e9c4783..365999a 100644 --- a/src/ui/widgets/Line/line.test.tsx +++ b/src/ui/widgets/Line/line.test.tsx @@ -59,7 +59,7 @@ describe("", (): void => { expect(lines[0].props.points).toEqual("1,10 15,20 4,15 "); }); - test("props override default properties", (): void => { + test("props override default properties, no arrows", (): void => { const lineProps = { width: 30, height: 20, @@ -75,8 +75,39 @@ describe("", (): void => { { x: 4, y: 15 } ] }, + arrows: 0 + }; + + const svg = LineRenderer(lineProps); + expect(svg.props.viewBox).toEqual("0 0 30 20"); + + const lines = svg.children as Array; + + expect(lines[0].props.stroke).toEqual("rgba(0,0,0,0)"); + expect(lines[0].props.strokeWidth).toEqual(15); + expect(lines[0].props.transform).toEqual("rotation(45,0,0)"); + expect(lines[0].props.points).toEqual("16,4 25,10 4,15 "); + }); + + test("line with filled arrowhead", (): void => { + const lineProps = { + width: 30, + height: 20, + lineWidth: 15, + backgroundColor: Color.fromRgba(0, 254, 250), + transparent: true, + rotationAngle: 45, + visible: true, + points: { + values: [ + { x: 16, y: 5 }, + { x: 26, y: 10 }, + { x: 6, y: 15 } + ] + }, arrows: 3, - arrowLength: 10 + arrowLength: 2, + fillArrow: true }; const svg = LineRenderer(lineProps); @@ -86,16 +117,52 @@ describe("", (): void => { const marker = lines[0].children as Array; // Check arrowhead definitions were created - expect(marker[0].props.markerWidth).toEqual("10"); - expect(marker[0].props.markerHeight).toEqual("10"); + expect(marker[0].props.markerWidth).toEqual("2"); + expect(marker[0].props.markerHeight).toEqual("2"); expect(marker[0].props.orient).toEqual("auto-start-reverse"); + expect(marker[0].props.markerUnits).toEqual("userSpaceOnUse"); expect(lines[1].props).toHaveProperty("markerStart"); expect(lines[1].props).toHaveProperty("markerEnd"); + expect(lines[1].props.points).toEqual("18,6 26,10 8,15 "); + }); + + test("line with unfilled arrowhead", (): void => { + const lineProps = { + width: 30, + height: 20, + lineWidth: 2, + backgroundColor: Color.fromRgba(0, 254, 250), + transparent: true, + rotationAngle: 45, + visible: true, + points: { + values: [ + { x: 40, y: 10 }, + { x: 40, y: 20 }, + { x: 20, y: 20 } + ] + }, + arrows: 1, + arrowLength: 2, + fillArrow: false + }; + + const svg = LineRenderer(lineProps); + expect(svg.props.viewBox).toEqual("0 0 30 20"); + + const lines = svg.children as Array; + const marker = lines[0].children as Array; + + // Check arrowhead definitions were created + expect(marker[0].props.markerWidth).toEqual("1"); + expect(marker[0].props.markerHeight).toEqual("1"); + expect(marker[0].props.orient).toEqual("auto-start-reverse"); + expect(marker[0].props.markerUnits).toEqual("strokeWidth"); + + expect(lines[1].props).toHaveProperty("markerStart"); expect(lines[1].props.stroke).toEqual("rgba(0,0,0,0)"); - expect(lines[1].props.strokeWidth).toEqual(15); - expect(lines[1].props.transform).toEqual("rotation(45,0,0)"); - expect(lines[1].props.points).toEqual("16,4 25,10 4,15 "); + expect(lines[1].props.points).toEqual("40,10 40,20 20,20 "); }); test("line component not created if no points to plot", (): void => { diff --git a/src/ui/widgets/Line/line.tsx b/src/ui/widgets/Line/line.tsx index a3e914c..e9bb2a5 100644 --- a/src/ui/widgets/Line/line.tsx +++ b/src/ui/widgets/Line/line.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { v4 as uuidv4 } from "uuid"; import { Widget } from "../widget"; import { PVWidgetPropType, PVComponent } from "../widgetProps"; import { @@ -23,7 +24,8 @@ const LineProps = { transparent: BoolPropOpt, rotationAngle: FloatPropOpt, arrows: FloatPropOpt, - arrowLength: FloatPropOpt + arrowLength: FloatPropOpt, + fillArrow: BoolPropOpt }; export type LineComponentProps = InferWidgetProps & @@ -40,34 +42,70 @@ export const LineComponent = (props: LineComponentProps): JSX.Element => { lineWidth = 1, points, arrowLength = 2, - arrows = 0 + arrows = 0, + fillArrow } = props; const transform = `rotation(${rotationAngle},0,0)`; // Each marker definition needs a unique ID or colours overlap // Get random decimal number, take decimal part and truncate - const uid = String(Math.random()).split(".")[1].substring(0, 6); + const uid = uuidv4(); + // Create a marker if arrows set let arrowSize = ""; let arrowConfig = {}; let markerConfig = <>; + const linePoints = points ? points.values : []; if (arrows) { arrowSize = `M 0 0 L ${arrowLength} ${arrowLength / 4} L 0 ${ arrowLength / 2 - } z`; + } ${fillArrow ? "z" : ""}`; // add z to close path if filling switch (arrows) { + // Arrow from case 1: arrowConfig = { markerStart: `url(#arrow${uid})` }; + // If filled, shorten line length to prevent overlap + if (fillArrow) { + linePoints[0] = recalculateLineLength( + linePoints[1], + linePoints[0], + arrowLength + ); + } break; + + // Arrow to case 2: arrowConfig = { markerEnd: `url(#arrow${uid})` }; + // If filled, shorten line length to prevent overlap + if (fillArrow) { + linePoints[linePoints.length - 1] = recalculateLineLength( + linePoints[linePoints.length - 2], + linePoints[linePoints.length - 1], + arrowLength + ); + } break; + + // Arrow both case 3: arrowConfig = { markerStart: `url(#arrow${uid})`, markerEnd: `url(#arrow${uid})` }; + if (fillArrow) { + linePoints[linePoints.length - 1] = recalculateLineLength( + linePoints[linePoints.length - 2], + linePoints[linePoints.length - 1], + arrowLength + ); + linePoints[0] = recalculateLineLength( + linePoints[1], + linePoints[0], + arrowLength + ); + } break; } markerConfig = ( @@ -75,22 +113,29 @@ export const LineComponent = (props: LineComponentProps): JSX.Element => { ); } - // 0 is none, 1 is from (marker start), 2 is to (marker end), 3 is both let coordinates = ""; if (points !== undefined && visible) { @@ -124,6 +169,43 @@ export const LineComponent = (props: LineComponentProps): JSX.Element => { } }; +/** + * Recalculate the length of the line when a filled arrow + * is present. This prevents the line from obscuring the + * arrow. SVG does not include any easy way to layer an arrow + * on top of a line without having the line show if it is thicker + * than the arrow width, so we shorten the line to prevent this. + * Calculate hypoteneuse of line and shorten by arrow length, then map + * this change to the x, y coordinates + * @param startPoint the first set of x,y coordinates of line segment + * @param endPoint the second set of x, y coordinates of line segment + */ +function recalculateLineLength( + startPoint: Point, + endPoint: Point, + arrowLen: number +): Point { + // Determine x and y distance between coordinate sets + let xLen = endPoint.x - startPoint.x; + let yLen = endPoint.y - startPoint.y; + // Calculate hypoteneuse length + const lineLen = Math.hypot(xLen, yLen); + // Calculate new length by subtracting arrowlength + const newLineLen = lineLen - arrowLen; + // If arrowLen longer than lineLen, make line short as possible + // Ideally shouldn't be used this way as arrow should be shorter + // Determine what fraction smaller new length is + const frac = (arrowLen >= lineLen ? 2 : newLineLen) / lineLen; + // Multiply lengths by fraction to get new lengths + xLen *= frac; + yLen *= frac; + // Calculate new final x y coordinates + endPoint.x = startPoint.x + Math.round(xLen); + endPoint.y = startPoint.y + Math.round(yLen); + // Return newly calculated final coordinates + return endPoint; +} + const LineWidgetProps = { ...LineProps, ...PVWidgetPropType From 95cb6a4055264cf63c430a2fc67dfbe0f2f2bf8f Mon Sep 17 00:00:00 2001 From: Abigail Alexander Date: Thu, 1 Feb 2024 15:43:24 +0000 Subject: [PATCH 7/7] Remove unneeded comment --- src/ui/widgets/Line/line.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui/widgets/Line/line.tsx b/src/ui/widgets/Line/line.tsx index e9bb2a5..b2a085c 100644 --- a/src/ui/widgets/Line/line.tsx +++ b/src/ui/widgets/Line/line.tsx @@ -49,7 +49,6 @@ export const LineComponent = (props: LineComponentProps): JSX.Element => { const transform = `rotation(${rotationAngle},0,0)`; // Each marker definition needs a unique ID or colours overlap - // Get random decimal number, take decimal part and truncate const uid = uuidv4(); // Create a marker if arrows set