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
249 changes: 54 additions & 195 deletions frontend/package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^1.6.0",
"chart.js": "^4.5.1",
"chartjs-plugin-zoom": "^2.2.0",
"cors": "^2.8.5",
"express": "^4.18.2",
"hammerjs": "^2.0.8",
"html2canvas": "^1.4.1",
"jszip": "^3.10.1",
"multer": "^1.4.5-lts.1",
"npyjs": "^1.0.4",
"react": "^18.2.0",
"react-chartjs-2": "^5.3.1",
"react-dom": "^18.2.0",
"recharts": "^2.10.0"
},
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const Graph: React.FC<GraphProps> = ({ config, data }) => {
}

const renderContent = () => {
if (config.typeId === 'metric-pair-correlation') {
if (config.typeId === 'metric-pair-correlation-mono' || config.typeId === 'metric-pair-correlation-bi') {
return <ScatterGraph config={config} data={data} chartData={chartData} />;
}
if (config.typeId === 'metric-table') {
Expand Down
193 changes: 30 additions & 163 deletions frontend/src/components/GraphConfigurator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { FigureConfig, MetricDimensionality } from '../types';
import { getAvailableGraphTypes, getGraphType } from '../utils/graphTypes';
import { buildLanguageLabelMap, getDisplayLanguageLabel } from '../utils/languageLabels';
import { useLanguageFilters, lookupLanguageInfo } from '../utils/useLanguageFilters';
import LanguageFilterPanel from './LanguageFilterPanel';
import ScatterGraphControls from './scatter/ScatterGraphControls';
import './GraphConfigurator.css';

/** Check whether two string arrays contain the same set of values. */
Expand Down Expand Up @@ -66,6 +68,7 @@ const GraphConfigurator: React.FC<GraphConfiguratorProps> = ({
languages: [],
metrics: [],
trendlineMode: 'none',
trendlineUncertainty: 'none',
});
const [validationErrors, setValidationErrors] = useState<string[]>([]);

Expand Down Expand Up @@ -193,7 +196,7 @@ const GraphConfigurator: React.FC<GraphConfiguratorProps> = ({

// Defaults for metrics depend on graph type and constraints
let defaultMetrics: string[] = [];
if (newTypeId === 'metric-pair-correlation') {
if (newTypeId === 'metric-pair-correlation-mono' || newTypeId === 'metric-pair-correlation-bi') {
// Choose first for X, second for Y
if (filteredMetrics.length >= 2) {
defaultMetrics = [filteredMetrics[0], filteredMetrics[1]];
Expand Down Expand Up @@ -262,29 +265,6 @@ const GraphConfigurator: React.FC<GraphConfiguratorProps> = ({
validateConfig(newConfig);
};

// For metric pair correlation: separate X and Y axis selectors
const handleMetricXChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const metricX = e.target.value;
const metrics = [metricX, config.metrics?.[1] || ''].filter(Boolean);
const newConfig = { ...config, metrics };
setConfig(newConfig);
validateConfig(newConfig);
};
const handleMetricYChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const metricY = e.target.value;
const metrics = [config.metrics?.[0] || '', metricY].filter(Boolean);
const newConfig = { ...config, metrics };
setConfig(newConfig);
validateConfig(newConfig);
};

const handleGroupByChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const groupBy = e.target.value as 'tokenizer' | 'language' | 'family';
const newConfig = { ...config, groupBy };
setConfig(newConfig);
validateConfig(newConfig);
};

const handleLanguageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedOptions = Array.from(e.target.selectedOptions, (option) => option.value);
const newConfig = {
Expand Down Expand Up @@ -358,9 +338,11 @@ const GraphConfigurator: React.FC<GraphConfiguratorProps> = ({
filters: {},
groupBy: (cfg as any).groupBy || 'tokenizer',
trendlineMode,
trendlineUncertainty: (cfg as any).trendlineUncertainty ?? 'none',
// Keep boolean flag in sync for any legacy consumers
showTrendline: trendlineMode !== 'none',
sentenceRange: cfg.sentenceRange,
axisTransforms: (cfg as any).axisTransforms,
};

// Always propagate the current configuration to the active figure.
Expand Down Expand Up @@ -512,62 +494,17 @@ const GraphConfigurator: React.FC<GraphConfiguratorProps> = ({
</div>
</div>

{config.typeId === 'metric-pair-correlation' ? (
<div className="config-section">
<label>Metric X (X-axis):</label>
<select
value={config.metrics?.[0] || ''}
onChange={handleMetricXChange}
className="single-select"
disabled={filteredMetrics.length === 0}
>
<option value="">Select metric</option>
{filteredMetrics.map((m) => (
<option key={m} value={m}>
{m}{getMetricDimensionLabel(m)}
</option>
))}
</select>
<label>Metric Y (Y-axis):</label>
<select
value={config.metrics?.[1] || ''}
onChange={handleMetricYChange}
className="single-select"
disabled={filteredMetrics.length === 0}
>
<option value="">Select metric</option>
{filteredMetrics.map((m) => (
<option key={m} value={m}>
{m}{getMetricDimensionLabel(m)}
</option>
))}
</select>
<div style={{ marginTop: '8px' }}>
<label>Color by:</label>
<select value={config.groupBy || 'tokenizer'} onChange={handleGroupByChange} className="single-select">
<option value="tokenizer">Tokenizer</option>
<option value="language">Language</option>
<option value="family">Language family</option>
</select>
</div>
<div style={{ marginTop: '8px' }}>
<label>Trendline:</label>
<select
value={(config as any).trendlineMode || ((config as any).showTrendline ? 'global' : 'none')}
onChange={(e) => {
const mode = e.target.value as 'none' | 'global' | 'groups';
const newConfig = { ...config, trendlineMode: mode };
setConfig(newConfig);
validateConfig(newConfig);
}}
className="single-select"
>
<option value="none">None</option>
<option value="global">Global</option>
<option value="groups">Groups</option>
</select>
</div>
</div>
{(config.typeId === 'metric-pair-correlation-mono' || config.typeId === 'metric-pair-correlation-bi') ? (
<ScatterGraphControls
config={config}
filteredMetrics={filteredMetrics}
getMetricDimensionLabel={getMetricDimensionLabel}
onChange={(patch) => {
const newConfig = { ...config, ...patch };
setConfig(newConfig);
validateConfig(newConfig);
}}
/>
) : config.typeId !== 'metric-table' && config.typeId !== 'tokenized-text' && (
<div className="config-section">
<label>
Expand Down Expand Up @@ -689,89 +626,19 @@ const GraphConfigurator: React.FC<GraphConfiguratorProps> = ({
)}

{/* Language Filters section (moved after Languages selector) */}
<div className="config-section">
<div className="config-header-row">
<label>Language Filters:</label>
<button
type="button"
className="clear-filters-btn"
onClick={clearLanguageFilters}
>
Clear filters
</button>
</div>
<div className="filters-grid">
<div>
<span>Continent:</span>
<select value={langFilters.continent} onChange={(e) => setLangFilter('continent', e.target.value)}>
<option value="">(any)</option>
{allContinents.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
</div>
<div>
<span>Families:</span>
<select multiple value={langFilters.families} onChange={(e) => setLangFilter('families', Array.from(e.target.selectedOptions).map(o => o.value))} className="multi-select">
{allFamilies.map((f) => (
<option key={f} value={f}>{f}</option>
))}
</select>
</div>
<div>
<span>Fineweb2 keys:</span>
<select multiple value={langFilters.fineweb2} onChange={(e) => setLangFilter('fineweb2', Array.from(e.target.selectedOptions).map(o => o.value))} className="multi-select">
{allFineweb2Keys.map((k) => (
<option key={k} value={k}>{k}</option>
))}
</select>
</div>
<div>
<span>Glottocodes:</span>
<select multiple value={langFilters.glottocodes} onChange={(e) => setLangFilter('glottocodes', Array.from(e.target.selectedOptions).map(o => o.value))} className="multi-select">
{allGlottocodes.map((g) => (
<option key={g} value={g}>{g}</option>
))}
</select>
</div>
<div>
<span>Morphology:</span>
<select multiple value={langFilters.morphology} onChange={(e) => setLangFilter('morphology', Array.from(e.target.selectedOptions).map(o => o.value))} className="multi-select">
{allMorphology.map((m) => (
<option key={m} value={m}>{m}</option>
))}
</select>
</div>
<div>
<span>Tier:</span>
<select value={langFilters.tier} onChange={(e) => setLangFilter('tier', e.target.value)}>
<option value="">(any)</option>
{allTiers.map((t) => (
<option key={t} value={t}>{t}</option>
))}
</select>
</div>
<div>
<span>Speakers:</span>
<div style={{ display: 'flex', gap: '6px' }}>
<select value={langFilters.speakerOp} onChange={(e) => setLangFilter('speakerOp', e.target.value as any)} style={{ width: '70px' }}>
<option value=">=">≥</option>
<option value="<=">≤</option>
</select>
<input type="number" inputMode="numeric" min="0" step="any" value={langFilters.speakerVal} onChange={(e) => setLangFilter('speakerVal', e.target.value)} placeholder="threshold" />
</div>
</div>
</div>
<div style={{ marginTop: '6px' }}>
<label>
<input type="checkbox" checked={langFilters.locked} onChange={(e) => setLangFilter('locked', e.target.checked)} />
{' '}Lock filters (prevent auto-selection and auto-clearing)
</label>
</div>
<div className="selected-count">
{matchingLanguages.length} match / {availableLanguages.length} total
</div>
</div>
<LanguageFilterPanel
filters={langFilters}
setFilter={setLangFilter}
clearFilters={clearLanguageFilters}
allContinents={allContinents}
allFamilies={allFamilies}
allFineweb2Keys={allFineweb2Keys}
allGlottocodes={allGlottocodes}
allMorphology={allMorphology}
allTiers={allTiers}
matchingLanguages={matchingLanguages}
availableLanguages={availableLanguages}
/>
{/* Generate Figure button removed; figure updates automatically */}
</>
)}
Expand Down
Loading
Loading