UNPKG

gazeplotter

Version:

Gazeplotter is a Svelte application for visualizing eye-tracking data.

263 lines (262 loc) 9.92 kB
import { PlotAxisBreaks } from '../PlotAxisBreaks/PlotAxisBreaks.ts'; import { IDENTIFIER_IS_AOI, IDENTIFIER_IS_OTHER_CATEGORY, IDENTIFIER_NOT_DEFINED, } from '../../../const/identifiers.ts'; import { getAois, getAoiVisibility, getNumberOfSegments, getParticipant, getParticipantEndTime, getSegment, getStimuli, hasStimulusAoiVisibility, } from '../../../stores/dataStore.js'; /** * Factory for data filling which is used to render scarf plot. */ export class ScarfPlotFillingFactoryS { HEIGHT_OF_X_AXIS = 20; // todo move to settings showTheseSegmentCategories = [0, 1]; heightOfBar = 20; heightOfNonFixation = 4; stimulusId; spaceAboveRect; spaceAboveLine = 2; aoiData; stimuliData; participants; timeline; stimuli; stylingAndLegend; chartHeight; settings; hasAtLeastOneAoiVisibility = false; get rectWrappedHeight() { return this.heightOfBar + this.spaceAboveRect * 2; } get lineWrappedHeight() { return this.heightOfNonFixation + this.spaceAboveLine; } get heightOfBarWrap() { const baseHeight = this.rectWrappedHeight; return this.showAoiVisibility ? baseHeight + this.lineWrappedHeight * this.aoiData.length : baseHeight; } get showAoiVisibility() { return (this.hasAtLeastOneAoiVisibility && this.settings.timeline !== 'ordinal'); } constructor(stimulusId, participantIds, axis, settings, participGap = 10) { this.stimulusId = stimulusId; this.hasAtLeastOneAoiVisibility = hasStimulusAoiVisibility(stimulusId); this.settings = settings; this.spaceAboveRect = participGap / 2; this.aoiData = getAois(stimulusId); this.stimuliData = getStimuli(); this.timeline = axis; const participants = []; for (let i = 0; i < participantIds.length; i++) { const participant = this.#prepareParticipant(participantIds[i]); if (participant !== null) participants.push(participant); } this.participants = participants; this.chartHeight = participants.length * this.heightOfBarWrap + this.HEIGHT_OF_X_AXIS; this.stimuli = this.#prepareStimuliList(); this.stylingAndLegend = this.#prepareStylingAndLegend(); } getFilling() { return { barHeight: this.heightOfBar, stimulusId: this.stimulusId, heightOfBarWrap: this.heightOfBarWrap, chartHeight: this.chartHeight, stimuli: this.stimuli, participants: this.participants, timeline: this.timeline, stylingAndLegend: this.stylingAndLegend, }; } #prepareStimuliList() { return this.stimuliData.map(stimulus => { return { id: stimulus.id, name: stimulus.displayedName, }; }); } #prepareStylingAndLegend() { const aoi = this.aoiData.map(aoi => { return { identifier: `${IDENTIFIER_IS_AOI}${aoi.id}`, name: aoi.displayedName, color: aoi.color, height: this.heightOfBar, heighOfLegendItem: this.heightOfBar, }; }); const stylingFixationButNoAoi = { identifier: `${IDENTIFIER_IS_AOI}${IDENTIFIER_NOT_DEFINED}`, name: 'No AOI hit', color: '#a6a6a6', height: this.heightOfBar, heighOfLegendItem: this.heightOfBar, }; aoi.push(stylingFixationButNoAoi); // TODO PŘEDĚLAT // for (let i = 1; i < this.showTheseSegmentCategories.length; i++) { // // // } const stylingSaccade = { identifier: `${IDENTIFIER_IS_OTHER_CATEGORY}${1}`, name: 'Saccade', color: '#555555', height: this.heightOfNonFixation, heighOfLegendItem: this.heightOfBar, }; const stylingOther = { identifier: `${IDENTIFIER_IS_OTHER_CATEGORY}${IDENTIFIER_NOT_DEFINED}`, name: 'Other', color: '#a6a6a6', height: this.heightOfNonFixation, heighOfLegendItem: this.heightOfBar, }; const category = []; category.push(stylingSaccade); category.push(stylingOther); return { visibility: this.#prepareVisibilityStyling(), aoi, category, }; } #prepareVisibilityStyling() { const iterateTo = this.aoiData.length; const response = []; if (!this.showAoiVisibility) return response; for (let i = 0; i < iterateTo; i++) { const currentAoi = this.aoiData[i]; const stylingBaseAoi = { identifier: `${IDENTIFIER_IS_AOI}${currentAoi.id}`, name: currentAoi.displayedName, color: currentAoi.color, height: this.heightOfNonFixation, heighOfLegendItem: this.heightOfBar, }; response.push(stylingBaseAoi); } return response; } #prepareParticipant(id) { const iterateTo = getNumberOfSegments(this.stimulusId, id); const sessionDuration = getParticipantEndTime(this.stimulusId, id); const label = getParticipant(id).displayedName; const width = this.settings.timeline === 'relative' ? '100%' : `${(sessionDuration / this.timeline.maxLabel) * 100}%`; const segments = []; for (let i = 0; i < iterateTo; i++) { segments.push(this.#prepareSegment(id, i, sessionDuration)); } // TODO napravit height // ?? dafak :D return { dynamicAoiVisibility: this.#prepareDynamicVisibility(id, sessionDuration), id, label, segments, width, }; } #prepareDynamicVisibility(participantId, sessionDuration) { const response = []; if (!this.showAoiVisibility) return response; for (let aoiIndex = 0; aoiIndex < this.aoiData.length; aoiIndex++) { const aoiId = this.aoiData[aoiIndex].id; const visibility = getAoiVisibility(this.stimulusId, aoiId, participantId); const visibilityContent = []; if (visibility !== null) { for (let i = 0; i < visibility.length; i = i + 2) { const start = visibility[i]; const end = visibility[i + 1]; const y = this.rectWrappedHeight + aoiIndex * this.lineWrappedHeight; visibilityContent.push(this.#getDynamicAoiVisibilityContent(start, end, y, sessionDuration, aoiId)); } } const visibilityObj = { content: visibilityContent, }; response.push(visibilityObj); } return response; } #getDynamicAoiVisibilityContent(start, end, y, sessionDuration, aoiId) { const x1 = `${(start / sessionDuration) * 100}%`; const x2 = `${(end / sessionDuration) * 100}%`; const identifier = `${IDENTIFIER_IS_AOI}${aoiId}`; return { x1, x2, y, identifier, }; } /** * * @param {int} participantId * @param {int} segmentId * @param sessionDuration total session duration [ms] for given participant */ #prepareSegment(participantId, segmentId, sessionDuration) { const segment = getSegment(this.stimulusId, participantId, segmentId); const isOrdinal = this.settings.timeline === 'ordinal'; const start = isOrdinal ? segmentId : segment.start; const end = isOrdinal ? segmentId + 1 : segment.end; const x = `${(start / sessionDuration) * 100}%`; const width = `${((end - start) / sessionDuration) * 100}%`; return { content: this.#prepareSegmentContents(segment, x, width) }; } #prepareSegmentContents(segment, x, width) { let aoiOrNotIdentifier = IDENTIFIER_IS_AOI; let typeDistinctionIdentifier = IDENTIFIER_NOT_DEFINED; const getIdentifier = () => { return aoiOrNotIdentifier + typeDistinctionIdentifier; }; if (segment.category.id !== 0) { const height = this.heightOfNonFixation; const y = this.spaceAboveRect + this.heightOfBar / 2 - height / 2; aoiOrNotIdentifier = IDENTIFIER_IS_OTHER_CATEGORY; if (this.showTheseSegmentCategories.includes(segment.category.id)) typeDistinctionIdentifier = segment.category.id.toString(); const nonFixationSegmentContent = { x, y, width, height, identifier: getIdentifier(), }; return [nonFixationSegmentContent]; } if (segment.aoi.length === 0) { const fixationWithoutAoiContent = { x, y: this.spaceAboveRect, width, height: this.heightOfBar, identifier: getIdentifier(), }; return [fixationWithoutAoiContent]; } const height = this.heightOfBar / segment.aoi.length; let yOfOneContent = this.spaceAboveRect; const result = []; for (const aoi of segment.aoi) { typeDistinctionIdentifier = aoi.id.toString(); const fixationWithAoiContent = { x, y: yOfOneContent, width, height, identifier: getIdentifier(), }; result.push(fixationWithAoiContent); yOfOneContent += height; } return result; } }