Skip to content
Open
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
31 changes: 31 additions & 0 deletions ui/src/data-services/models/algorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,25 @@ import { Entity } from './entity'

export type ServerAlgorithm = any // TODO: Update this type

export interface ServerAlgorithmEvaluation {
id: number
occurrence_set: { id: number; name: string }
accuracy: number | null
accuracy_by_species: number | null
occurrences_scored: number
occurrences_skipped: number
species_scored: number
created_at: string
}

export interface AlgorithmEvaluation {
id: string
occurrenceSetName: string
accuracy?: number
accuracyBySpecies?: number
occurrencesScored: number
}

export class Algorithm extends Entity {
protected readonly _algorithm: ServerAlgorithm

Expand Down Expand Up @@ -43,4 +62,16 @@ export class Algorithm extends Entity {
? this._algorithm.category_count
: undefined
}

get evaluations(): AlgorithmEvaluation[] {
const rows: ServerAlgorithmEvaluation[] = this._algorithm.evaluations ?? []

return rows.map((row) => ({
id: `${row.id}`,
occurrenceSetName: row.occurrence_set.name,
accuracy: row.accuracy ?? undefined,
accuracyBySpecies: row.accuracy_by_species ?? undefined,
occurrencesScored: row.occurrences_scored,
}))
}
}
32 changes: 32 additions & 0 deletions ui/src/data-services/models/species-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,43 @@ import { ServerSpecies, Species } from './species'

export type ServerSpeciesDetails = ServerSpecies & any // TODO: Update this type

export interface ServerAlgorithmPerformance {
algorithm: { id: number; name: string; key: string; version: number }
occurrence_set: { id: number; name: string }
accuracy: number
occurrences_scored: number
correct: number
overall_accuracy: number | null
overall_accuracy_by_species: number | null
}

export interface AlgorithmPerformance {
algorithmId: string
algorithmName: string
accuracy: number
occurrencesScored: number
correct: number
occurrenceSetName: string
}

export class SpeciesDetails extends Species {
public constructor(species: ServerSpeciesDetails) {
super(species)
}

get algorithmPerformance(): AlgorithmPerformance[] {
const rows = this._species.algorithm_performance ?? []

return rows.map((row: ServerAlgorithmPerformance) => ({
algorithmId: `${row.algorithm.id}`,
algorithmName: row.algorithm.name,
accuracy: row.accuracy,
occurrencesScored: row.occurrences_scored,
correct: row.correct,
occurrenceSetName: row.occurrence_set.name,
}))
}

get commonNameLabel(): string | undefined {
return this._species.common_name_en ?? undefined
}
Expand Down
4 changes: 4 additions & 0 deletions ui/src/data-services/models/species.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ export class Species extends Taxon {
return this._species.occurrences_count ?? 0
}

get numTrainingCrops(): number | undefined {
return this._species.training_crops_ready ?? undefined
}

get numVerified(): number {
return this._species.verified_count ?? 0
}
Expand Down
31 changes: 31 additions & 0 deletions ui/src/data-services/models/taxa-list.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import { Entity, ServerEntity } from 'data-services/models/entity'

export type ServerTaxaListBestModel = {
id: number
name: string
accuracy: number | null
accuracy_by_species: number | null
occurrence_set: string
}

export interface TaxaListBestModel {
id: string
name: string
accuracyBySpecies?: number
occurrenceSetName: string
}

export type ServerTaxaList = ServerEntity & {
best_model: ServerTaxaListBestModel | null // Highest scoring algorithm on these taxa
projects: number[] // Array of project IDs
taxa: string // URL to taxa API endpoint (filtered by this taxa list)
taxa_count: number // Number of taxa in list
Expand All @@ -15,6 +31,21 @@ export class TaxaList extends Entity {
this._taxaList = taxaList
}

get bestModel(): TaxaListBestModel | undefined {
const model = this._taxaList.best_model

if (!model) {
return undefined
}

return {
id: `${model.id}`,
name: model.name,
accuracyBySpecies: model.accuracy_by_species ?? undefined,
occurrenceSetName: model.occurrence_set,
}
}

get taxaCount() {
return this._taxaList.taxa_count
}
Expand Down
1 change: 1 addition & 0 deletions ui/src/data-services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface FetchParams {
filters?: { field: string; value?: string; error?: string }[]
withCounts?: boolean
withExampleOccurrences?: boolean
withTrainingCropCounts?: boolean
}

export interface APIValidationError {
Expand Down
3 changes: 3 additions & 0 deletions ui/src/data-services/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const getFetchUrl = ({
if (params?.withExampleOccurrences) {
queryParams.with_example_occurrences = String(true)
}
if (params?.withTrainingCropCounts) {
queryParams.with_training_crop_counts = String(true)
}

const baseUrl = `${API_URL}/${collection}`
const queryString = new URLSearchParams(queryParams).toString()
Expand Down
8 changes: 8 additions & 0 deletions ui/src/pages/algorithm-details/algorithm-details-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useNavigate, useParams } from 'react-router-dom'
import { APP_ROUTES } from 'utils/constants'
import { getAppRoute } from 'utils/getAppRoute'
import { STRING, translate } from 'utils/language'
import { AlgorithmEvaluations } from './algorithm-evaluations'
import styles from './styles.module.scss'

export const AlgorithmDetailsDialog = ({ id }: { id: string }) => {
Expand Down Expand Up @@ -91,6 +92,13 @@ const AlgorithmDetailsContent = ({ algorithm }: { algorithm: Algorithm }) => (
/>
</FormRow>
</FormSection>
{algorithm.evaluations.length > 0 && (
<FormSection title={translate(STRING.EVALUATION)}>
<div className={styles.tableContainer}>
<AlgorithmEvaluations algorithm={algorithm} />
</div>
</FormSection>
)}
{algorithm.uri || algorithm.categoryMapURI ? (
<FormSection title={translate(STRING.EXTERNAL_RESOURCES)}>
<div className="flex flex-col items-start gap-3">
Expand Down
71 changes: 71 additions & 0 deletions ui/src/pages/algorithm-details/algorithm-evaluations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Algorithm, AlgorithmEvaluation } from 'data-services/models/algorithm'
import {
BasicTableCell,
Table,
TableBackgroundTheme,
TableColumn,
TextAlign,
} from 'nova-ui-kit'
import { STRING, translate } from 'utils/language'

const ACCURACY_DECIMALS = 2

const accuracyLabel = (accuracy?: number) =>
accuracy !== undefined
? accuracy.toFixed(ACCURACY_DECIMALS)
: translate(STRING.VALUE_NOT_AVAILABLE)

export const columns: TableColumn<AlgorithmEvaluation>[] = [
{
id: 'occurrence-set',
name: translate(STRING.FIELD_LABEL_EVALUATION_SET),
renderCell: (item: AlgorithmEvaluation) => (
<BasicTableCell
style={{ width: '150px', whiteSpace: 'normal' }}
value={item.occurrenceSetName}
/>
),
},
{
id: 'accuracy',
name: translate(STRING.FIELD_LABEL_ACCURACY),
styles: {
textAlign: TextAlign.Right,
},
renderCell: (item: AlgorithmEvaluation) => (
<BasicTableCell value={accuracyLabel(item.accuracy)} />
),
},
{
id: 'accuracy-by-species',
name: translate(STRING.FIELD_LABEL_ACCURACY_BY_SPECIES),
styles: {
textAlign: TextAlign.Right,
},
renderCell: (item: AlgorithmEvaluation) => (
<BasicTableCell value={accuracyLabel(item.accuracyBySpecies)} />
),
},
{
id: 'occurrences-scored',
name: translate(STRING.FIELD_LABEL_OCCURRENCES_SCORED),
styles: {
textAlign: TextAlign.Right,
},
renderCell: (item: AlgorithmEvaluation) => (
<BasicTableCell value={item.occurrencesScored} />
),
},
]

export const AlgorithmEvaluations = ({
algorithm,
}: {
algorithm: Algorithm
}) => (
<Table
backgroundTheme={TableBackgroundTheme.White}
items={algorithm.evaluations}
columns={columns}
/>
)
38 changes: 37 additions & 1 deletion ui/src/pages/species-details/species-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { TaxonDetails } from 'components/taxon-details/taxon-details'
import { Tag } from 'components/taxon-tags/tag'
import { TagsForm } from 'components/taxon-tags/tags-form'
import { useProjectDetails } from 'data-services/hooks/projects/useProjectDetails'
import { SpeciesDetails as Species } from 'data-services/models/species-details'
import {
AlgorithmPerformance,
SpeciesDetails as Species,
} from 'data-services/models/species-details'
import { ExternalLinkIcon, LockIcon } from 'lucide-react'
import {
Box,
Expand All @@ -31,6 +34,19 @@ export const TABS = {
CHARTS: 'charts',
}

const ACCURACY_DECIMALS = 2

const performanceLabel = ({
algorithmName,
accuracy,
correct,
occurrencesScored,
occurrenceSetName,
}: AlgorithmPerformance) =>
`${algorithmName}: ${accuracy.toFixed(
ACCURACY_DECIMALS
)} (${correct}/${occurrencesScored}) on ${occurrenceSetName}`

export const SpeciesDetails = ({
species,
selectedTab,
Expand Down Expand Up @@ -195,6 +211,26 @@ export const SpeciesDetails = ({
/>
</div>
</InfoBlockField>
<InfoBlockField
label={translate(STRING.ALGORITHMS_AND_PERFORMANCE)}
>
{species.algorithmPerformance.length ? (
species.algorithmPerformance.map((performance) => (
<InfoBlockFieldValue
key={performance.algorithmId}
value={performanceLabel(performance)}
to={getAppRoute({
to: APP_ROUTES.ALGORITHM_DETAILS({
projectId: projectId as string,
algorithmId: performance.algorithmId,
}),
})}
/>
))
) : (
<InfoBlockFieldValue value={undefined} />
)}
</InfoBlockField>
<InfoBlockField
className="no-print"
label={translate(STRING.EXTERNAL_RESOURCES)}
Expand Down
12 changes: 12 additions & 0 deletions ui/src/pages/species/species-columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,18 @@ export const columns: (project: {
</Link>
),
},
{
id: 'training-images-ready',
name: translate(STRING.FIELD_LABEL_TRAINING_IMAGES_READY),
styles: {
textAlign: TextAlign.Right,
},
renderCell: (item: Species) => (
<BasicTableCell
value={item.numTrainingCrops ?? translate(STRING.VALUE_NOT_AVAILABLE)}
/>
),
},
{
id: 'best-determination-score',
name: translate(STRING.FIELD_LABEL_BEST_SCORE),
Expand Down
2 changes: 2 additions & 0 deletions ui/src/pages/species/species.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const Species = () => {
'last-seen': true,
occurrences: true,
verified: true,
'training-images-ready': true,
'best-determination-score': true,
'created-at': false,
'updated-at': false,
Expand All @@ -65,6 +66,7 @@ export const Species = () => {
filters,
// This list renders the Example column and links Last-seen / Best-score to an occurrence.
withExampleOccurrences: true,
withTrainingCropCounts: columnSettings['training-images-ready'],
})
// Ordered example occurrences, one per taxon row that has one, so the modal's
// prev/next steps to the next taxon's example (rows without an example are skipped).
Expand Down
Loading
Loading