Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(npm run build:*)",
"Bash(npm test:*)"
]
}
}
129 changes: 86 additions & 43 deletions src/command/describe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
/**
* Abstract base class for describe commands.
*/
abstract class DescribeCommand implements Command {
abstract class AnnounceCommand implements Command {
protected readonly context: Context;
protected readonly textViewModel: TextViewModel;
protected readonly audioService: AudioService;
protected readonly textService: TextService;

/**
* Creates an instance of DescribeCommand.
* Creates an instance of AnnounceCommand.
* @param {Context} context - The application context.
* @param {TextViewModel} textViewModel - The text view model.
* @param {AudioService} audioService - The audio service.
Expand Down Expand Up @@ -54,9 +54,9 @@
/**
* Command to describe the X-axis label.
*/
export class DescribeXCommand extends DescribeCommand {
export class AnnounceXCommand extends AnnounceCommand {
/**
* Creates an instance of DescribeXCommand.
* Creates an instance of AnnounceXCommand.
* @param {Context} context - The application context.
* @param {TextViewModel} textViewModel - The text view model.
* @param {AudioService} audioService - The audio service.
Expand Down Expand Up @@ -90,9 +90,9 @@
/**
* Command to describe the Y-axis label.
*/
export class DescribeYCommand extends DescribeCommand {
export class AnnounceYCommand extends AnnounceCommand {
/**
* Creates an instance of DescribeYCommand.
* Creates an instance of AnnounceYCommand.
* @param {Context} context - The application context.
* @param {TextViewModel} textViewModel - The text view model.
* @param {AudioService} audioService - The audio service.
Expand Down Expand Up @@ -126,9 +126,9 @@
/**
* Command to describe the fill property.
*/
export class DescribeFillCommand extends DescribeCommand {
export class AnnounceFillCommand extends AnnounceCommand {
/**
* Creates an instance of DescribeFillCommand.
* Creates an instance of AnnounceFillCommand.
* @param {Context} context - The application context.
* @param {TextViewModel} textViewModel - The text view model.
* @param {AudioService} audioService - The audio service.
Expand Down Expand Up @@ -162,9 +162,9 @@
/**
* Command to describe the title of the figure or subplot.
*/
export class DescribeTitleCommand extends DescribeCommand {
export class AnnounceTitleCommand extends AnnounceCommand {
/**
* Creates an instance of DescribeTitleCommand.
* Creates an instance of AnnounceTitleCommand.
* @param {Context} context - The application context.
* @param {TextViewModel} textViewModel - The text view model.
* @param {AudioService} audioService - The audio service.
Expand Down Expand Up @@ -253,9 +253,9 @@
/**
* Command to describe the subtitle of the figure.
*/
export class DescribeSubtitleCommand extends DescribeCommand {
export class AnnounceSubtitleCommand extends AnnounceCommand {
/**
* Creates an instance of DescribeSubtitleCommand.
* Creates an instance of AnnounceSubtitleCommand.
* @param {Context} context - The application context.
* @param {TextViewModel} textViewModel - The text view model.
* @param {AudioService} audioService - The audio service.
Expand Down Expand Up @@ -291,9 +291,9 @@
/**
* Command to describe the caption of the figure.
*/
export class DescribeCaptionCommand extends DescribeCommand {
export class AnnounceCaptionCommand extends AnnounceCommand {
/**
* Creates an instance of DescribeCaptionCommand.
* Creates an instance of AnnounceCaptionCommand.
* @param {Context} context - The application context.
* @param {TextViewModel} textViewModel - The text view model.
* @param {AudioService} audioService - The audio service.
Expand Down Expand Up @@ -329,13 +329,13 @@
/**
* Command to describe the current point with audio, braille, and highlight.
*/
export class DescribePointCommand extends DescribeCommand {
export class AnnouncePointCommand extends AnnounceCommand {
private readonly audio: AudioService;
private readonly brailleViewModel: BrailleViewModel;
private readonly highlight: HighlightService;

/**
* Creates an instance of DescribePointCommand.
* Creates an instance of AnnouncePointCommand.
* @param {Context} context - The application context.
* @param {AudioService} audioService - The audio service.
* @param {HighlightService} highlightService - The highlight service.
Expand Down Expand Up @@ -382,7 +382,7 @@
* Command to announce the current position in the chart.
* Formats output based on text mode (terse/verbose) and chart type.
*/
export class AnnouncePositionCommand extends DescribeCommand {
export class AnnouncePositionCommand extends AnnounceCommand {
/**
* Creates an instance of AnnouncePositionCommand.
* @param {Context} context - The application context.
Expand All @@ -403,11 +403,6 @@
* Executes the command to announce the current position.
*/
public execute(): void {
// Check if speech is off
if (this.textService.isOff()) {
return;
}

// Get current state
const state = this.context.state;

Expand All @@ -417,6 +412,11 @@
return;
}

// Warn if text mode is off instead of announcing position
if (this.textViewModel.warnIfTextOff()) {
return;
}

// Get position from audio.panning (contains x, y, rows, cols)
const { panning } = state.audio;
const { x, y, rows, cols } = panning;
Expand All @@ -435,12 +435,25 @@
) {
this.announceSegmentedBarPosition(state, x, cols);
} else if (traceType === TraceType.SMOOTH) {
// Violin KDE plots: y=violin index, x=position within violin
this.announceViolinPosition(y, rows, x, cols);
} else if (traceType === TraceType.LINE && state.groupCount && state.groupCount > 1) {
// Multi-line plots: x=line index, y=position within line
this.announceMultiLinePosition(x, rows, y, cols);
} else if (this.is2DPlot(rows, cols)) {
if (rows > 1) {
// Multi-violin plots: y=violin index, x=position within violin
this.announceMultiViolinPosition(y, rows, x, cols);
} else {
// Single smooth/violin plot: 1D position within the curve
this.announceSmoothPosition(x, cols);
}
}

Check failure on line 445 in src/command/describe.ts

View workflow job for this annotation

GitHub Actions / Lint Code

Closing curly brace does not appear on the same line as the subsequent block
// Check for multi plots (multiline, panel, layer, facet)
else if (traceType === TraceType.LINE && state.groupCount && state.groupCount > 1) {
// Multi-line plots: x=position in the line, y=line index
this.announceMultiLinePosition(x, cols, y, rows);
}

Check failure on line 450 in src/command/describe.ts

View workflow job for this annotation

GitHub Actions / Lint Code

Closing curly brace does not appear on the same line as the subsequent block
else if (traceType === TraceType.SCATTER) {
// Scatter plot: use x/y for column/row position, but don't include 'Position' as it sounds weird
this.announceScatter(x, y, rows, cols);
}

Check failure on line 454 in src/command/describe.ts

View workflow job for this annotation

GitHub Actions / Lint Code

Closing curly brace does not appear on the same line as the subsequent block
// Default position announcement
else if (this.is2DPlot(rows, cols)) {
this.announce2DPosition(x, y, rows, cols);
} else {
this.announce1DPosition(x, cols);
Expand All @@ -461,7 +474,7 @@
const position = x + 1;
const total = cols;

if (this.textService.isTerse()) {
if (this.textService.isTerse() || this.textService.isOff()) {
const percent = cols > 1 ? Math.round((x / (cols - 1)) * 100) : 0;
this.textViewModel.update(`${percent}%`);
} else {
Expand All @@ -476,7 +489,7 @@
const colPos = x + 1;
const rowPos = y + 1;

if (this.textService.isTerse()) {
if (this.textService.isTerse() || this.textService.isOff()) {
const colPercent = cols > 1 ? Math.round((x / (cols - 1)) * 100) : 0;
const rowPercent = rows > 1 ? Math.round((y / (rows - 1)) * 100) : 0;
this.textViewModel.update(`${colPercent}%, ${rowPercent}%`);
Expand All @@ -501,7 +514,7 @@
const totalBoxes = braille.values.length;
const position = boxIndex + 1;

if (this.textService.isTerse()) {
if (this.textService.isTerse() || this.textService.isOff()) {
const percent = totalBoxes > 1 ? Math.round((boxIndex / (totalBoxes - 1)) * 100) : 0;
this.textViewModel.update(`${percent}%, ${section.toLowerCase()}`);
} else {
Expand All @@ -523,7 +536,7 @@
const totalCandles = braille.values[0].length;
const position = candleIndex + 1;

if (this.textService.isTerse()) {
if (this.textService.isTerse() || this.textService.isOff()) {
const percent = totalCandles > 1 ? Math.round((candleIndex / (totalCandles - 1)) * 100) : 0;
this.textViewModel.update(`${percent}%, ${section.toLowerCase()}`);
} else {
Expand All @@ -540,7 +553,7 @@
const position = x + 1;
const total = cols;

if (this.textService.isTerse()) {
if (this.textService.isTerse() || this.textService.isOff()) {
const percent = cols > 1 ? Math.round((x / (cols - 1)) * 100) : 0;
this.textViewModel.update(`${percent}%, ${level}`);
} else {
Expand All @@ -549,24 +562,36 @@
}

/**
* Announces position for violin plots.
* Shows which violin (row) and position within that violin (col).
* Announces position for smooth/violin plots.
* Treats as 1D plot - only announces position within the curve.
*/
private announceViolinPosition(
private announceSmoothPosition(
posIndex: number,
totalPos: number,
): void {
// Smooth plots are 1D - just use position within the curve
this.announce1DPosition(posIndex, totalPos);
}

/**
* Announces position for multi-violin plots.
* Shows which violin and position within that violin.
*/
private announceMultiViolinPosition(
violinIndex: number,
totalViolins: number,
posIndex: number,
totalPos: number,
): void {
const violinPos = violinIndex + 1;
const pos = posIndex + 1;
const violinPrefix = `Violin ${violinPos} of ${totalViolins}`;

if (this.textService.isTerse()) {
const violinPercent = totalViolins > 1 ? Math.round((violinIndex / (totalViolins - 1)) * 100) : 0;
if (this.textService.isTerse() || this.textService.isOff()) {
const posPercent = totalPos > 1 ? Math.round((posIndex / (totalPos - 1)) * 100) : 0;
this.textViewModel.update(`${violinPercent}%, ${posPercent}%`);
this.textViewModel.update(`${violinPrefix}, ${posPercent}%`);
} else {
this.textViewModel.update(`Position is ${violinPos} of ${totalViolins}, ${pos} of ${totalPos}`);
this.textViewModel.update(`${violinPrefix}, Position is ${pos} of ${totalPos}`);
}
}

Expand All @@ -575,20 +600,38 @@
* Always shows "Plot X of Y" prefix, followed by position within the line.
*/
private announceMultiLinePosition(
lineIndex: number,
totalLines: number,
posIndex: number,
totalPos: number,
lineIndex: number,
totalLines: number,
): void {
const linePos = lineIndex + 1;
const pos = posIndex + 1;
const plotPrefix = `Plot ${linePos} of ${totalLines}`;

if (this.textService.isTerse()) {
if (this.textService.isTerse() || this.textService.isOff()) {
const posPercent = totalPos > 1 ? Math.round((posIndex / (totalPos - 1)) * 100) : 0;
this.textViewModel.update(`${plotPrefix}, ${posPercent}%`);
} else {
this.textViewModel.update(`${plotPrefix}, Position is ${pos} of ${totalPos}`);
}
}
/**
* Announces position for 2D plots (e.g., heatmaps).
*/
private announceScatter(x: number, y: number, rows: number, cols: number): void {

Check failure on line 622 in src/command/describe.ts

View workflow job for this annotation

GitHub Actions / Lint Code

Expected blank line between class members
const colPos = x + 1;
const rowPos = y + 1;

if (this.textService.isTerse() || this.textService.isOff()) {
const colPercent = cols > 1 ? Math.round((x / (cols - 1)) * 100) : 0;
const rowPercent = rows > 1 ? Math.round((y / (rows - 1)) * 100) : 0;
this.textViewModel.update(`${colPercent}%, ${rowPercent}%`);
} else {
this.textViewModel.update(
`Column ${colPos} of ${cols}, row ${rowPos} of ${rows}`,
);
}
}

Check failure on line 635 in src/command/describe.ts

View workflow job for this annotation

GitHub Actions / Lint Code

Block must not be padded by blank lines

}
45 changes: 23 additions & 22 deletions src/command/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@
} from './autoplay';
import {
AnnouncePositionCommand,
DescribeCaptionCommand,
DescribeFillCommand,
DescribePointCommand,
DescribeSubtitleCommand,
DescribeTitleCommand,
DescribeXCommand,
DescribeYCommand,
AnnounceCaptionCommand,

Check failure on line 32 in src/command/factory.ts

View workflow job for this annotation

GitHub Actions / Lint Code

Expected "AnnounceCaptionCommand" to come before "AnnouncePositionCommand"
AnnounceFillCommand,
AnnouncePointCommand,
AnnounceSubtitleCommand,
AnnounceTitleCommand,
AnnounceXCommand as AnnounceXCommand,

Check failure on line 37 in src/command/factory.ts

View workflow job for this annotation

GitHub Actions / Lint Code

Import AnnounceXCommand unnecessarily renamed
AnnounceYCommand,
} from './describe';
import { GoToExtremaToggleCommand } from './goTo';
import {
Expand Down Expand Up @@ -225,27 +225,27 @@
return new CommandPaletteSelectCommand(this.commandPaletteViewModel);
case 'COMMAND_PALETTE_CLOSE':
return new CommandPaletteCloseCommand(this.commandPaletteViewModel);
case 'DESCRIBE_X':
return new DescribeXCommand(this.context, this.textViewModel, this.audioService, this.textService);
case 'DESCRIBE_Y':
return new DescribeYCommand(this.context, this.textViewModel, this.audioService, this.textService);
case 'DESCRIBE_FILL':
return new DescribeFillCommand(this.context, this.textViewModel, this.audioService, this.textService);
case 'DESCRIBE_POINT':
return new DescribePointCommand(
case 'ANNOUNCE_X':
return new AnnounceXCommand(this.context, this.textViewModel, this.audioService, this.textService);
case 'ANNOUNCE_Y':
return new AnnounceYCommand(this.context, this.textViewModel, this.audioService, this.textService);
case 'ANNOUNCE_FILL':
return new AnnounceFillCommand(this.context, this.textViewModel, this.audioService, this.textService);
case 'ANNOUNCE_POINT':
return new AnnouncePointCommand(
this.context,
this.audioService,
this.highlightService,
this.brailleViewModel,
this.textViewModel,
this.textService,
);
case 'DESCRIBE_TITLE':
return new DescribeTitleCommand(this.context, this.textViewModel, this.audioService, this.textService);
case 'DESCRIBE_SUBTITLE':
return new DescribeSubtitleCommand(this.context, this.textViewModel, this.audioService, this.textService);
case 'DESCRIBE_CAPTION':
return new DescribeCaptionCommand(this.context, this.textViewModel, this.audioService, this.textService);
case 'ANNOUNCE_TITLE':
return new AnnounceTitleCommand(this.context, this.textViewModel, this.audioService, this.textService);
case 'ANNOUNCE_SUBTITLE':
return new AnnounceSubtitleCommand(this.context, this.textViewModel, this.audioService, this.textService);
case 'ANNOUNCE_CAPTION':
return new AnnounceCaptionCommand(this.context, this.textViewModel, this.audioService, this.textService);
case 'ANNOUNCE_POSITION':
return new AnnouncePositionCommand(
this.context,
Expand All @@ -255,10 +255,11 @@
);

case 'ACTIVATE_FIGURE_LABEL_SCOPE':
return new ToggleScopeCommand(this.context, Scope.FIGURE_LABEL, this.textViewModel);
case 'DEACTIVATE_FIGURE_LABEL_SCOPE':
return new ToggleScopeCommand(this.context, Scope.FIGURE_LABEL);
case 'ACTIVATE_TRACE_LABEL_SCOPE':
return new ToggleScopeCommand(this.context, Scope.TRACE_LABEL);
return new ToggleScopeCommand(this.context, Scope.TRACE_LABEL, this.textViewModel);
case 'DEACTIVATE_TRACE_LABEL_SCOPE':
return new ToggleScopeCommand(this.context, Scope.TRACE);
case 'AUTOPLAY_UPWARD':
Expand Down
Loading
Loading