websocket-text-relay 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/changelog.md +4 -0
  2. package/docs/code-structure.md +15 -11
  3. package/eslint.config.js +5 -1
  4. package/package.json +9 -9
  5. package/src/ui/css/main.css +22 -78
  6. package/src/ui/favicon.png +0 -0
  7. package/src/ui/index.html +19 -14
  8. package/src/ui/js/components/activityLabels.js +41 -0
  9. package/src/ui/js/components/activityTimeSeries.js +58 -0
  10. package/src/ui/js/components/drawSessionLabel.js +117 -0
  11. package/src/ui/js/components/footerStatus.js +37 -0
  12. package/src/ui/js/components/headers.js +100 -0
  13. package/src/ui/js/components/sessionWedges.js +96 -0
  14. package/src/ui/js/components/statusRing.js +51 -0
  15. package/src/ui/js/data/wtrActivity.js +85 -0
  16. package/src/ui/js/data/wtrActivity.types.js +3 -0
  17. package/src/ui/js/data/wtrStatus.js +134 -0
  18. package/src/ui/js/data/wtrStatus.types.js +61 -0
  19. package/src/ui/js/{util → setup}/EventEmitter.js +5 -1
  20. package/src/ui/js/{util → setup}/WebsocketClient.js +1 -4
  21. package/src/ui/js/setup/dependencyManager.js +9 -0
  22. package/src/ui/js/setup/evalOnChange.js +18 -0
  23. package/src/ui/js/setup/eventSubscriber.js +21 -0
  24. package/src/ui/js/setup.js +141 -0
  25. package/src/ui/js/util/constants.js +5 -1
  26. package/src/ui/js/util/drawing.js +26 -76
  27. package/src/websocket-interface/httpServer.js +1 -1
  28. package/src/ui/js/components/ActivityTimeseriesGraph.js +0 -194
  29. package/src/ui/js/components/HeaderSummary.js +0 -22
  30. package/src/ui/js/components/ServerStatus.js +0 -43
  31. package/src/ui/js/components/SessionLabels.js +0 -319
  32. package/src/ui/js/components/SessionWedges.js +0 -127
  33. package/src/ui/js/components/StatusRing.js +0 -54
  34. package/src/ui/js/components/grids.js +0 -36
  35. package/src/ui/js/index.js +0 -121
  36. package/src/ui/js/main.js +0 -128
  37. package/src/ui/js/util/DependencyManager.js +0 -31
package/changelog.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 1.1.5 - 2025/09/05
2
+
3
+ Minor UI spacing fix
4
+
1
5
  ## 1.1.4 - 2025/08/14
2
6
 
3
7
  Fixed a crash that occurred when the status UI requests a file that doesn't exist.
@@ -87,21 +87,27 @@ The root of the static site is the `src/ui` directory.
87
87
  The UI is entirely made up of SVG elements, so the only thing the index.html file has to do is set up the root
88
88
  SVG element with some groups to act as containers for the different components to use.
89
89
 
90
- ### js/index.js
90
+ ### js/setup.js
91
91
 
92
92
  - Sets up the websocket-text-relay client with handlers for css and javascript files.
93
93
  - initializes the simple dependency management system that allows the UI to be split into several javascript files.
94
- - Hooks up events to handle resizing the SVG element on window resize.
95
94
  - emit data and activity events that the UI components can hook into
96
95
 
97
- Whenever a javascript file is edited, its exports are updated and the main.js file is rerun.
98
-
99
- ### js/util/DependencyManager.js
96
+ ### js/setup/dependencyManager.js
100
97
 
101
98
  This is a very quick and simple dependency management system. The dependency container is just an object in the global scope.
102
99
  An exportDeps function is created to make it easy to specify which objects in scope are to be exported.
103
100
 
104
- An onEvent function is also created and exported here, it prevents event leaks by cleaning up any registered event handlers whenever the javascript is reevaluated.
101
+ ### js/setup/eventSubscriber.js
102
+
103
+ Provides the onEvent function that each file can use when subscribing to events. The events are automatically
104
+ unsubscribed when that file gets reevaluated. See the `js/components/sessionWedges.js` file for an example of usage.
105
+
106
+ ### js/setup/evalOnChange.js
107
+
108
+ This provides a function where you can define what files should be run after the current file is updated.
109
+ Rather than calculate a dependency graph and try to automatically rerun files, simply define the behavior you want
110
+ while you are editing the file. See the `js/util/constants.js` file for an example of usage.
105
111
 
106
112
  ### js/util/drawing.js
107
113
 
@@ -115,10 +121,8 @@ The center of the UI is at (0, 0) with a minimum height and width of 2. Having a
115
121
 
116
122
  ### js/components/
117
123
 
118
- The components directory contains the javascript classes that render the different elements on the screen.
119
-
120
- Each class handles the state for its component and has a draw function that renders it to the screen.
124
+ The components directory contains the javascript files that render the different elements on the screen.
121
125
 
122
- Any components affected by data updates will also have an update function. When called the component will update the elements it manages with the new data.
126
+ ### js/data/
123
127
 
124
- Some components also respond to text update activity, these components will have a triggerActivity function as well.
128
+ Read data from the websocket server and construct data stores and events required by the components.
package/eslint.config.js CHANGED
@@ -2,12 +2,16 @@ import js from "@eslint/js"
2
2
  import globals from "globals"
3
3
  import { defineConfig } from "eslint/config"
4
4
 
5
+ const wtrGlobals = {
6
+ __WTR__: "readonly",
7
+ }
8
+
5
9
  export default defineConfig([
6
10
  {
7
11
  files: ["**/*.{js,mjs,cjs}"],
8
12
  plugins: { js },
9
13
  extends: ["js/recommended"],
10
- languageOptions: { globals: { ...globals.node, ...globals.browser } },
14
+ languageOptions: { globals: { ...globals.node, ...globals.browser, ...wtrGlobals } },
11
15
  },
12
16
  {
13
17
  rules: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "websocket-text-relay",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "An LSP server for sending live file updates from your text editor to the front end via websockets.",
5
5
  "bin": {
6
6
  "websocket-text-relay": "./start.js"
@@ -36,15 +36,15 @@
36
36
  },
37
37
  "homepage": "https://github.com/niels4/websocket-text-relay#readme",
38
38
  "devDependencies": {
39
- "@eslint/js": "^9.33.0",
40
- "@vitest/coverage-v8": "^3.2.4",
41
- "@vitest/ui": "^3.2.4",
42
- "eslint": "^9.33.0",
43
- "globals": "^16.3.0",
44
- "prettier": "3.6.2",
45
- "vitest": "^3.2.4"
39
+ "@eslint/js": "9.39.2",
40
+ "@vitest/coverage-v8": "4.0.16",
41
+ "@vitest/ui": "4.0.16",
42
+ "eslint": "9.39.2",
43
+ "globals": "16.5.0",
44
+ "prettier": "3.7.4",
45
+ "vitest": "4.0.16"
46
46
  },
47
47
  "dependencies": {
48
- "ws": "^8.18.3"
48
+ "ws": "8.18.3"
49
49
  }
50
50
  }
@@ -9,16 +9,12 @@
9
9
  --color-evening-shadow: #514a45;
10
10
  }
11
11
 
12
- .color_palette {
13
- --bg2: var(--color-navy-nightfall);
14
- --accent-active: var(var(--color-golden-sunset));
15
- }
16
-
17
12
  .color_theme {
18
- --main-background-color: var(--bg2);
13
+ --accent-active: var(var(--color-golden-sunset));
14
+ --main-background-color: var(--color-navy-nightfall);
19
15
  --header-text-color: var(--color-ivory-daybreak);
20
16
  --summary-value-text-color: hsl(from var(--header-text-color) h s l / 0.9);
21
- --offline-color: oklch(from var(--color-golden-sunset) calc(l * 0.5) calc(c * 0.25) h);
17
+ --offline-color: oklch(from var(--color-tangerine-sunset) calc(l * 0.4) calc(c * 0.1) h);
22
18
  --online-color: oklch(from var(--color-golden-sunset) calc(l * 0.8) calc(c * 0.25) h);
23
19
  --active-color: var(--color-golden-sunset);
24
20
  --wedge-online-color: oklch(from var(--color-azure-afternoon) calc(l * 0.5) calc(c * 0.5) h);
@@ -40,55 +36,33 @@ body {
40
36
  font-size: 0.1pt;
41
37
  }
42
38
 
43
- #grid_group {
44
- stroke: #fff;
45
- stroke-width: 0.001;
46
- }
47
-
48
- .grid_axis {
49
- stroke-width: 0.003;
50
- stroke: hsl(76, 90%, 80%);
51
- }
52
-
53
- #header_summary_group {
39
+ .header {
54
40
  fill: var(--header-text-color);
55
41
  font-weight: 300;
42
+ font-size: 0.1pt;
56
43
  }
57
44
 
58
45
  .right_header {
59
46
  text-anchor: end;
60
47
  }
61
48
 
62
- .header_number {
63
- font-weight: 100;
64
- }
65
-
66
- #status_ring_group {
49
+ .status_ring_wrapper.offline {
67
50
  stroke: var(--offline-color);
68
51
  }
69
52
 
70
- #status_ring_group.online {
53
+ .status_ring_wrapper.online {
71
54
  stroke: var(--online-color);
72
55
  }
73
56
 
74
- #status_ring_group.active {
57
+ .status_ring_wrapper.active {
75
58
  stroke: var(--active-color);
76
59
  }
77
60
 
78
- .single_wedge_group {
79
- display: none;
80
- }
81
-
82
- .single_wedge_group.online,
83
- .single_wedge_group.active {
84
- display: inherit;
85
- }
86
-
87
- .online .wedge_node {
61
+ .wedge_node {
88
62
  fill: var(--wedge-online-color);
89
63
  }
90
64
 
91
- .active .wedge_node {
65
+ .wedge_node.active {
92
66
  fill: var(--wedge-active-color);
93
67
  }
94
68
 
@@ -115,57 +89,27 @@ body {
115
89
  fill: var(--active-color);
116
90
  }
117
91
 
118
- .timeseries_bg {
119
- /* fill: #ffffff1f; */
120
- fill: none;
121
- }
122
-
123
- .timeseries_path {
92
+ .time_series_path {
124
93
  stroke: var(--timeseries-line-color);
125
- clip-path: url(#inner_circle_clip);
94
+ stroke-width: 3;
95
+ vector-effect: non-scaling-stroke;
96
+ shape-rendering: crispEdges;
126
97
  }
127
98
 
128
- .timeseries_big_label {
129
- /* fill: var(--timeseries-label-color); */
130
- font-size: 0.04pt;
131
- font-weight: 400;
132
- text-anchor: end;
133
- }
134
-
135
- .timeseries_small_label {
136
- /* fill: var(--timeseries-label-color); */
99
+ .small_label {
137
100
  font-size: 0.03pt;
138
101
  font-weight: 400;
139
- text-anchor: end;
102
+ fill: var(--timeseries-label-color);
103
+ text-anchor: middle;
104
+ dominant-baseline: central;
140
105
  }
141
106
 
142
- .timeseries_value {
107
+ .time_series_value {
143
108
  fill: var(--timeseries-value-color);
144
109
  font-size: 0.05pt;
145
110
  font-weight: 300;
146
111
  text-anchor: middle;
147
- }
148
-
149
- .tooltip_text {
150
- fill: #fff;
151
- font-size: 0.025pt;
152
- font-weight: 200;
153
- text-anchor: middle;
154
- }
155
-
156
- .tooltip_outline {
157
- stroke: #fff;
158
- stroke-width: 0.002;
159
- fill: #000;
160
- }
161
-
162
- .tooltip_display_group {
163
- opacity: 0;
164
- transition: opacity 0.35s ease;
165
- }
166
-
167
- .tooltip_wrapper_group:hover .tooltip_display_group {
168
- opacity: 1;
112
+ dominant-baseline: central;
169
113
  }
170
114
 
171
115
  .server_status_label {
@@ -189,11 +133,11 @@ body {
189
133
  text-anchor: middle;
190
134
  }
191
135
 
192
- .test_circle {
136
+ .wedge_label_dot {
193
137
  fill: #fff;
194
138
  }
195
139
 
196
- .test_line {
140
+ .wedge_label_line {
197
141
  stroke: #fff;
198
142
  stroke-width: 0.005;
199
143
  }
Binary file
package/src/ui/index.html CHANGED
@@ -6,31 +6,36 @@
6
6
 
7
7
  <title>WTR Status</title>
8
8
 
9
+ <link rel="icon" type="image/png" href="favicon.png" />
9
10
  <link rel="stylesheet" href="css/fonts.css" />
10
11
  <style id="main_style"></style>
11
- <script type="module" src="js/index.js" defer></script>
12
+ <script type="module" src="js/setup.js" defer></script>
12
13
 
13
14
  <style>
14
- body {
15
- margin: 0;
15
+ html {
16
+ height: 100dvh;
16
17
  overscroll-behavior: none;
17
- display: grid;
18
- align-items: center;
19
- justify-content: center;
20
- padding-top: 2px;
18
+ }
19
+
20
+ #svg_root {
21
+ position: absolute;
22
+ top: 0;
23
+ left: 0;
24
+ height: 100dvh;
25
+ width: 100dvw;
21
26
  }
22
27
  </style>
23
28
  </head>
24
29
 
25
- <body class="base_colors color_palette color_theme">
30
+ <body class="base_colors color_theme">
26
31
  <svg id="svg_root" viewBox="-1 -1 2 2" xmlns="http://www.w3.org/2000/svg">
27
- <g id="grid_group"></g>
28
- <g id="header_summary_group"></g>
29
- <g id="status_ring_group"></g>
30
- <g id="client_wedges_group" class="wedge_group"></g>
32
+ <g id="headers_group"></g>
31
33
  <g id="editor_wedges_group" class="wedge_group"></g>
32
- <g id="activity_timeseries_graph"></g>
33
- <g id="server_status_group"></g>
34
+ <g id="client_wedges_group" class="wedge_group"></g>
35
+ <g id="activity_time_series_group"></g>
36
+ <g id="activity_labels_group"></g>
37
+ <g id="status_ring_group"></g>
38
+ <g id="footer_status_group"></g>
34
39
  </svg>
35
40
  </body>
36
41
  </html>
@@ -0,0 +1,41 @@
1
+ const { drawText, wtrActivityDataWindowEmitter, onEvent, constants } = __WTR__
2
+ const { innerRingRadius } = constants
3
+
4
+ const parentGroup = document.getElementById("activity_labels_group")
5
+ parentGroup.innerHTML = ""
6
+
7
+ const valuePadding = 0.068
8
+ const labelPadding = 0.14
9
+
10
+ drawText({
11
+ text: "Max",
12
+ y: -innerRingRadius + labelPadding,
13
+ className: "small_label",
14
+ parent: parentGroup,
15
+ })
16
+
17
+ const maxValueText = drawText({
18
+ text: "0",
19
+ y: -innerRingRadius + valuePadding,
20
+ className: "time_series_value",
21
+ parent: parentGroup,
22
+ })
23
+
24
+ drawText({
25
+ text: "Updates / second",
26
+ y: innerRingRadius - labelPadding,
27
+ className: "small_label",
28
+ parent: parentGroup,
29
+ })
30
+
31
+ const currentValueText = drawText({
32
+ text: "0",
33
+ y: innerRingRadius - valuePadding,
34
+ className: "time_series_value",
35
+ parent: parentGroup,
36
+ })
37
+
38
+ onEvent(wtrActivityDataWindowEmitter, "data", ({ maxValue, currentValue }) => {
39
+ maxValueText.textContent = maxValue
40
+ currentValueText.textContent = currentValue
41
+ })
@@ -0,0 +1,58 @@
1
+ const { drawSvgElement, constants, wtrActivityDataWindowEmitter, onEvent } = __WTR__
2
+ const { innerRingRadius, dataWindowSize } = constants
3
+
4
+ const parentGroup = document.getElementById("activity_time_series_group")
5
+ parentGroup.innerHTML = ""
6
+
7
+ const animationProps = { duration: 1000, easing: "linear", iterations: 1, fill: "forwards" }
8
+
9
+ const minX = -innerRingRadius
10
+ const width = innerRingRadius * 2
11
+ const height = 0.25
12
+ const maxY = height / 2
13
+ const intervalWidth = width / (dataWindowSize - 2)
14
+
15
+ const clipId = "time-series-clip"
16
+
17
+ const clipPath = drawSvgElement({
18
+ tag: "clipPath",
19
+ attributes: { id: clipId },
20
+ parent: parentGroup,
21
+ })
22
+
23
+ drawSvgElement({
24
+ tag: "circle",
25
+ attributes: { r: innerRingRadius },
26
+ parent: clipPath,
27
+ })
28
+
29
+ const clippedGroup = drawSvgElement({
30
+ tag: "g",
31
+ attributes: { "clip-path": `url(#${clipId})` },
32
+ parent: parentGroup,
33
+ })
34
+
35
+ const valuePath = drawSvgElement({
36
+ tag: "path",
37
+ attributes: { d: "" },
38
+ className: "time_series_path",
39
+ parent: clippedGroup,
40
+ })
41
+
42
+ const getValueScale = (maxValue) => (maxValue === 0 ? 0.00001 : height / maxValue)
43
+
44
+ onEvent(wtrActivityDataWindowEmitter, "data", (data) => {
45
+ valuePath.setAttribute("d", data.path)
46
+ const prevValueScale = getValueScale(data.prevMaxValue)
47
+ const valueScale = getValueScale(data.maxValue)
48
+
49
+ const animationKeyFrames = [
50
+ {
51
+ transform: `translateX(${minX}px) translateY(${maxY}px) scaleX(${intervalWidth}) scaleY(${-prevValueScale})`,
52
+ },
53
+ {
54
+ transform: `translateX(${minX - intervalWidth}px) translateY(${maxY}px) scaleX(${intervalWidth}) scaleY(${-valueScale})`,
55
+ },
56
+ ]
57
+ valuePath.animate(animationKeyFrames, animationProps)
58
+ })
@@ -0,0 +1,117 @@
1
+ const {
2
+ drawSvgElement,
3
+ drawPolarCircle,
4
+ drawPolarLine,
5
+ drawText,
6
+ polarToCartesian,
7
+ exportDeps,
8
+ evalOnChange,
9
+ } = __WTR__
10
+
11
+ evalOnChange(["js/components/sessionWedges.js"])
12
+
13
+ const labelLineDistance = 0.07
14
+ const underlinePadding = 0.02
15
+ const summaryCircleRadius = 0.014
16
+ const summaryValueSpacing = 0.1
17
+
18
+ /**
19
+ * @param {{session: EditorStatus | ClientStatus, direction: 1 | -1}}
20
+ */
21
+ const drawSessionLabel = ({ wedgeCenterAngle, wedgeCenterRadius, session, direction, parent }) => {
22
+ drawPolarCircle({
23
+ angle: wedgeCenterAngle,
24
+ radius: wedgeCenterRadius,
25
+ r: 0.01,
26
+ className: "wedge_label_dot",
27
+ parent,
28
+ })
29
+
30
+ const textStartRadius = wedgeCenterRadius + labelLineDistance
31
+ drawPolarLine({
32
+ startAngle: wedgeCenterAngle,
33
+ startRadius: wedgeCenterRadius,
34
+ endAngle: wedgeCenterAngle,
35
+ endRadius: textStartRadius,
36
+ className: "wedge_label_line",
37
+ parent,
38
+ })
39
+
40
+ const serverIndicator = session.isServer ? "* " : ""
41
+ const [textStartX, textStartY] = polarToCartesian(wedgeCenterAngle, textStartRadius)
42
+ const nameTextNode = drawText({
43
+ x: textStartX,
44
+ y: textStartY - underlinePadding,
45
+ text: serverIndicator + session.name,
46
+ textAnchor: direction === 1 ? "end" : "start",
47
+ className: "wedge_identifier",
48
+ parent,
49
+ })
50
+ const nameTextBBox = nameTextNode.getBBox()
51
+
52
+ const underlineX2 = direction === 1 ? nameTextBBox.x : nameTextBBox.x + nameTextBBox.width
53
+ drawSvgElement({
54
+ tag: "line",
55
+ attributes: { x1: textStartX, y1: textStartY, x2: underlineX2, y2: textStartY },
56
+ className: "wedge_label_line",
57
+ parent,
58
+ })
59
+
60
+ const summaryGroup = drawSvgElement({ tag: "g", parent })
61
+
62
+ let currentSummaryX = textStartX
63
+
64
+ const { leftText, rightText } =
65
+ direction === 1
66
+ ? { leftText: session.openCount, rightText: session.activeOpenCount }
67
+ : { leftText: session.watchCount, rightText: session.activeWatchCount }
68
+
69
+ drawText({
70
+ x: currentSummaryX + summaryCircleRadius * 2,
71
+ y: textStartY,
72
+ text: leftText,
73
+ dominantBaseline: "middle",
74
+ className: "summary_text_value",
75
+ parent: summaryGroup,
76
+ })
77
+
78
+ drawSvgElement({
79
+ tag: "circle",
80
+ attributes: {
81
+ cx: currentSummaryX,
82
+ cy: textStartY - summaryCircleRadius / 2,
83
+ r: summaryCircleRadius,
84
+ },
85
+ className: "summary_watched_circle",
86
+ parent: summaryGroup,
87
+ })
88
+
89
+ currentSummaryX += summaryValueSpacing
90
+
91
+ drawText({
92
+ x: currentSummaryX + summaryCircleRadius * 2,
93
+ y: textStartY,
94
+ text: rightText,
95
+ dominantBaseline: "middle",
96
+ className: "summary_text_value",
97
+ parent: summaryGroup,
98
+ })
99
+
100
+ drawSvgElement({
101
+ tag: "circle",
102
+ attributes: {
103
+ cx: currentSummaryX,
104
+ cy: textStartY - summaryCircleRadius / 2,
105
+ r: summaryCircleRadius,
106
+ },
107
+ className: "summary_active_circle",
108
+ parent: summaryGroup,
109
+ })
110
+
111
+ const summaryGroupBBox = summaryGroup.getBBox()
112
+ const translateX = direction === 1 ? -summaryGroupBBox.width - 0.02 : 0.05
113
+ const translateY = summaryGroupBBox.height / 2 + 0.014
114
+ summaryGroup.setAttribute("transform", `translate(${translateX}, ${translateY})`)
115
+ }
116
+
117
+ exportDeps({ drawSessionLabel })
@@ -0,0 +1,37 @@
1
+ const { drawText, wtrStatusEmitter, onEvent } = __WTR__
2
+
3
+ const valueTextClass = "server_status_value"
4
+ const offlineTextClass = "server_status_offline"
5
+
6
+ const parentGroup = document.getElementById("footer_status_group")
7
+ parentGroup.innerHTML = ""
8
+
9
+ drawText({
10
+ x: 0,
11
+ y: 0.85,
12
+ text: "WS Server PID",
13
+ className: "server_status_label",
14
+ parent: parentGroup,
15
+ })
16
+
17
+ const pidElement = drawText({ x: 0, y: 0.748, text: "-1", parent: parentGroup })
18
+
19
+ const offlineElement = drawText({
20
+ x: 0,
21
+ y: 0.748,
22
+ text: "OFFLINE",
23
+ className: offlineTextClass,
24
+ parent: parentGroup,
25
+ })
26
+
27
+ onEvent(wtrStatusEmitter, "data", (/** @type {WtrStatus} */ data) => {
28
+ const server = data.editors.find((editor) => editor.isServer)
29
+ if (!data.isOnline || server == null) {
30
+ pidElement.classList.remove(valueTextClass)
31
+ offlineElement.classList.add(offlineTextClass)
32
+ } else {
33
+ pidElement.textContent = server.lsPid
34
+ pidElement.classList.add(valueTextClass)
35
+ offlineElement.classList.remove(offlineTextClass)
36
+ }
37
+ })
@@ -0,0 +1,100 @@
1
+ const { drawText, drawSvgElement } = __WTR__
2
+
3
+ const parentGroup = document.getElementById("headers_group")
4
+ parentGroup.innerHTML = ""
5
+
6
+ const legendCircleRadius = 0.014
7
+
8
+ const baseLineY = -0.73
9
+ const xOffset = 0.86
10
+ const legendY = baseLineY + 0.05
11
+
12
+ // left header
13
+ drawText({ x: -xOffset, y: baseLineY, text: "editors", className: "header", parent: parentGroup })
14
+
15
+ // left legend
16
+ let circleStart = -xOffset + 0.093
17
+ let labelStart = circleStart + legendCircleRadius * 2
18
+
19
+ drawSvgElement({
20
+ tag: "circle",
21
+ className: "summary_watched_circle",
22
+ attributes: { r: legendCircleRadius, cx: circleStart, cy: legendY },
23
+ parent: parentGroup,
24
+ })
25
+
26
+ drawText({
27
+ text: "Open",
28
+ textAnchor: "start",
29
+ className: "small_label",
30
+ x: labelStart,
31
+ y: legendY,
32
+ parent: parentGroup,
33
+ })
34
+
35
+ circleStart += 0.1822
36
+ labelStart = circleStart + legendCircleRadius * 2
37
+
38
+ drawSvgElement({
39
+ tag: "circle",
40
+ className: "summary_active_circle",
41
+ attributes: { r: legendCircleRadius, cx: circleStart, cy: legendY },
42
+ parent: parentGroup,
43
+ })
44
+
45
+ drawText({
46
+ text: "Active",
47
+ textAnchor: "start",
48
+ className: "small_label",
49
+ x: labelStart,
50
+ y: legendY,
51
+ parent: parentGroup,
52
+ })
53
+
54
+ // right header
55
+ drawText({
56
+ x: xOffset,
57
+ y: baseLineY,
58
+ text: "clients",
59
+ className: ["header", "right_header"],
60
+ parent: parentGroup,
61
+ })
62
+
63
+ // right legend
64
+ circleStart = xOffset - 0.3914
65
+ labelStart = circleStart + legendCircleRadius * 2
66
+
67
+ drawSvgElement({
68
+ tag: "circle",
69
+ className: "summary_watched_circle",
70
+ attributes: { r: legendCircleRadius, cx: circleStart, cy: legendY },
71
+ parent: parentGroup,
72
+ })
73
+
74
+ drawText({
75
+ text: "Watch",
76
+ textAnchor: "start",
77
+ className: "small_label",
78
+ x: labelStart,
79
+ y: legendY,
80
+ parent: parentGroup,
81
+ })
82
+
83
+ circleStart += 0.1952
84
+ labelStart = circleStart + legendCircleRadius * 2
85
+
86
+ drawSvgElement({
87
+ tag: "circle",
88
+ className: "summary_active_circle",
89
+ attributes: { r: legendCircleRadius, cx: circleStart, cy: legendY },
90
+ parent: parentGroup,
91
+ })
92
+
93
+ drawText({
94
+ text: "Active",
95
+ textAnchor: "start",
96
+ className: "small_label",
97
+ x: labelStart,
98
+ y: legendY,
99
+ parent: parentGroup,
100
+ })