• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

source-academy / plugins / 27974749198

22 Jun 2026 06:25PM UTC coverage: 41.032% (-40.0%) from 81.081%
27974749198

Pull #38

github

web-flow
Merge 5879a1978 into a31e3fb33
Pull Request #38: Add Data Visualisation

98 of 298 branches covered (32.89%)

Branch coverage included in aggregate %.

214 of 488 new or added lines in 20 files covered. (43.85%)

236 of 516 relevant lines covered (45.74%)

1.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.64
/src/web/data-display/src/SideContentDataVisualizer.tsx
1
import {
2
  AnchorButton,
3
  Button,
4
  ButtonGroup,
5
  Card,
6
  Checkbox,
7
  Classes,
8
  Icon,
9
  Tooltip,
10
} from "@blueprintjs/core";
11
import { IconNames } from "@blueprintjs/icons";
12
import { useHotkeys, type HotkeyItem } from "@mantine/hooks";
13
import type { Tab } from "@sourceacademy/common-tabs";
14
import classNames from "classnames";
15
import i18n from "i18next";
16
import { useEffect, useState } from "react";
17

18
import { Trans, initReactI18next, useTranslation } from "react-i18next";
19
import DataVisualizer from "./dataVisualizer";
20
import type { Step } from "./dataVisualizerTypes";
21
import type { Config } from "@sourceacademy/common-data-display";
22
import React from "react";
23

24
type Props = {
25
  workspaceLocation: string;
26
  config: Config;
27
};
28
export function ItalicLink({ href, children }: { href: string; children?: React.ReactNode }) {
29
  return (
2✔
30
    <a href={href} rel="noopener noreferrer" target="_blank">
31
      <i>{children}</i>
32
    </a>
33
  );
34
}
35

36
const translations = {
1✔
37
  defaultText: "The data visualizer helps you to visualize data structures.",
38
  instructions:
39
    "It is activated by calling the function <0/>, where <1/> would be the <2/> data structure that you want to visualize and <3/> is the number of structures.",
40
  reference: "The data visualizer uses box-and-pointer diagrams, as introduced in <0 />.",
41
  label: "Data Visualizer",
42
  previous: "Previous",
43
  next: "Next",
44
  call: "Call",
45
  structure: "Structure",
46
  views: {
47
    original: "Original View",
48
    binaryTree: "Binary Tree View",
49
    generalTree: "General Tree View",
50
  },
51
};
52
i18n.use(initReactI18next).init({
1✔
53
  resources: {
54
    en: { translation: translations },
55
  },
56
  fallbackLng: "en",
57
  interpolation: {
58
    escapeValue: false,
59
  },
60
});
61

62
function SideContentDataVisualizer({ workspaceLocation, config }: Props) {
63
  const [steps, setSteps] = useState<Step[]>([]);
2✔
64
  const [currentStep, setCurrentStep] = useState(0);
2✔
65
  const { t } = useTranslation();
2✔
66

67
  useEffect(() => {
2✔
68
    DataVisualizer.init(steps => {
1✔
69
      setSteps(steps);
1✔
70
      setCurrentStep(0);
1✔
71
    });
72
  }, [workspaceLocation]);
73

74
  const onPrevButtonClick = () => {
2✔
NEW
75
    setCurrentStep(prev => Math.max(0, prev - 1));
×
76
  };
77

78
  const onNextButtonClick = () => {
2✔
NEW
79
    setCurrentStep(prev => Math.min(steps.length - 1, prev + 1));
×
80
  };
81

82
  const onViewModeClick = (prevStep: number) => {
2✔
NEW
83
    setCurrentStep(prevStep);
×
84
  };
85

86
  const step: Step | undefined = steps[currentStep];
2✔
87
  const firstStep = currentStep === 0;
2✔
88
  const finalStep = !steps || currentStep === steps.length - 1;
2✔
89

90
  const hotkeyBindings: HotkeyItem[] = [
2✔
91
    ["ArrowLeft", onPrevButtonClick],
92
    ["ArrowRight", onNextButtonClick],
93
  ];
94
  useHotkeys(hotkeyBindings);
2✔
95

96
  return (
2✔
97
    <div className={classNames("sa-data-visualizer", Classes.DARK)}>
98
      {steps.length > 1 ? (
2!
99
        <div
100
          style={{
101
            position: "relative",
102
            display: "flex",
103
            justifyContent: "center",
104
            alignItems: "center",
105
            marginBottom: 10,
106
          }}
107
        >
108
          <Button
109
            style={{
110
              position: "absolute",
111
              left: 0,
112
            }}
113
            size="large"
114
            variant="outlined"
115
            icon={IconNames.ARROW_LEFT}
116
            onClick={onPrevButtonClick}
117
            disabled={firstStep}
118
          >
119
            {t("previous")}
120
          </Button>
121
          <h3 className={Classes.TEXT_LARGE}>
122
            {t("call")} {currentStep + 1}/{steps.length}
123
          </h3>
124
          <Button
125
            style={{
126
              position: "absolute",
127
              right: 0,
128
            }}
129
            size="large"
130
            variant="outlined"
131
            icon={IconNames.ARROW_RIGHT}
132
            onClick={onNextButtonClick}
133
            disabled={finalStep}
134
          >
135
            {t("next")}
136
          </Button>
137
        </div>
138
      ) : null}
139
      {steps.length > 0 ? (
2!
140
        <div
141
          key={step.length} // To ensure the style refreshes if the step length changes
142
          style={{
143
            display: "flex",
144
            flexDirection: "row",
145
            overflowX: "auto",
146
          }}
147
        >
148
          {step?.map((elem, i) => (
NEW
149
            <div key={i} style={{ margin: step.length > 1 ? 0 : "0 auto" }}>
×
150
              {" "}
151
              {/* To center element when there is only one */}
152
              <Card style={{ background: "#1a2530", padding: 10 }}>
153
                {step.length > 1 && (
×
154
                  <h5
155
                    className={classNames(Classes.HEADING, Classes.MONOSPACE_TEXT)}
156
                    style={{ marginTop: 0, marginBottom: 20, whiteSpace: "nowrap" }}
157
                  >
158
                    {t("structure")} {i + 1}
159
                  </h5>
160
                )}
161
                {elem}
162
              </Card>
163
            </div>
164
          ))}
165
        </div>
166
      ) : (
167
        <DataVisualizerDefaultText config={config} />
168
      )}
169
      {steps.length > 0 && (
2!
170
        <>
171
          <ButtonGroup>
172
            <Tooltip content={t("views.original")} position="top">
173
              <AnchorButton
174
                style={{
175
                  display: "flex",
176
                  flexDirection: "row",
177
                  alignItems: "center",
178
                  justifyContent: "center",
179
                }}
180
                onMouseUp={() => {
NEW
181
                  DataVisualizer.setMode("normal");
×
NEW
182
                  DataVisualizer.redraw();
×
NEW
183
                  onViewModeClick(currentStep);
×
184
                }}
185
              >
186
                <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
187
                  <Icon icon="grid-view" />
188
                  <Checkbox
189
                    checked={DataVisualizer.getNormalMode()}
190
                    style={{ marginTop: 7 }}
191
                    tabIndex={-1}
192
                    aria-hidden="true"
193
                  />
194
                </div>
195
              </AnchorButton>
196
            </Tooltip>
197
          </ButtonGroup>
198

199
          <Tooltip content={t("views.binaryTree")} position="top">
200
            <AnchorButton
201
              style={{
202
                display: "flex",
203
                flexDirection: "row",
204
                alignItems: "center",
205
                justifyContent: "center",
206
                marginLeft: 10,
207
              }}
208
              onMouseUp={() => {
NEW
209
                DataVisualizer.setMode("binTree");
×
NEW
210
                DataVisualizer.redraw();
×
NEW
211
                onViewModeClick(currentStep);
×
212
              }}
213
            >
214
              <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
215
                <Icon icon="one-to-many" style={{ transform: "rotate(90deg)", marginLeft: 6 }} />
216
                <Checkbox
217
                  checked={DataVisualizer.getBinTreeMode()}
218
                  style={{ marginTop: 7 }}
219
                  tabIndex={-1}
220
                  aria-hidden="true"
221
                />
222
              </div>
223
            </AnchorButton>
224
          </Tooltip>
225
          <Tooltip content={t("views.generalTree")} position="top">
226
            <AnchorButton
227
              style={{
228
                display: "flex",
229
                flexDirection: "row",
230
                alignItems: "center",
231
                justifyContent: "center",
232
                marginLeft: 10,
233
              }}
234
              onMouseUp={() => {
NEW
235
                DataVisualizer.setMode("tree");
×
NEW
236
                DataVisualizer.redraw();
×
NEW
237
                onViewModeClick(currentStep);
×
238
              }}
239
            >
240
              <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
241
                <Icon icon="diagram-tree" />
242
                <Checkbox
243
                  checked={DataVisualizer.getTreeMode()}
244
                  style={{ marginTop: 7 }}
245
                  tabIndex={-1}
246
                  aria-hidden="true"
247
                />
248
              </div>
249
            </AnchorButton>
250
          </Tooltip>
251
        </>
252
      )}
253
    </div>
254
  );
255
}
256

257
const makeDataVisualizerTabFrom = (location: string, config: Config): Tab => ({
1✔
258
  label: i18n.t("label"),
259
  iconName: IconNames.EYE_OPEN,
260
  body: <SideContentDataVisualizer workspaceLocation={location} config={config} />,
261
  id: "dataviz",
262
});
263

264
function parseFunctionCallText(functionCallText: string) {
265
  const parts = functionCallText.split(/(x(?:\d+|n))/g);
2✔
266
  const parsedParts = parts.map((part, index) => {
2✔
267
    if (index % 2 === 1) {
14✔
268
      return (
6✔
269
        <React.Fragment key={index}>
270
          x<sub>{part.slice(1)}</sub>
271
        </React.Fragment>
272
      );
273
    }
274
    return part;
8✔
275
  });
276
  return <>{...parsedParts}</>;
2✔
277
}
278

279
function DataVisualizerDefaultText({ config }: { config: Config }) {
280
  const { t } = useTranslation();
2✔
281
  return (
2✔
282
    <p id="data-visualizer-default-text" className={Classes.RUNNING_TEXT}>
283
      {t("defaultText")}
284
      <br />
285
      <br />
286
      <Trans
287
        i18nKey={"instructions"}
288
        components={[
289
          <code>{parseFunctionCallText(config.functionCallText)}</code>,
290

291
          <code>
292
            x<sub>k</sub>
293
          </code>,
294

295
          <code>
296
            k<sup>th</sup>
297
          </code>,
298

299
          <code>n</code>,
300
        ]}
301
      />
302
      <br />
303
      <br />
304
      <Trans
305
        i18nKey={"reference"}
306
        components={[
307
          <ItalicLink href={config.sicpTextbookUrl}>{config.sicpTextbookName}</ItalicLink>,
308
        ]}
309
      />
310
    </p>
311
  );
312
}
313

314
export default makeDataVisualizerTabFrom;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc