lodash-es#sampleSize TypeScript Examples

The following examples show how to use lodash-es#sampleSize. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: CategoryView.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
CategoryView = () => {
  const mdBreakpointMatch = useMediaMatch('md')
  const { id = '' } = useParams()

  const { categories, totalVideosCount, loading } = useCategories()

  const mappedVideoCategories = categories?.map((category) => ({
    ...videoCategories[category.id],
    ...category,
  }))
  const otherCategory: Array<VideoCategoryData & VideoCategoryFieldsFragment> = React.useMemo(
    () =>
      sampleSize(
        mappedVideoCategories?.filter((category) => category.id !== id),
        3
      ),
    [id, mappedVideoCategories]
  )
  const currentCategory = mappedVideoCategories?.find((category) => category.id === id)

  const headTags = useHeadTags(currentCategory?.name)

  const { categoriesFeaturedVideos } = useCategoriesFeaturedVideos(id)
  const videoHeroVideos = useVideoHeroVideos(categoriesFeaturedVideos)

  return (
    <VideoContentTemplate cta={['popular', 'new', 'home']}>
      {headTags}
      <VideoCategoryHero
        header={{
          title: currentCategory?.name ?? undefined,
          icon: currentCategory?.icon,
        }}
        videos={videoHeroVideos}
      />

      {!!categoriesFeaturedVideos?.length && (
        <>
          <TitleContainer>
            <Text variant="h500">Featured category videos</Text>
          </TitleContainer>
          <Grid>
            {categoriesFeaturedVideos?.map((featuredVideo, idx) => (
              <VideoTileViewer id={featuredVideo.video.id} key={idx} />
            ))}
          </Grid>
        </>
      )}

      <CategoryVideos categoryId={id} />

      <TitleContainer>
        <Text variant="h500">Other categories</Text>
        <Button
          icon={<SvgActionChevronR />}
          to={absoluteRoutes.viewer.discover()}
          iconPlacement="right"
          variant="secondary"
        >
          {mdBreakpointMatch ? 'Browse categories' : ''}
        </Button>
      </TitleContainer>
      <CategoriesContainer>
        {otherCategory.map((category) => (
          <GridItem key={category.id} colSpan={{ base: 6, lg: 4 }}>
            <VideoCategoryCard
              title={category.name ?? ''}
              isLoading={loading}
              coverImg={category.coverImg}
              color={category.color}
              categoryVideosCount={category.activeVideosCounter}
              icon={category.icon}
              videosTotalCount={totalVideosCount}
              variant={mdBreakpointMatch ? 'default' : 'compact'}
              id={category.id}
            />
          </GridItem>
        ))}
      </CategoriesContainer>
    </VideoContentTemplate>
  )
}
Example #2
Source File: higlass-csv-datafetcher.ts    From gosling.js with MIT License 4 votes vote down vote up
/**
 * HiGlass data fetcher specific for Gosling which ultimately will accept any types of data other than CSV files.
 */
function CSVDataFetcher(HGC: any, ...args: any): any {
    if (!new.target) {
        throw new Error('Uncaught TypeError: Class constructor cannot be invoked without "new"');
    }

    class CSVDataFetcherClass {
        // @ts-ignore
        private dataConfig: GeminiDataConfig;
        // @ts-ignore
        private tilesetInfoLoading: boolean;
        private dataPromise: Promise<any> | undefined;
        private chromSizes: any;
        private values: any;
        private assembly: Assembly;
        private filter: FilterTransform[] | undefined;

        constructor(params: any[]) {
            const [dataConfig] = params;
            this.dataConfig = dataConfig;
            this.tilesetInfoLoading = false;
            this.assembly = this.dataConfig.assembly;
            this.filter = this.dataConfig.filter;

            if (!dataConfig.url) {
                console.error('Please provide the `url` of the data');
                return;
            }

            // Prepare chromosome interval information
            const chromosomeSizes: { [k: string]: number } = GET_CHROM_SIZES(this.assembly).size;
            const chromosomeCumPositions: { id: number; chr: string; pos: number }[] = [];
            const chromosomePositions: { [k: string]: { id: number; chr: string; pos: number } } = {};
            let prevEndPosition = 0;

            Object.keys(GET_CHROM_SIZES(this.assembly).size).forEach((chrStr, i) => {
                const positionInfo = {
                    id: i,
                    chr: chrStr,
                    pos: prevEndPosition
                };

                chromosomeCumPositions.push(positionInfo);
                chromosomePositions[chrStr] = positionInfo;

                prevEndPosition += GET_CHROM_SIZES(this.assembly).size[chrStr];
            });
            this.chromSizes = {
                chrToAbs: (chrom: string, chromPos: number) => this.chromSizes.chrPositions[chrom].pos + chromPos,
                cumPositions: chromosomeCumPositions,
                chrPositions: chromosomePositions,
                totalLength: prevEndPosition,
                chromLengths: chromosomeSizes
            };

            if (dataConfig.data) {
                // we have raw data that we can use right away
                this.values = dataConfig.data;
            } else {
                this.dataPromise = this.fetchCSV();
            }
        }

        fetchCSV() {
            const {
                url,
                chromosomeField,
                genomicFields,
                headerNames,
                chromosomePrefix,
                longToWideId,
                genomicFieldsToConvert
            } = this.dataConfig;

            const separator = this.dataConfig.separator ?? ',';

            return fetch(url)
                .then(response => {
                    return response.ok ? response.text() : Promise.reject(response.status);
                })
                .then(text => {
                    const textWithHeader = headerNames ? `${headerNames.join(separator)}\n${text}` : text;
                    return d3dsvFormat(separator).parse(textWithHeader, (row: any) => {
                        let successfullyGotChrInfo = true;

                        // !!! Experimental
                        if (genomicFieldsToConvert) {
                            // This spec is used when multiple chromosomes are stored in a single row
                            genomicFieldsToConvert.forEach((d: any) => {
                                const cField = d.chromosomeField;
                                d.genomicFields.forEach((g: string) => {
                                    try {
                                        if (this.assembly !== 'unknown') {
                                            // This means we need to use the relative position considering the start position of individual chr.
                                            const chr = chromosomePrefix
                                                ? row[cField].replace(chromosomePrefix, 'chr')
                                                : row[cField].includes('chr')
                                                ? row[cField]
                                                : `chr${row[cField]}`;
                                            row[g] = GET_CHROM_SIZES(this.assembly).interval[chr][0] + +row[g];
                                        } else {
                                            // In this case, we use the genomic position as it is w/o adding the cumulative length of chr.
                                            // So, nothing to do additionally.
                                        }
                                    } catch (e) {
                                        // genomic position did not parse properly
                                        successfullyGotChrInfo = false;
                                    }
                                });
                            });
                        } else {
                            genomicFields.forEach((g: string) => {
                                if (!row[chromosomeField]) {
                                    // TODO:
                                    return;
                                }
                                try {
                                    const chr = chromosomePrefix
                                        ? row[chromosomeField].replace(chromosomePrefix, 'chr')
                                        : row[chromosomeField].includes('chr')
                                        ? row[chromosomeField]
                                        : `chr${row[chromosomeField]}`;
                                    row[g] = GET_CHROM_SIZES(this.assembly).interval[chr][0] + +row[g];
                                } catch (e) {
                                    // genomic position did not parse properly
                                    successfullyGotChrInfo = false;
                                }
                            });
                        }

                        if (!successfullyGotChrInfo) {
                            // store row only when chromosome information is correctly parsed
                            return undefined;
                        }

                        return row;
                    });
                })
                .then(json => {
                    if (longToWideId && json[0]?.[longToWideId]) {
                        // rows having identical IDs are juxtaposed horizontally
                        const keys = Object.keys(json[0]);
                        const newJson: { [k: string]: { [k: string]: string | number } } = {};
                        json.forEach(d => {
                            if (!newJson[d[longToWideId]]) {
                                newJson[d[longToWideId]] = JSON.parse(JSON.stringify(d));
                            } else {
                                keys.forEach(k => {
                                    newJson[d[longToWideId]][`${k}_2`] = d[k];
                                });
                            }
                        });
                        this.values = Object.keys(newJson).map(k => newJson[k]);
                    } else {
                        this.values = json;
                    }
                })
                .catch(error => {
                    console.error('[Gosling Data Fetcher] Error fetching data', error);
                });
        }

        generateTilesetInfo(callback?: any) {
            this.tilesetInfoLoading = false;

            const TILE_SIZE = 1024;
            const totalLength = this.chromSizes.totalLength;
            const retVal = {
                tile_size: TILE_SIZE,
                max_zoom: Math.ceil(Math.log(totalLength / TILE_SIZE) / Math.log(2)),
                max_width: totalLength,
                min_pos: [0, 0],
                max_pos: [totalLength, totalLength]
            };

            if (callback) {
                callback(retVal);
            }

            return retVal;
        }

        tilesetInfo(callback?: any) {
            if (!this.dataPromise) {
                // data promise is not prepared yet
                return;
            }

            this.tilesetInfoLoading = true;

            return this.dataPromise
                .then(() => this.generateTilesetInfo(callback))
                .catch(err => {
                    this.tilesetInfoLoading = false;
                    console.error('[Gosling Data Fetcher] Error parsing data:', err);
                });
        }

        fetchTilesDebounced(receivedTiles: any, tileIds: any) {
            const tiles: { [k: string]: any } = {};

            const validTileIds: any[] = [];
            const tilePromises = [];

            for (const tileId of tileIds) {
                const parts = tileId.split('.');
                const z = parseInt(parts[0], 10);
                const x = parseInt(parts[1], 10);
                const y = parseInt(parts[2], 10);

                if (Number.isNaN(x) || Number.isNaN(z)) {
                    console.warn('[Gosling Data Fetcher] Invalid tile zoom or position:', z, x, y);
                    continue;
                }

                validTileIds.push(tileId);
                tilePromises.push(this.tile(z, x, y));
            }

            Promise.all(tilePromises).then(values => {
                values.forEach((value, i) => {
                    const validTileId = validTileIds[i];
                    tiles[validTileId] = value;
                    tiles[validTileId].tilePositionId = validTileId;
                });
                receivedTiles(tiles);
            });

            return tiles;
        }

        tile(z: any, x: any, y: any) {
            return this.tilesetInfo()?.then((tsInfo: any) => {
                const tileWidth = +tsInfo.max_width / 2 ** +z;

                // get the bounds of the tile
                const minX = tsInfo.min_pos[0] + x * tileWidth;
                const maxX = tsInfo.min_pos[0] + (x + 1) * tileWidth;

                // filter the data so that only the visible data is sent to tracks
                let tabularData = this.values.filter((d: any) => {
                    if (this.dataConfig.genomicFields) {
                        return this.dataConfig.genomicFields.find((g: any) => minX < d[g] && d[g] <= maxX);
                    } else {
                        const allGenomicFields: string[] = [];
                        this.dataConfig.genomicFieldsToConvert.forEach((d: any) =>
                            allGenomicFields.push(...d.genomicFields)
                        );
                        return allGenomicFields.find((g: any) => minX < d[g] && d[g] <= maxX);
                    }
                });

                // filter data based on the `DataTransform` spec
                this.filter?.forEach(f => {
                    tabularData = filterData(f, tabularData);
                });

                const sizeLimit = this.dataConfig.sampleLength ?? 1000;
                return {
                    // sample the data to make it managable for visualization components
                    tabularData: tabularData.length > sizeLimit ? sampleSize(tabularData, sizeLimit) : tabularData,
                    server: null,
                    tilePos: [x, y],
                    zoomLevel: z
                };
            });
        }
    }

    return new CSVDataFetcherClass(args);
}
Example #3
Source File: higlass-raw-datafetcher.ts    From gosling.js with MIT License 4 votes vote down vote up
/**
 * HiGlass data fetcher specific for Gosling which ultimately will accept any types of data other than CSV files.
 */
function RawDataFetcher(HGC: any, ...args: any): any {
    if (!new.target) {
        throw new Error('Uncaught TypeError: Class constructor cannot be invoked without "new"');
    }

    class RawDataFetcherClass {
        // @ts-ignore
        private dataConfig: GeminiDataConfig;
        // @ts-ignore
        private tilesetInfoLoading: boolean;
        private chromSizes: any;
        private values: any;
        private assembly: string;

        constructor(params: any[]) {
            const [dataConfig] = params;
            this.dataConfig = dataConfig;
            this.tilesetInfoLoading = false;
            this.assembly = this.dataConfig.assembly;

            if (!dataConfig.values) {
                console.error('Please provide `values` of the raw data');
                return;
            }

            // Prepare chromosome interval information
            const chromosomeSizes: { [k: string]: number } = GET_CHROM_SIZES(this.assembly).size;
            const chromosomeCumPositions: { id: number; chr: string; pos: number }[] = [];
            const chromosomePositions: { [k: string]: { id: number; chr: string; pos: number } } = {};
            let prevEndPosition = 0;

            Object.keys(GET_CHROM_SIZES(this.assembly).size).forEach((chrStr, i) => {
                const positionInfo = {
                    id: i,
                    chr: chrStr,
                    pos: prevEndPosition
                };

                chromosomeCumPositions.push(positionInfo);
                chromosomePositions[chrStr] = positionInfo;

                prevEndPosition += GET_CHROM_SIZES(this.assembly).size[chrStr];
            });
            this.chromSizes = {
                chrToAbs: (chrom: string, chromPos: number) => this.chromSizes.chrPositions[chrom].pos + chromPos,
                cumPositions: chromosomeCumPositions,
                chrPositions: chromosomePositions,
                totalLength: prevEndPosition,
                chromLengths: chromosomeSizes
            };

            this.values = dataConfig.values.map((row: any) => {
                let successfullyGotChrInfo = true;

                this.dataConfig.genomicFields.forEach((g: any) => {
                    if (!row[this.dataConfig.chromosomeField]) {
                        // TODO:
                        return;
                    }
                    try {
                        const chr = row[this.dataConfig.chromosomeField].includes('chr')
                            ? row[this.dataConfig.chromosomeField]
                            : `chr${row[this.dataConfig.chromosomeField]}`;
                        row[g] = GET_CHROM_SIZES(this.assembly).interval[chr][0] + +row[g];
                    } catch (e) {
                        // genomic position did not parse properly
                        successfullyGotChrInfo = false;
                        // console.warn(
                        //     '[Gosling Data Fetcher] Genomic position cannot be parsed correctly.',
                        //     this.dataConfig.chromosomeField
                        // );
                    }
                });

                if (!successfullyGotChrInfo) {
                    // store row only when chromosome information is correctly parsed
                    return undefined;
                }

                return row;
            });
        }

        tilesetInfo(callback?: any) {
            this.tilesetInfoLoading = false;

            const TILE_SIZE = 1024;
            const totalLength = this.chromSizes.totalLength;
            const retVal = {
                tile_size: TILE_SIZE,
                max_zoom: Math.ceil(Math.log(totalLength / TILE_SIZE) / Math.log(2)),
                max_width: totalLength,
                min_pos: [0, 0],
                max_pos: [totalLength, totalLength]
            };

            if (callback) {
                callback(retVal);
            }

            return retVal;
        }

        fetchTilesDebounced(receivedTiles: any, tileIds: any) {
            const tiles: { [k: string]: any } = {};

            const validTileIds: any[] = [];
            const tilePromises = [];

            for (const tileId of tileIds) {
                const parts = tileId.split('.');
                const z = parseInt(parts[0], 10);
                const x = parseInt(parts[1], 10);
                const y = parseInt(parts[2], 10);

                if (Number.isNaN(x) || Number.isNaN(z)) {
                    console.warn('[Gosling Data Fetcher] Invalid tile zoom or position:', z, x, y);
                    continue;
                }

                validTileIds.push(tileId);
                tilePromises.push(this.tile(z, x, y));
            }

            Promise.all(tilePromises).then(values => {
                values.forEach((value, i) => {
                    const validTileId = validTileIds[i];
                    tiles[validTileId] = value;
                    tiles[validTileId].tilePositionId = validTileId;
                });
                receivedTiles(tiles);
            });

            return tiles;
        }

        tile(z: any, x: any, y: any) {
            const tsInfo = this.tilesetInfo();
            const tileWidth = +tsInfo.max_width / 2 ** +z;

            // get the bounds of the tile
            const minX = tsInfo.min_pos[0] + x * tileWidth;
            const maxX = tsInfo.min_pos[0] + (x + 1) * tileWidth;

            // filter the data so that visible data is sent to tracks
            let tabularData = this.values;

            const sizeLimit = this.dataConfig.sampleLength ?? 1000;

            if (sizeLimit < tabularData.length) {
                tabularData = tabularData.filter((d: any) => {
                    return this.dataConfig.genomicFields.find((g: any) => minX < d[g] && d[g] <= maxX);
                });
            }

            // sample the data to make it managable for visualization components
            if (sizeLimit < tabularData.length) {
                tabularData = sampleSize(tabularData, sizeLimit);
            }

            return {
                tabularData,
                server: null,
                tilePos: [x, y],
                zoomLevel: z
            };
        }
    }

    return new RawDataFetcherClass(args);
}
Example #4
Source File: gosling-track.ts    From gosling.js with MIT License 4 votes vote down vote up
/**
 * Each GoslingTrack draws either a track of multiple tracks with SAME DATA that are overlaid.
 * @param HGC
 * @param args
 * @returns
 */
function GoslingTrack(HGC: any, ...args: any[]): any {
    if (!new.target) {
        throw new Error('Uncaught TypeError: Class constructor cannot be invoked without "new"');
    }

    // Services
    const { tileProxy } = HGC.services;
    const { showMousePosition } = HGC.utils;

    class GoslingTrackClass extends HGC.tracks.BarTrack {
        private viewUid: string;
        private options!: GoslingTrackOption;
        private tileSize: number;
        private worker: any;
        private isRangeBrushActivated: boolean;
        private mRangeBrush: LinearBrushModel;
        private _xScale!: ScaleLinear<number, number>;
        private _yScale!: ScaleLinear<number, number>;
        private pMouseHover: Graphics;
        private pMouseSelection: Graphics;
        // TODO: add members that are used explicitly in the code

        constructor(params: any[]) {
            const [context, options] = params;

            // Check whether to load a worker
            let dataWorker;
            if (usePrereleaseRendering(options.spec)) {
                try {
                    if (options.spec.data?.type === 'bam') {
                        dataWorker = spawn(new BamWorker());
                        context.dataFetcher = new BAMDataFetcher(HGC, context.dataConfig, dataWorker);
                    } else if (options.spec.data?.type === 'vcf') {
                        dataWorker = spawn(new VcfWorker());
                        context.dataFetcher = new GoslingVcfData(HGC, context.dataConfig, dataWorker);
                    }
                } catch (e) {
                    console.warn('Error loading worker', e);
                }
            }

            super(context, options);

            this.worker = dataWorker;
            context.dataFetcher.track = this;
            this.context = context;
            this.viewUid = context.viewUid;

            // Add unique IDs to each of the overlaid tracks that will be rendered independently.
            if ('overlay' in this.options.spec) {
                this.options.spec.overlay = (this.options.spec as OverlaidTrack).overlay.map(o => {
                    return { ...o, _renderingId: uuid.v1() };
                });
            } else {
                this.options.spec._renderingId = uuid.v1();
            }

            this.tileSize = this.tilesetInfo?.tile_size ?? 1024;

            // This is tracking the xScale of an entire view, which is used when no tiling concepts are used
            this.drawnAtScale = HGC.libraries.d3Scale.scaleLinear();
            this.scalableGraphics = {};

            const { valid, errorMessages } = validateTrack(this.options.spec);

            if (!valid) {
                console.warn('The specification of the following track is invalid', errorMessages, this.options.spec);
            }

            this.extent = { min: Number.MAX_SAFE_INTEGER, max: Number.MIN_SAFE_INTEGER };

            // Graphics for highlighting visual elements under the cursor
            this.pMouseHover = new HGC.libraries.PIXI.Graphics();
            this.pMouseSelection = new HGC.libraries.PIXI.Graphics();
            this.pMain.addChild(this.pMouseHover);
            this.pMain.addChild(this.pMouseSelection);

            // Brushes on the color legend
            this.gLegend = HGC.libraries.d3Selection.select(this.context.svgElement).append('g');

            // Enable click event
            this.isRangeBrushActivated = false;
            this.pMask.interactive = true;
            this.gBrush = HGC.libraries.d3Selection.select(this.context.svgElement).append('g');
            this.mRangeBrush = new LinearBrushModel(
                this.gBrush,
                HGC.libraries,
                this.onRangeBrush.bind(this),
                this.options.spec.experimental?.rangeSelectBrush
            );
            this.pMask.mousedown = (e: InteractionEvent) =>
                this.onMouseDown(
                    e.data.getLocalPosition(this.pMain).x,
                    e.data.getLocalPosition(this.pMain).y,
                    e.data.originalEvent.altKey
                );
            this.pMask.mouseup = (e: InteractionEvent) =>
                this.onMouseUp(e.data.getLocalPosition(this.pMain).x, e.data.getLocalPosition(this.pMain).y);
            this.pMask.mousemove = (e: InteractionEvent) => this.onMouseMove(e.data.getLocalPosition(this.pMain).x);
            this.pMask.mouseout = () => this.onMouseOut();

            // Remove a mouse graphic if created by a parent, and draw ourselves
            // https://github.com/higlass/higlass/blob/38f0c4415f0595c3b9d685a754d6661dc9612f7c/app/scripts/utils/show-mouse-position.js#L28
            // this.getIsFlipped = () => { return this.originalSpec.orientation === 'vertical' };
            this.flipText = this.options.spec.orientation === 'vertical';

            if (this.hideMousePosition) {
                this.hideMousePosition();
                this.hideMousePosition = undefined;
            }
            if (this.options?.showMousePosition && !this.hideMousePosition) {
                this.hideMousePosition = showMousePosition(
                    this,
                    Is2DTrack(resolveSuperposedTracks(this.options.spec)[0]),
                    this.isShowGlobalMousePosition()
                );
            }

            // We do not use HiGlass' trackNotFoundText
            this.pLabel.removeChild(this.trackNotFoundText);

            /* Custom loading label */
            const loadingTextStyle = getTextStyle({ color: 'black', size: 12 });
            this.loadingTextStyleObj = new HGC.libraries.PIXI.TextStyle(loadingTextStyle);
            this.loadingTextBg = new HGC.libraries.PIXI.Graphics();
            this.loadingText = new HGC.libraries.PIXI.Text('', loadingTextStyle);
            this.loadingText.anchor.x = 1;
            this.loadingText.anchor.y = 1;
            this.pLabel.addChild(this.loadingTextBg);
            this.pLabel.addChild(this.loadingText);

            this.svgData = [];
            this.textGraphics = [];
            this.textsBeingUsed = 0; // this variable is being used to improve the performance of text rendering
            this.loadingStatus = { loading: 0, processing: 0, rendering: 0 };

            // This improves the arc/link rendering performance
            HGC.libraries.PIXI.GRAPHICS_CURVES.adaptive = this.options.spec.style?.enableSmoothPath ?? false;
            if (HGC.libraries.PIXI.GRAPHICS_CURVES.adaptive) {
                HGC.libraries.PIXI.GRAPHICS_CURVES.maxLength = 1;
                HGC.libraries.PIXI.GRAPHICS_CURVES.maxSegments = 2048 * 10;
            }
        }

        /* ----------------------------------- RENDERING CYCLE ----------------------------------- */

        /**
         * Draw all tiles from the bottom.
         * (https://github.com/higlass/higlass/blob/54f5aae61d3474f9e868621228270f0c90ef9343/app/scripts/TiledPixiTrack.js#L727)
         */
        draw() {
            if (PRINT_RENDERING_CYCLE) console.warn('draw()');

            this.clearMouseEventData();
            this.svgData = [];
            this.textsBeingUsed = 0;
            this.pMouseHover?.clear();

            // this.pMain.clear();
            // this.pMain.removeChildren();

            // this.pBackground.clear();
            // this.pBackground.removeChildren();
            // this.pBorder.clear();
            // this.pBorder.removeChildren();

            const processTilesAndDraw = () => {
                // Preprocess all tiles at once so that we can share scales across tiles.
                this.preprocessAllTiles();

                // This function calls `drawTile` on each tile.
                super.draw();

                // Record tiles so that we ignore loading same tiles again
                this.prevVisibleAndFetchedTiles = this.visibleAndFetchedTiles();
            };

            if (
                usePrereleaseRendering(this.options.spec) &&
                !isEqual(this.visibleAndFetchedTiles(), this.prevVisibleAndFetchedTiles)
            ) {
                this.updateTileAsync(processTilesAndDraw);
            } else {
                processTilesAndDraw();
            }

            // Based on the updated marks, update range selection
            this.mRangeBrush.drawBrush(true);
        }

        /*
         * Do whatever is necessary before rendering a new tile. This function is called from `receivedTiles()`.
         * (Refer to https://github.com/higlass/higlass/blob/54f5aae61d3474f9e868621228270f0c90ef9343/app/scripts/HorizontalLine1DPixiTrack.js#L50)
         */
        initTile(tile: any) {
            if (PRINT_RENDERING_CYCLE) console.warn('initTile(tile)');

            // super.initTile(tile); // This calls `drawTile()`

            // Since `super.initTile(tile)` prints warning, we call `drawTile` ourselves without calling `super.initTile(tile)`.
            this.drawTile(tile);
        }

        updateTile(/* tile: any */) {} // Never mind about this function for the simplicity.
        renderTile(/* tile: any */) {} // Never mind about this function for the simplicity.

        /**
         * Display a tile upon receiving a new one or when explicitly called by a developer, e.g., calling `this.draw()`
         */
        drawTile(tile: any) {
            if (PRINT_RENDERING_CYCLE) console.warn('drawTile(tile)');

            tile.drawnAtScale = this._xScale.copy(); // being used in `super.draw()`

            if (!tile.goslingModels) {
                // We do not have a track model prepared to visualize
                return;
            }

            tile.graphics.clear();
            tile.graphics.removeChildren();

            // !! A single tile contains one track or multiple tracks overlaid
            /* Render marks and embellishments */
            tile.goslingModels.forEach((model: GoslingTrackModel) => {
                // check visibility condition
                const trackWidth = this.dimensions[0];
                const zoomLevel = this._xScale.invert(trackWidth) - this._xScale.invert(0);

                if (!model.trackVisibility({ zoomLevel })) {
                    return;
                }

                // This is for testing the upcoming rendering methods
                // if (usePrereleaseRendering(this.originalSpec)) {
                //     // Use worker to create visual properties
                //     drawScaleMark(HGC, this, tile, tm);
                //     return;
                // }

                drawPreEmbellishment(HGC, this, tile, model, this.options.theme);
                drawMark(HGC, this, tile, model);
                drawPostEmbellishment(HGC, this, tile, model, this.options.theme);
            });

            this.forceDraw();
        }

        /**
         * Render this track again using a new option when a user changed the option.
         * (Refer to https://github.com/higlass/higlass/blob/54f5aae61d3474f9e868621228270f0c90ef9343/app/scripts/HorizontalLine1DPixiTrack.js#L75)
         */
        rerender(newOptions: GoslingTrackOption) {
            if (PRINT_RENDERING_CYCLE) console.warn('rerender(options)');
            // !! We only call draw for the simplicity
            // super.rerender(newOptions); // This calls `renderTile()` on every tiles

            this.options = newOptions;

            if (this.options.spec.layout === 'circular') {
                // TODO (May-27-2022): remove the following line when we support a circular brush.
                // If the spec is changed to use the circular layout, we remove the current linear brush
                // because circular brush is not supported.
                this.mRangeBrush.remove();
            }

            this.clearMouseEventData();
            this.svgData = [];
            this.textsBeingUsed = 0;

            // this.flipText = this.originalSpec.orientation === 'vertical';

            // if (this.hideMousePosition) {
            //     this.hideMousePosition();
            //     this.hideMousePosition = undefined;
            // }
            // if (this.options?.showMousePosition && !this.hideMousePosition) {
            //     this.hideMousePosition = showMousePosition(
            //       this,
            //       Is2DTrack(resolveSuperposedTracks(this.originalSpec)[0]),
            //       this.isShowGlobalMousePosition(),
            //     );
            // }

            this.preprocessAllTiles(true);
            this.draw();
            this.forceDraw();
        }

        clearMouseEventData() {
            const models: GoslingTrackModel[] = this.visibleAndFetchedTiles()
                .map(tile => tile.goslingModels ?? [])
                .flat();
            models.forEach(model => model.getMouseEventModel().clear());
        }

        /**
         * End of the rendering cycle. This function is called when the track is removed entirely.
         */
        remove() {
            super.remove();

            if (this.gLegend) {
                this.gLegend.remove();
                this.gLegend = null;
            }
            this.mRangeBrush.remove();
        }
        /*
         * Rerender all tiles when track size is changed.
         * (Refer to https://github.com/higlass/higlass/blob/54f5aae61d3474f9e868621228270f0c90ef9343/app/scripts/PixiTrack.js#L186).
         */
        setDimensions(newDimensions: [number, number]) {
            if (PRINT_RENDERING_CYCLE) console.warn('setDimensions()');

            this.oldDimensions = this.dimensions; // initially, [1, 1]
            super.setDimensions(newDimensions); // This simply updates `this._xScale` and `this._yScale`

            this.mRangeBrush.setSize(newDimensions[1]);

            // const visibleAndFetched = this.visibleAndFetchedTiles();
            // visibleAndFetched.map((tile: any) => this.initTile(tile));
        }

        /**
         * Record new position.
         */
        setPosition(newPosition: [number, number]) {
            super.setPosition(newPosition); // This simply changes `this.position`

            [this.pMain.position.x, this.pMain.position.y] = this.position;

            this.mRangeBrush.setOffset(...newPosition);
        }

        /**
         * A function to redraw this track. Typically called when an asynchronous event occurs (i.e. tiles loaded)
         * (Refer to https://github.com/higlass/higlass/blob/54f5aae61d3474f9e868621228270f0c90ef9343/app/scripts/TiledPixiTrack.js#L71)
         */
        forceDraw() {
            this.animate();
        }

        /**
         * Called when location or zoom level has been changed by click-and-drag interaction
         * (https://github.com/higlass/higlass/blob/54f5aae61d3474f9e868621228270f0c90ef9343/app/scripts/HorizontalLine1DPixiTrack.js#L215)
         * For brushing, refer to https://github.com/higlass/higlass/blob/caf230b5ee41168ea491572618612ac0cc804e5a/app/scripts/HeatmapTiledPixiTrack.js#L1493
         */
        zoomed(newXScale: ScaleLinear<number, number>, newYScale: ScaleLinear<number, number>) {
            if (PRINT_RENDERING_CYCLE) console.warn('zoomed()');

            const range = this.mRangeBrush.getRange();
            this.mRangeBrush.updateRange(
                range ? [newXScale(this._xScale.invert(range[0])), newXScale(this._xScale.invert(range[1]))] : null
            );

            // super.zoomed(newXScale, newYScale); // This function updates `this._xScale` and `this._yScale` and call this.draw();
            this.xScale(newXScale);
            this.yScale(newYScale);

            this.refreshTiles();

            // if (this.scalableGraphics) {
            // this.scaleScalableGraphics(Object.values(this.scalableGraphics), newXScale, this.drawnAtScale);
            // }

            // if (!usePrereleaseRendering(this.originalSpec)) {
            this.draw();
            // }
            this.forceDraw();
        }

        /**
         * This is currently for testing the new way of rendering visual elements.
         */
        updateTileAsync(callback: () => void) {
            this.xDomain = this._xScale.domain();
            this.xRange = this._xScale.range();

            this.drawLoadingCue();

            this.worker.then((tileFunctions: any) => {
                tileFunctions
                    .getTabularData(
                        this.dataFetcher.uid,
                        Object.values(this.fetchedTiles).map((x: any) => x.remoteId)
                    )
                    .then((toRender: any) => {
                        this.drawLoadingCue();
                        const tiles = this.visibleAndFetchedTiles();

                        const tabularData = JSON.parse(Buffer.from(toRender).toString());
                        if (tiles?.[0]) {
                            const tile = tiles[0];
                            tile.tileData.tabularData = tabularData;
                            const [refTile] = HGC.utils.trackUtils.calculate1DVisibleTiles(
                                this.tilesetInfo,
                                this._xScale
                            );
                            tile.tileData.zoomLevel = refTile[0];
                            tile.tileData.tilePos = [refTile[1]];
                        }

                        this.drawLoadingCue();
                        callback();
                        this.drawLoadingCue();
                    });
            });
        }

        /**
         * Stretch out the scaleble graphics to have proper effect upon zoom and pan.
         */
        scaleScalableGraphics(graphics: Graphics[], xScale: any, drawnAtScale: any) {
            const drawnAtScaleExtent = drawnAtScale.domain()[1] - drawnAtScale.domain()[0];
            const xScaleExtent = xScale.domain()[1] - xScale.domain()[0];

            const tileK = drawnAtScaleExtent / xScaleExtent;
            const newRange = xScale.domain().map(drawnAtScale);

            const posOffset = newRange[0];
            graphics.forEach(g => {
                g.scale.x = tileK;
                g.position.x = -posOffset * tileK;
            });
        }

        /**
         * Return the set of ids of all tiles which are both visible and fetched.
         */
        visibleAndFetchedIds() {
            return Object.keys(this.fetchedTiles).filter(x => this.visibleTileIds.has(x));
        }

        /**
         * Return the set of all tiles which are both visible and fetched.
         */
        visibleAndFetchedTiles() {
            return this.visibleAndFetchedIds().map((x: any) => this.fetchedTiles[x]);
        }

        // !! This is called in the constructor, `super(context, options)`. So be aware to use variables that is prepared.
        calculateVisibleTiles() {
            if (usePrereleaseRendering(this.options.spec)) {
                const tiles = HGC.utils.trackUtils.calculate1DVisibleTiles(this.tilesetInfo, this._xScale);

                for (const tile of tiles) {
                    const { tileWidth } = this.getTilePosAndDimensions(
                        tile[0],
                        [tile[1], tile[1]],
                        this.tilesetInfo.tile_size
                    );

                    // base pairs
                    const DEFAULT_MAX_TILE_WIDTH =
                        this.options.spec.data?.type === 'bam' ? 2e4 : Number.MAX_SAFE_INTEGER;

                    if (tileWidth > (this.tilesetInfo.max_tile_width || DEFAULT_MAX_TILE_WIDTH)) {
                        this.forceDraw();
                        return;
                    }
                    this.forceDraw();
                }

                this.setVisibleTiles(tiles);
            } else {
                if (!this.tilesetInfo) {
                    // if we don't know anything about this dataset, no point in trying to get tiles
                    return;
                }

                // calculate the zoom level given the scales and the data bounds
                this.zoomLevel = this.calculateZoomLevel();

                if (this.tilesetInfo.resolutions) {
                    const sortedResolutions = this.tilesetInfo.resolutions
                        .map((x: number) => +x)
                        .sort((a: number, b: number) => b - a);

                    this.xTiles = tileProxy.calculateTilesFromResolution(
                        sortedResolutions[this.zoomLevel],
                        this._xScale,
                        this.tilesetInfo.min_pos[0],
                        this.tilesetInfo.max_pos[0]
                    );

                    if (Is2DTrack(resolveSuperposedTracks(this.options.spec)[0])) {
                        // it makes sense only when the y-axis is being used for a genomic field
                        this.yTiles = tileProxy.calculateTilesFromResolution(
                            sortedResolutions[this.zoomLevel],
                            this._yScale,
                            this.tilesetInfo.min_pos[0],
                            this.tilesetInfo.max_pos[0]
                        );
                    }

                    const tiles = this.tilesToId(this.xTiles, this.yTiles, this.zoomLevel);
                    this.setVisibleTiles(tiles);
                } else {
                    this.xTiles = tileProxy.calculateTiles(
                        this.zoomLevel,
                        this.relevantScale(),
                        this.tilesetInfo.min_pos[0],
                        this.tilesetInfo.max_pos[0],
                        this.tilesetInfo.max_zoom,
                        this.tilesetInfo.max_width
                    );

                    if (Is2DTrack(resolveSuperposedTracks(this.options.spec)[0])) {
                        // it makes sense only when the y-axis is being used for a genomic field
                        this.yTiles = tileProxy.calculateTiles(
                            this.zoomLevel,
                            this._yScale,
                            this.tilesetInfo.min_pos[1],
                            this.tilesetInfo.max_pos[1],
                            this.tilesetInfo.max_zoom,
                            this.tilesetInfo.max_width1 || this.tilesetInfo.max_width
                        );
                    }

                    const tiles = this.tilesToId(this.xTiles, this.yTiles, this.zoomLevel);
                    this.setVisibleTiles(tiles);
                }
            }
        }

        /**
         * Get the tile's position in its coordinate system.
         */
        getTilePosAndDimensions(zoomLevel: number, tilePos: [number, number], binsPerTileIn?: number) {
            const binsPerTile = binsPerTileIn || this.tilesetInfo.bins_per_dimension || 256;

            if (this.tilesetInfo.resolutions) {
                const sortedResolutions = this.tilesetInfo.resolutions
                    .map((x: number) => +x)
                    .sort((a: number, b: number) => b - a);

                // A resolution specifies the number of BP per bin
                const chosenResolution = sortedResolutions[zoomLevel];

                const [xTilePos, yTilePos] = tilePos;

                const tileWidth = chosenResolution * binsPerTile;
                const tileHeight = tileWidth;

                const tileX = tileWidth * xTilePos;
                const tileY = tileHeight * yTilePos;

                return {
                    tileX,
                    tileY,
                    tileWidth,
                    tileHeight
                };
            } else {
                const [xTilePos, yTilePos] = tilePos;

                const minX = this.tilesetInfo.min_pos[0];

                const minY = this.tilesetInfo.min_pos[1];

                const tileWidth = this.tilesetInfo.max_width / 2 ** zoomLevel;
                const tileHeight = this.tilesetInfo.max_width / 2 ** zoomLevel;

                const tileX = minX + xTilePos * tileWidth;
                const tileY = minY + yTilePos * tileHeight;

                return {
                    tileX,
                    tileY,
                    tileWidth,
                    tileHeight
                };
            }
        }

        /**
         * Convert tile positions to tile IDs
         */
        tilesToId(xTiles: any[], yTiles: any[], zoomLevel: any) {
            if (xTiles && !yTiles) {
                // this means only the `x` axis is being used
                return xTiles.map(x => [zoomLevel, x]);
            } else {
                // this means both `x` and `y` axes are being used together
                const tiles: any = [];
                xTiles.forEach(x => yTiles.forEach(y => tiles.push([zoomLevel, x, y])));
                return tiles;
            }
        }

        /**
         * Show visual cue during waiting for visualizations being rendered.
         */
        drawLoadingCue() {
            if (this.fetching.size) {
                const margin = 6;

                // Show textual message
                const text = `Fetching... ${Array.from(this.fetching).join(' ')}`;
                this.loadingText.text = text;
                this.loadingText.x = this.position[0] + this.dimensions[0] - margin / 2.0;
                this.loadingText.y = this.position[1] + this.dimensions[1] - margin / 2.0;

                // Show background
                const metric = HGC.libraries.PIXI.TextMetrics.measureText(text, this.loadingTextStyleObj);
                const { width: w, height: h } = metric;

                this.loadingTextBg.clear();
                this.loadingTextBg.lineStyle(1, colorToHex('grey'), 1, 0.5);
                this.loadingTextBg.beginFill(colorToHex('white'), 0.8);
                this.loadingTextBg.drawRect(
                    this.position[0] + this.dimensions[0] - w - margin - 1,
                    this.position[1] + this.dimensions[1] - h - margin - 1,
                    w + margin,
                    h + margin
                );

                this.loadingText.visible = true;
                this.loadingTextBg.visible = true;
            } else {
                this.loadingText.visible = false;
                this.loadingTextBg.visible = false;
            }
        }

        /**
         * This function reorganize the tileset information so that it can be more conveniently managed afterwards.
         */
        reorganizeTileInfo() {
            const tiles = this.visibleAndFetchedTiles();

            this.tileSize = this.tilesetInfo?.tile_size ?? 1024;

            tiles.forEach((t: any) => {
                // A new object to store all datasets
                t.gos = {};

                // ! `tileData` is an array-like object
                const keys = Object.keys(t.tileData).filter(d => !+d && d !== '0'); // ignore array indexes

                // Store objects first
                keys.forEach(k => {
                    t.gos[k] = t.tileData[k];
                });

                // Store raw data
                t.gos.raw = Array.from(t.tileData);
            });
        }

        updateScaleOffsetFromOriginalSpec(
            _renderingId: string,
            scaleOffset: [number, number],
            channelKey: 'color' | 'stroke'
        ) {
            resolveSuperposedTracks(this.options.spec).map(spec => {
                if (spec._renderingId === _renderingId) {
                    const channel = spec[channelKey];
                    if (IsChannelDeep(channel)) {
                        channel.scaleOffset = scaleOffset;
                    }
                }
            });
        }

        shareScaleOffsetAcrossTracksAndTiles(scaleOffset: [number, number], channelKey: 'color' | 'stroke') {
            const models: GoslingTrackModel[] = [];
            this.visibleAndFetchedTiles().forEach((tile: any) => {
                models.push(...tile.goslingModels);
            });
            models.forEach(d => {
                const channel = d.spec()[channelKey];
                if (IsChannelDeep(channel)) {
                    channel.scaleOffset = scaleOffset;
                }
                const channelOriginal = d.originalSpec()[channelKey];
                if (IsChannelDeep(channelOriginal)) {
                    channelOriginal.scaleOffset = scaleOffset;
                }
            });
        }

        /**
         * Check whether tiles should be merged.
         */
        shouldCombineTiles() {
            return (
                ((this.options.spec as SingleTrack | OverlaidTrack).dataTransform?.find(t => t.type === 'displace') &&
                    this.visibleAndFetchedTiles()?.[0]?.tileData &&
                    // we do not need to combine tiles w/ multivec, vector, matrix
                    !this.visibleAndFetchedTiles()?.[0]?.tileData.dense) ||
                this.options.spec.data?.type === 'bam'
            ); // BAM data fetcher already combines the datasets;
        }

        /**
         * Combile multiple tiles into a single large tile.
         * This is sometimes necessary, for example, when applying a displacement algorithm.
         */
        combineAllTilesIfNeeded() {
            if (!this.shouldCombineTiles()) {
                // This means we do not need to combine tiles
                return;
            }

            const tiles = this.visibleAndFetchedTiles();

            if (!tiles || tiles.length === 0) {
                // Does not make sense to combine tiles
                return;
            }

            // Increase the size of tiles by length
            this.tileSize = (this.tilesetInfo?.tile_size ?? 1024) * tiles.length;

            let newData: Datum[] = [];

            tiles.forEach((t: any, i: number) => {
                // Combine data
                newData = [...newData, ...t.tileData];

                // Flag to force using only one tile
                t.mergedToAnotherTile = i !== 0;
            });

            tiles[0].gos.raw = newData;

            // Remove duplicated if possible
            if (tiles[0].gos.raw[0]?.uid) {
                tiles[0].gos.raw = uniqBy(tiles[0].gos.raw, 'uid');
            }
        }

        preprocessAllTiles(force = false) {
            const models: GoslingTrackModel[] = [];

            this.reorganizeTileInfo();

            this.combineAllTilesIfNeeded();

            this.visibleAndFetchedTiles().forEach((tile: any) => {
                if (force) {
                    tile.goslingModels = [];
                }
                // tile preprocessing is done only once per tile
                const tileModels = this.preprocessTile(tile);
                tileModels?.forEach((m: GoslingTrackModel) => {
                    models.push(m);
                });
            });

            shareScaleAcrossTracks(models);

            const flatTileData = ([] as Datum[]).concat(...models.map(d => d.data()));
            if (flatTileData.length !== 0) {
                publish('rawData', { id: this.viewUid, data: flatTileData });
            }

            // console.log('processed gosling model', models);

            // IMPORTANT: If no genomic fields specified, no point to use multiple tiles, i.e., we need to draw a track only once with the data combined.
            /*
            if (!getGenomicChannelKeyFromTrack(this.originalSpec) && false) {
                // TODO:
                const visibleModels: GoslingTrackModel[][] = this.visibleAndFetchedTiles().map(
                    (d: any) => d.goslingModels
                );
                const modelsWeUse: GoslingTrackModel[] = visibleModels[0];
                const modelsWeIgnore: GoslingTrackModel[][] = visibleModels.slice(1);

                // concatenate the rows in the data
                modelsWeIgnore.forEach((ignored, i) => {
                    modelsWeUse.forEach(m => {
                        m.addDataRows(ignored[0].data());
                    });
                    this.visibleAndFetchedTiles()[i + 1].goslingModels = [];
                });
            }
            */
        }

        /**
         * Construct tabular data from a higlass tileset and a gosling track model.
         * Return the generated gosling track model.
         */
        preprocessTile(tile: any) {
            if (tile.mergedToAnotherTile) {
                tile.goslingModels = [];
                return;
            }

            if (tile.goslingModels && tile.goslingModels.length !== 0) {
                // already have the gosling models constructed
                return tile.goslingModels;
            }

            if (!tile.gos.tilePos) {
                // we do not have this information ready yet, so we cannot get tileX
                return;
            }

            // Single tile can contain multiple gosling models if multiple tracks are superposed.
            tile.goslingModels = [];

            const spec = JSON.parse(JSON.stringify(this.options.spec));

            const [trackWidth, trackHeight] = this.dimensions; // actual size of a track

            resolveSuperposedTracks(spec).forEach(resolved => {
                if (resolved.mark === 'brush') {
                    // interactive brushes are drawn by another plugin track, called `gosling-brush`
                    return;
                }

                if (!tile.gos.tabularData) {
                    // If the data is not already stored in a tabular form, convert them.
                    const { tileX, tileY, tileWidth, tileHeight } = this.getTilePosAndDimensions(
                        tile.gos.zoomLevel,
                        tile.gos.tilePos,
                        this.tilesetInfo.bins_per_dimension || this.tilesetInfo?.tile_size
                    );

                    tile.gos.tabularData = getTabularData(resolved, {
                        ...tile.gos,
                        tileX,
                        tileY,
                        tileWidth,
                        tileHeight,
                        tileSize: this.tileSize
                    });
                }

                tile.gos.tabularDataFiltered = Array.from(tile.gos.tabularData);

                /*
                 * Data Transformation applied to each of the overlaid tracks.
                 */
                if (resolved.dataTransform) {
                    resolved.dataTransform.forEach(t => {
                        switch (t.type) {
                            case 'filter':
                                tile.gos.tabularDataFiltered = filterData(t, tile.gos.tabularDataFiltered);
                                break;
                            case 'concat':
                                tile.gos.tabularDataFiltered = concatString(t, tile.gos.tabularDataFiltered);
                                break;
                            case 'replace':
                                tile.gos.tabularDataFiltered = replaceString(t, tile.gos.tabularDataFiltered);
                                break;
                            case 'log':
                                tile.gos.tabularDataFiltered = calculateData(t, tile.gos.tabularDataFiltered);
                                break;
                            case 'exonSplit':
                                tile.gos.tabularDataFiltered = splitExon(
                                    t,
                                    tile.gos.tabularDataFiltered,
                                    resolved.assembly
                                );
                                break;
                            case 'genomicLength':
                                tile.gos.tabularDataFiltered = calculateGenomicLength(t, tile.gos.tabularDataFiltered);
                                break;
                            case 'svType':
                                tile.gos.tabularDataFiltered = inferSvType(t, tile.gos.tabularDataFiltered);
                                break;
                            case 'coverage':
                                tile.gos.tabularDataFiltered = aggregateCoverage(
                                    t,
                                    tile.gos.tabularDataFiltered,
                                    this._xScale.copy()
                                );
                                break;
                            case 'subjson':
                                tile.gos.tabularDataFiltered = parseSubJSON(t, tile.gos.tabularDataFiltered);
                                break;
                            case 'displace':
                                tile.gos.tabularDataFiltered = displace(
                                    t,
                                    tile.gos.tabularDataFiltered,
                                    this._xScale.copy()
                                );
                                break;
                        }
                    });
                }

                // TODO: Remove the following block entirely and use the `rawData` API in the Editor (June-02-2022)
                // Send data preview to the editor so that it can be shown to users.
                try {
                    if (PubSub) {
                        const NUM_OF_ROWS_IN_PREVIEW = 100;
                        const numOrRows = tile.gos.tabularDataFiltered.length;
                        PubSub.publish('data-preview', {
                            id: this.viewUid,
                            dataConfig: JSON.stringify({ data: resolved.data }),
                            data:
                                NUM_OF_ROWS_IN_PREVIEW > numOrRows
                                    ? tile.gos.tabularDataFiltered
                                    : sampleSize(tile.gos.tabularDataFiltered, NUM_OF_ROWS_IN_PREVIEW)
                            // ...
                        });
                    }
                } catch (e) {
                    // ..
                }

                // Replace width and height information with the actual values for responsive encoding
                const axisSize = IsXAxis(resolved) ? HIGLASS_AXIS_SIZE : 0; // Why the axis size must be added here?
                const [w, h] = [trackWidth, trackHeight + axisSize];
                const circularFactor = Math.min(w, h) / Math.min(resolved.width, resolved.height);
                if (resolved.innerRadius) {
                    resolved.innerRadius = resolved.innerRadius * circularFactor;
                }
                if (resolved.outerRadius) {
                    resolved.outerRadius = resolved.outerRadius * circularFactor;
                }
                resolved.width = w;
                resolved.height = h;

                // Construct separate gosling models for individual tiles
                const gm = new GoslingTrackModel(resolved, tile.gos.tabularDataFiltered, this.options.theme);

                // Add a track model to the tile object
                tile.goslingModels.push(gm);
            });

            return tile.goslingModels;
        }

        getIndicesOfVisibleDataInTile(tile: any) {
            const visible = this._xScale.range();

            if (!this.tilesetInfo) return [null, null];

            const { tileX, tileWidth } = this.getTilePosAndDimensions(
                tile.gos.zoomLevel,
                tile.gos.tilePos,
                this.tilesetInfo.bins_per_dimension || this.tilesetInfo?.tile_size
            );

            const tileXScale = HGC.libraries.d3Scale
                .scaleLinear()
                .domain([0, this.tilesetInfo?.tile_size || this.tilesetInfo?.bins_per_dimension])
                .range([tileX, tileX + tileWidth]);

            const start = Math.max(0, Math.round(tileXScale.invert(this._xScale.invert(visible[0]))));
            const end = Math.min(tile.gos.dense.length, Math.round(tileXScale.invert(this._xScale.invert(visible[1]))));

            return [start, end];
        }

        /**
         * Returns the minimum in the visible area (not visible tiles)
         */
        minVisibleValue() {}

        /**
         * Returns the maximum in the visible area (not visible tiles)
         */
        maxVisibleValue() {}

        exportSVG() {} // We do not support SVG export

        /**
         * From all tiles and overlaid tracks, collect element(s) that are withing a mouse position.
         */
        getElementsWithinMouse(mouseX: number, mouseY: number) {
            // Collect all gosling track models
            const models: GoslingTrackModel[] = this.visibleAndFetchedTiles()
                .map(tile => tile.goslingModels ?? [])
                .flat();

            // TODO: `Omit` this properties in the schema of individual overlaid tracks.
            // These should be defined only once for a group of overlaid traks (09-May-2022)
            // See https://github.com/gosling-lang/gosling.js/issues/677
            const mouseEvents = this.options.spec.experimental?.mouseEvents;
            const multiHovering = IsMouseEventsDeep(mouseEvents) && mouseEvents.enableMouseOverOnMultipleMarks;
            const idField = IsMouseEventsDeep(mouseEvents) && mouseEvents.groupMarksByField;

            // Collect all mouse event data from tiles and overlaid tracks
            const mergedCapturedElements: MouseEventData[] = models
                .map(model => model.getMouseEventModel().findAll(mouseX, mouseY, true))
                .flat();

            if (!multiHovering) {
                // Select only one on the top of a cursor
                mergedCapturedElements.splice(1, mergedCapturedElements.length - 1);
            }

            // Iterate again to select sibling marks (e.g., entire glyphs)
            if (mergedCapturedElements.length !== 0 && idField) {
                const source = Array.from(mergedCapturedElements);
                models.forEach(model => {
                    const siblings = model.getMouseEventModel().getSiblings(source, idField);
                    mergedCapturedElements.push(...siblings);
                });
            }

            return mergedCapturedElements;
        }

        /**
         * Highlight marks that are either mouse overed or selected.
         */
        highlightMarks(
            g: Graphics,
            marks: MouseEventData[],
            style: {
                stroke: string;
                strokeWidth: number;
                strokeOpacity: number;
                color: string;
                opacity: number;
            }
        ) {
            g.lineStyle(
                style.strokeWidth,
                colorToHex(style.stroke),
                style.strokeOpacity, // alpha
                0.5 // alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter)
            );
            g.beginFill(colorToHex(style.color), style.color === 'none' ? 0 : style.opacity);

            marks.forEach(d => {
                if (d.type === 'point') {
                    const [x, y, r = 3] = d.polygon;
                    g.drawCircle(x, y, r);
                } else if (d.type === 'line') {
                    g.moveTo(d.polygon[0], d.polygon[1]);
                    flatArrayToPairArray(d.polygon).map(d => g.lineTo(d[0], d[1]));
                } else {
                    g.drawPolygon(d.polygon);
                }
            });
        }

        onRangeBrush(range: [number, number] | null, skipApiTrigger = false) {
            this.pMouseSelection.clear();

            if (range === null) {
                // brush just removed
                if (!skipApiTrigger) {
                    publish('rangeSelect', { id: this.viewUid, genomicRange: null, data: [] });
                }
                return;
            }

            const [startX, endX] = range;

            // Collect all gosling track models
            const models: GoslingTrackModel[] = this.visibleAndFetchedTiles()
                .map(tile => tile.goslingModels ?? [])
                .flat();

            // Collect all mouse event data from tiles and overlaid tracks
            let capturedElements: MouseEventData[] = models
                .map(model => model.getMouseEventModel().findAllWithinRange(startX, endX, true))
                .flat();

            // Deselect marks if their siblings are not selected.
            // e.g., if only one exon is selected in a gene, we do not select it.
            const mouseEvents = this.options.spec.experimental?.mouseEvents;
            const idField = IsMouseEventsDeep(mouseEvents) && mouseEvents.groupMarksByField;
            if (capturedElements.length !== 0 && idField) {
                models.forEach(model => {
                    const siblings = model.getMouseEventModel().getSiblings(capturedElements, idField);
                    const siblingIds = Array.from(new Set(siblings.map(d => d.value[idField])));
                    capturedElements = capturedElements.filter(d => siblingIds.indexOf(d.value[idField]) === -1);
                });
            }

            if (capturedElements.length !== 0) {
                // selection effect graphics
                const g = this.pMouseSelection;

                if (!this.options.spec.experimental?.selectedMarks?.showOnTheBack) {
                    // place on the top
                    this.pMain.removeChild(g);
                    this.pMain.addChild(g);
                }

                this.highlightMarks(
                    g,
                    capturedElements,
                    Object.assign({}, DEFAULT_MARK_HIGHLIGHT_STYLE, this.options.spec.experimental?.selectedMarks)
                );
            }

            /* API call */
            if (!skipApiTrigger) {
                const genomicRange: [string, string] = [
                    getRelativeGenomicPosition(Math.floor(this._xScale.invert(startX))),
                    getRelativeGenomicPosition(Math.floor(this._xScale.invert(endX)))
                ];

                publish('rangeSelect', {
                    id: this.viewUid,
                    genomicRange,
                    data: capturedElements.map(d => d.value)
                });
            }

            this.forceDraw();
        }

        onMouseDown(mouseX: number, mouseY: number, isAltPressed: boolean) {
            // Record these so that we do not triger click event when dragged.
            this.mouseDownX = mouseX;
            this.mouseDownY = mouseY;

            // Determine whether to activate a range brush
            const mouseEvents = this.options.spec.experimental?.mouseEvents;
            const rangeSelectEnabled = !!mouseEvents || (IsMouseEventsDeep(mouseEvents) && !!mouseEvents.rangeSelect);
            this.isRangeBrushActivated = rangeSelectEnabled && isAltPressed;

            this.pMouseHover.clear();
        }

        onMouseMove(mouseX: number) {
            if (this.options.spec.layout === 'circular') {
                // TODO: We do not yet support range selection on circular tracks
                return;
            }

            if (this.isRangeBrushActivated) {
                this.mRangeBrush.updateRange([mouseX, this.mouseDownX]).drawBrush().visible().disable();
            }
        }

        onMouseUp(mouseX: number, mouseY: number) {
            const mouseEvents = this.options.spec.experimental?.mouseEvents;
            const clickEnabled = !!mouseEvents || (IsMouseEventsDeep(mouseEvents) && !!mouseEvents.click);
            const isDrag = Math.sqrt((this.mouseDownX - mouseX) ** 2 + (this.mouseDownY - mouseY) ** 2) > 1;

            if (!this.isRangeBrushActivated && !isDrag) {
                // Clicking outside the brush should remove the brush and the selection.
                this.mRangeBrush.clear();
                this.pMouseSelection.clear();
            } else {
                // Dragging ended, so enable adjusting the range brush
                this.mRangeBrush.enable();
            }

            this.isRangeBrushActivated = false;

            if (!this.tilesetInfo) {
                // Do not have enough information
                return;
            }

            // click API
            if (!isDrag && clickEnabled) {
                // Identify the current position
                const genomicPosition = getRelativeGenomicPosition(Math.floor(this._xScale.invert(mouseX)));

                // Get elements within mouse
                const capturedElements = this.getElementsWithinMouse(mouseX, mouseY);

                if (capturedElements.length !== 0) {
                    publish('click', {
                        id: this.viewUid,
                        genomicPosition,
                        data: capturedElements.map(d => d.value)
                    });
                }
            }
        }

        onMouseOut() {
            this.isRangeBrushActivated = false;
            document.body.style.cursor = 'default';
            this.pMouseHover.clear();
        }

        getMouseOverHtml(mouseX: number, mouseY: number) {
            if (this.isRangeBrushActivated) {
                // In the middle of drawing range brush.
                return;
            }

            if (!this.tilesetInfo) {
                // Do not have enough information
                return;
            }

            this.pMouseHover.clear();

            // Current position
            const genomicPosition = getRelativeGenomicPosition(Math.floor(this._xScale.invert(mouseX)));

            // Get elements within mouse
            const capturedElements = this.getElementsWithinMouse(mouseX, mouseY);

            // Change cursor
            // https://developer.mozilla.org/en-US/docs/Web/CSS/cursor
            if (capturedElements.length !== 0) {
                document.body.style.cursor = 'pointer';
            } else {
                document.body.style.cursor = 'default';
            }

            if (capturedElements.length !== 0) {
                const mouseEvents = this.options.spec.experimental?.mouseEvents;
                const mouseOverEnabled = !!mouseEvents || (IsMouseEventsDeep(mouseEvents) && !!mouseEvents.mouseOver);
                if (mouseOverEnabled) {
                    // Display mouse over effects
                    const g = this.pMouseHover;

                    if (!this.options.spec.experimental?.mouseOveredMarks?.showHoveringOnTheBack) {
                        // place on the top
                        this.pMain.removeChild(g);
                        this.pMain.addChild(g);
                    }

                    this.highlightMarks(
                        g,
                        capturedElements,
                        Object.assign(
                            {},
                            DEFAULT_MARK_HIGHLIGHT_STYLE,
                            this.options.spec.experimental?.mouseOveredMarks
                        )
                    );

                    // API call
                    publish('mouseOver', {
                        id: this.viewUid,
                        genomicPosition,
                        data: capturedElements.map(d => d.value)
                    });
                }

                // Display a tooltip
                const models = this.visibleAndFetchedTiles()
                    .map(tile => tile.goslingModels ?? [])
                    .flat();

                const firstTooltipSpec = models
                    .find(m => m.spec().tooltip && m.spec().tooltip?.length !== 0)
                    ?.spec().tooltip;

                if (firstTooltipSpec) {
                    let content = firstTooltipSpec
                        .map((d: any) => {
                            const rawValue = capturedElements[0].value[d.field];
                            let value = rawValue;
                            if (d.type === 'quantitative' && d.format) {
                                value = format(d.format)(+rawValue);
                            } else if (d.type === 'genomic') {
                                // e.g., chr1:204133
                                value = getRelativeGenomicPosition(+rawValue);
                            }

                            return (
                                '<tr>' +
                                `<td style='padding: 4px 8px'>${d.alt ?? d.field}</td>` +
                                `<td style='padding: 4px 8px'><b>${value}</b></td>` +
                                '</tr>'
                            );
                        })
                        .join('');

                    content = `<table style='text-align: left; margin-top: 12px'>${content}</table>`;
                    if (capturedElements.length > 1) {
                        content +=
                            `<div style='padding: 4px 8px; margin-top: 4px; text-align: center; color: grey'>` +
                            `${capturedElements.length - 1} Additional Selections...` +
                            '</div>';
                    }
                    return `<div>${content}</div>`;
                }
            }
        }
    }
    return new GoslingTrackClass(args);
}