import { SyllogisticArgument, SyllogisticStatement } from '../../../types.js';
/**
* Enhanced Venn diagram visualizer for syllogistic logic
* Creates SVG visualizations for syllogistic arguments with proper shading and labels
*/
export class EnhancedVennDiagramVisualizer {
// SVG dimensions
private width = 600;
private height = 500;
private margin = 50;
// Circle parameters
private radius = 120;
/**
* Generate a Venn diagram for a syllogistic argument
* @param argument The syllogistic argument to visualize
* @returns SVG string representation of the Venn diagram
*/
generateVennDiagram(argument: SyllogisticArgument): string {
// Determine the syllogistic figure based on the argument
const figure = this.determineFigure(argument);
// Calculate the circle positions based on the figure
const circles = this.calculateCirclePositions(figure);
// Determine shading areas based on the premise types
const shading = this.determineShading(argument);
// Generate the SVG content
return this.generateSVG(circles, shading, argument);
}
/**
* Determine the syllogistic figure (1-4) based on the argument structure
* @param argument The syllogistic argument
* @returns The figure number (1-4)
*/
private determineFigure(argument: SyllogisticArgument): number {
const majorPremise = argument.majorPremise;
const minorPremise = argument.minorPremise;
// Find the middle term
const majorTerms = [majorPremise.subject, majorPremise.predicate];
const minorTerms = [minorPremise.subject, minorPremise.predicate];
const conclusionTerms = [argument.conclusion.subject, argument.conclusion.predicate];
// The middle term appears in both premises but not in the conclusion
const middleTerm = majorTerms.find(term =>
minorTerms.includes(term) && !conclusionTerms.includes(term)
);
if (!middleTerm) {
// Default to figure 1 if middle term cannot be determined
return 1;
}
// Figure is determined by the position of the middle term in the premises
if (majorPremise.subject === middleTerm && minorPremise.subject === middleTerm) {
return 4; // M-P, M-S
} else if (majorPremise.subject === middleTerm && minorPremise.predicate === middleTerm) {
return 3; // M-P, S-M
} else if (majorPremise.predicate === middleTerm && minorPremise.subject === middleTerm) {
return 1; // P-M, M-S
} else if (majorPremise.predicate === middleTerm && minorPremise.predicate === middleTerm) {
return 2; // P-M, S-M
}
// Default to figure 1 if pattern doesn't match any standard figure
return 1;
}
/**
* Calculate circle positions based on the syllogistic figure
* @param figure The syllogistic figure (1-4)
* @returns Object with circle positions and labels
*/
private calculateCirclePositions(figure: number): {
S: { x: number; y: number; r: number; label: string };
P: { x: number; y: number; r: number; label: string };
M: { x: number; y: number; r: number; label: string };
} {
// Base positions for the circles
const centerX = this.width / 2;
const centerY = this.height / 2;
const horizontalOffset = this.radius * 0.8;
const verticalOffset = this.radius * 0.6;
// Default positions (figure 1)
let S = { x: centerX - horizontalOffset, y: centerY + verticalOffset, r: this.radius, label: 'S' };
let P = { x: centerX + horizontalOffset, y: centerY + verticalOffset, r: this.radius, label: 'P' };
let M = { x: centerX, y: centerY - verticalOffset, r: this.radius, label: 'M' };
// Adjust positions based on figure
switch (figure) {
case 1: // P-M, M-S (traditional arrangement)
// Default positions are fine
break;
case 2: // P-M, S-M
// Swap S and P positions
S = { x: centerX + horizontalOffset, y: centerY + verticalOffset, r: this.radius, label: 'S' };
P = { x: centerX - horizontalOffset, y: centerY + verticalOffset, r: this.radius, label: 'P' };
break;
case 3: // M-P, S-M
// Move M to the top left, P to the top right, S to the bottom
M = { x: centerX - horizontalOffset, y: centerY - verticalOffset, r: this.radius, label: 'M' };
P = { x: centerX + horizontalOffset, y: centerY - verticalOffset, r: this.radius, label: 'P' };
S = { x: centerX, y: centerY + verticalOffset, r: this.radius, label: 'S' };
break;
case 4: // M-P, M-S
// Move M to the top, P to the bottom right, S to the bottom left
M = { x: centerX, y: centerY - verticalOffset, r: this.radius, label: 'M' };
P = { x: centerX + horizontalOffset, y: centerY + verticalOffset, r: this.radius, label: 'P' };
S = { x: centerX - horizontalOffset, y: centerY + verticalOffset, r: this.radius, label: 'S' };
break;
}
return { S, P, M };
}
/**
* Determine shading areas based on the premise types
* @param argument The syllogistic argument
* @returns Object indicating which regions should be shaded
*/
private determineShading(argument: SyllogisticArgument): {
S_only: boolean;
P_only: boolean;
M_only: boolean;
S_M_not_P: boolean;
S_P_not_M: boolean;
M_P_not_S: boolean;
S_M_P: boolean;
existentialMarkers: Array<{region: string; x: number; y: number}>;
} {
// Initialize with no shading
const shading = {
S_only: false, // S outside M and P
P_only: false, // P outside S and M
M_only: false, // M outside S and P
S_M_not_P: false, // S and M but not P
S_P_not_M: false, // S and P but not M
M_P_not_S: false, // M and P but not S
S_M_P: false, // S and M and P
existentialMarkers: [] as Array<{region: string; x: number; y: number}>
};
// Extract terms and their positions
const majorTerm = argument.conclusion.predicate;
const minorTerm = argument.conclusion.subject;
// Find the middle term (appears in both premises but not in conclusion)
const majorTerms = [argument.majorPremise.subject, argument.majorPremise.predicate];
const minorTerms = [argument.minorPremise.subject, argument.minorPremise.predicate];
const conclusionTerms = [minorTerm, majorTerm];
const middleTerm = majorTerms.find(term =>
minorTerms.includes(term) && !conclusionTerms.includes(term)
) || 'M';
// Apply shading based on the figure and statement types
const figure = this.determineFigure(argument);
// Apply shadings for the major premise
this.applyShadingForPremise(
shading,
argument.majorPremise,
majorTerm,
middleTerm,
figure,
'major'
);
// Apply shadings for the minor premise
this.applyShadingForPremise(
shading,
argument.minorPremise,
minorTerm,
middleTerm,
figure,
'minor'
);
return shading;
}
/**
* Apply shading rules based on a syllogistic premise
* @param shading The shading configuration to update
* @param premise The premise statement
* @param term The major or minor term
* @param middleTerm The middle term
* @param figure The syllogistic figure
* @param premiseType Whether this is the major or minor premise
*/
private applyShadingForPremise(
shading: {
S_only: boolean;
P_only: boolean;
M_only: boolean;
S_M_not_P: boolean;
S_P_not_M: boolean;
M_P_not_S: boolean;
S_M_P: boolean;
existentialMarkers: Array<{region: string; x: number; y: number}>;
},
premise: SyllogisticStatement,
term: string,
middleTerm: string,
figure: number,
premiseType: 'major' | 'minor'
): void {
// Determine which regions to shade based on the statement type
switch (premise.type) {
case 'A': // All X are Y
if (premiseType === 'major') {
// Major premise: All M are P (or All P are M depending on figure)
if (premise.subject === middleTerm) {
// All M are P - shade M outside P (M_only)
shading.M_only = true;
} else {
// All P are M - shade P outside M (P_only)
shading.P_only = true;
}
} else {
// Minor premise: All S are M (or All M are S depending on figure)
if (premise.subject === term) {
// All S are M - shade S outside M (S_only and S_P_not_M)
shading.S_only = true;
shading.S_P_not_M = true;
} else {
// All M are S - shade M outside S (M_only and M_P_not_S)
shading.M_only = true;
shading.M_P_not_S = true;
}
}
break;
case 'E': // No X are Y
if (premiseType === 'major') {
// Major premise: No M are P (or No P are M)
// Shade the intersection of M and P (M_P_not_S and S_M_P)
shading.M_P_not_S = true;
shading.S_M_P = true;
} else {
// Minor premise: No S are M (or No M are S)
// Shade the intersection of S and M (S_M_not_P and S_M_P)
shading.S_M_not_P = true;
shading.S_M_P = true;
}
break;
case 'I': // Some X are Y
if (premiseType === 'major') {
// Major premise: Some M are P (or Some P are M)
// Add existential marker to M∩P region
shading.existentialMarkers.push({
region: premise.subject === middleTerm ? 'M_P' : 'P_M',
x: 0, // Will be calculated when generating SVG
y: 0
});
} else {
// Minor premise: Some S are M (or Some M are S)
// Add existential marker to S∩M region
shading.existentialMarkers.push({
region: premise.subject === term ? 'S_M' : 'M_S',
x: 0, // Will be calculated when generating SVG
y: 0
});
}
break;
case 'O': // Some X are not Y
if (premiseType === 'major') {
// Major premise: Some M are not P (or Some P are not M)
// Add existential marker to M−P region
shading.existentialMarkers.push({
region: premise.subject === middleTerm ? 'M_only' : 'P_only',
x: 0, // Will be calculated when generating SVG
y: 0
});
} else {
// Minor premise: Some S are not M (or Some M are not S)
// Add existential marker to S−M region
shading.existentialMarkers.push({
region: premise.subject === term ? 'S_only' : 'M_only',
x: 0, // Will be calculated when generating SVG
y: 0
});
}
break;
}
}
/**
* Generate the SVG for the Venn diagram
* @param circles Circle positions and labels
* @param shading Shading configuration
* @param argument The syllogistic argument
* @returns SVG string representation
*/
private generateSVG(
circles: {
S: { x: number; y: number; r: number; label: string };
P: { x: number; y: number; r: number; label: string };
M: { x: number; y: number; r: number; label: string };
},
shading: {
S_only: boolean;
P_only: boolean;
M_only: boolean;
S_M_not_P: boolean;
S_P_not_M: boolean;
M_P_not_S: boolean;
S_M_P: boolean;
existentialMarkers: Array<{region: string; x: number; y: number}>;
},
argument: SyllogisticArgument
): string {
// Start the SVG content
let svgContent = `<svg width="${this.width}" height="${this.height}" xmlns="http://www.w3.org/2000/svg">`;
// Add definitions for clip paths and patterns
svgContent += this.generateSVGDefs(circles);
// Add background
svgContent += `<rect x="0" y="0" width="${this.width}" height="${this.height}" fill="#f8f8f8" />`;
// Add regions with appropriate shading
svgContent += this.generateShadedRegions(circles, shading);
// Add circle outlines
svgContent += `
<circle cx="${circles.S.x}" cy="${circles.S.y}" r="${circles.S.r}" fill="none" stroke="#333" stroke-width="2" />
<circle cx="${circles.P.x}" cy="${circles.P.y}" r="${circles.P.r}" fill="none" stroke="#333" stroke-width="2" />
<circle cx="${circles.M.x}" cy="${circles.M.y}" r="${circles.M.r}" fill="none" stroke="#333" stroke-width="2" />
`;
// Add labels for circles
svgContent += `
<text x="${circles.S.x}" y="${circles.S.y - circles.S.r - 10}"
text-anchor="middle" font-family="Arial" font-size="20">
${argument.conclusion.subject}
</text>
<text x="${circles.P.x}" y="${circles.P.y - circles.P.r - 10}"
text-anchor="middle" font-family="Arial" font-size="20">
${argument.conclusion.predicate}
</text>
`;
// Find and label the middle term
const majorTerms = [argument.majorPremise.subject, argument.majorPremise.predicate];
const minorTerms = [argument.minorPremise.subject, argument.minorPremise.predicate];
const conclusionTerms = [argument.conclusion.subject, argument.conclusion.predicate];
const middleTerm = majorTerms.find(term =>
minorTerms.includes(term) && !conclusionTerms.includes(term)
) || 'M';
svgContent += `
<text x="${circles.M.x}" y="${circles.M.y - circles.M.r - 10}"
text-anchor="middle" font-family="Arial" font-size="20">
${middleTerm}
</text>
`;
// Add existential markers if needed
svgContent += this.generateExistentialMarkers(circles, shading);
// Add title
const figure = this.determineFigure(argument);
const mood = argument.majorPremise.type + argument.minorPremise.type + argument.conclusion.type;
svgContent += `
<text x="${this.width / 2}" y="${this.margin / 2}"
text-anchor="middle" font-family="Arial" font-size="24" font-weight="bold">
Figure ${figure}, Mood ${mood}
</text>
`;
// Close the SVG
svgContent += '</svg>';
return svgContent;
}
/**
* Generate SVG definitions for clip paths
* @param circles Circle positions
* @returns SVG definitions string
*/
private generateSVGDefs(circles: {
S: { x: number; y: number; r: number };
P: { x: number; y: number; r: number };
M: { x: number; y: number; r: number };
}): string {
return `
<defs>
<clipPath id="S-circle">
<circle cx="${circles.S.x}" cy="${circles.S.y}" r="${circles.S.r}" />
</clipPath>
<clipPath id="P-circle">
<circle cx="${circles.P.x}" cy="${circles.P.y}" r="${circles.P.r}" />
</clipPath>
<clipPath id="M-circle">
<circle cx="${circles.M.x}" cy="${circles.M.y}" r="${circles.M.r}" />
</clipPath>
</defs>
`;
}
/**
* Generate shaded regions
* @param circles Circle positions
* @param shading Shading configuration
* @returns SVG string for shaded regions
*/
private generateShadedRegions(circles: any, shading: any): string {
let svg = '';
// Shade the requested regions
if (shading.S_only) {
svg += `<circle cx="${circles.S.x}" cy="${circles.S.y}" r="${circles.S.r}"
fill="#888" opacity="0.6" clip-path="url(#S-circle)" />`;
}
// Add more shading logic here...
return svg;
}
/**
* Generate existential markers
* @param circles Circle positions
* @param shading Shading configuration
* @returns SVG string for existential markers
*/
private generateExistentialMarkers(circles: any, shading: any): string {
let svg = '';
shading.existentialMarkers.forEach((marker: any) => {
let x = 0, y = 0;
// Calculate position based on region
switch (marker.region) {
case 'M_P':
x = (circles.M.x + circles.P.x) / 2;
y = (circles.M.y + circles.P.y) / 2;
break;
// Add cases for other regions...
}
// Draw the marker
svg += `<circle cx="${x}" cy="${y}" r="6" fill="#ff0000" />`;
});
return svg;
}
}