lodash-es#isEqual TypeScript Examples

The following examples show how to use lodash-es#isEqual. 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: useCacheRender.ts    From UUI with MIT License 6 votes vote down vote up
export function useValueCacheRender<T>(data: T, render: (data: T) => React.ReactNode, options: { comparator?: (previous: T, current: T) => boolean }) {
  const [rendered, setRendered] = useState(render(data))
  const previous = usePrevious<T>(data) as T
  const current = data

  useEffect(() => {
    const isSame = options?.comparator ? options.comparator(previous, current) : isEqual(previous, current)
    if (!isSame) {
      setRendered(render(current))
    }
  }, [previous, current, options, render])

  return rendered
}
Example #2
Source File: dia-backend-auth.service.ts    From capture-lite with GNU General Public License v3.0 6 votes vote down vote up
private updateLanguage$(headers: { [header: string]: string | string[] }) {
    return this.languageService.currentLanguageKey$.pipe(
      distinctUntilChanged(isEqual),
      concatMap(language =>
        this.httpClient.patch(
          `${BASE_URL}/auth/users/me/`,
          { language },
          { headers }
        )
      )
    );
  }
Example #3
Source File: capacitor-filesystem-table.ts    From capture-lite with GNU General Public License v3.0 6 votes vote down vote up
async delete(tuples: T[], comparator = isEqual) {
    return this.mutex.runExclusive(async () => {
      this.assertTuplesExist(tuples, comparator);
      await this.initialize();
      const afterDeletion = differenceWith(
        this.tuples$.value,
        tuples,
        comparator
      );
      this.tuples$.next(afterDeletion);
      await this.dumpJson();
      return tuples;
    });
  }
Example #4
Source File: capacitor-filesystem-table.ts    From capture-lite with GNU General Public License v3.0 6 votes vote down vote up
async insert(
    tuples: T[],
    onConflict = OnConflictStrategy.ABORT,
    comparator = isEqual
  ) {
    return this.mutex.runExclusive(async () => {
      assertNoDuplicatedTuples(tuples, comparator);
      await this.initialize();
      if (onConflict === OnConflictStrategy.ABORT) {
        this.assertNoConflictWithExistedTuples(tuples, comparator);
        this.tuples$.next([...(this.tuples$.value ?? []), ...tuples]);
      } else if (onConflict === OnConflictStrategy.IGNORE) {
        this.tuples$.next(
          uniqWith([...(this.tuples$.value ?? []), ...tuples], comparator)
        );
      } else {
        this.tuples$.next(
          uniqWith([...tuples, ...(this.tuples$.value ?? [])], comparator)
        );
      }
      await this.dumpJson();
      return tuples;
    });
  }
Example #5
Source File: go-pro-bluetooth.service.ts    From capture-lite with GNU General Public License v3.0 6 votes vote down vote up
async sendBluetoothReadCommand(command: number[]) {
    await this.initialize();
    await this.checkBluetoothDeviceConnection();

    if (isEqual(command, this.shutdownCommand)) {
      this.getGoProWiFiCreds();
    }

    // TODO: add other read commands if necessary
  }
Example #6
Source File: gosling-track-model.test.ts    From gosling.js with MIT License 6 votes vote down vote up
describe('default options should be added into the original spec', () => {
    it('original spec should be the same after making a gosling model', () => {
        const model = new GoslingTrackModel(MINIMAL_TRACK_SPEC, [], getTheme());
        expect(isEqual(model.originalSpec(), MINIMAL_TRACK_SPEC)).toEqual(true);
    });

    it('default opacity should be added if it is missing in the spec', () => {
        const model = new GoslingTrackModel(MINIMAL_TRACK_SPEC, [], getTheme());
        const spec = model.spec();
        expect(spec.opacity).not.toBeUndefined();
        expect(IsChannelValue(spec.opacity) ? spec.opacity.value : undefined).toBe(1);
    });

    it('default color scheme for quantitative data field should be added if range is not specified', () => {
        const track: Track = {
            ...MINIMAL_TRACK_SPEC,
            color: { field: 'f', type: 'quantitative' }
        };
        const model = new GoslingTrackModel(track, [], getTheme());
        const spec = model.spec();
        const range = IsChannelDeep(spec.color) ? spec.color.range : [];
        expect(range).not.toBeUndefined();
        expect(range).toBe('viridis');
    });
});
Example #7
Source File: History.ts    From LogicFlow with Apache License 2.0 6 votes vote down vote up
add(data) {
    if (isEqual(last(this.undos), data)) return;
    this.undos.push(data);
    // 因为undo的时候,会触发add.
    // 所以需要区分这个add是undo触发的,还是用户正常操作触发的。
    // 如果是用户正常操作触发的,需要清空redos
    if (!isEqual(this.curData, data)) {
      this.redos = [];
    }
    this.eventCenter.emit(EventType.HISTORY_CHANGE,
      {
        data: {
          undos: this.undos,
          redos: this.redos,
          undoAble: this.undos.length > 1,
          redoAble: this.redos.length > 0,
        },
      });
    if (this.undos.length > this.maxSize) {
      this.undos.shift();
    }
  }
Example #8
Source File: useDeepMemo.ts    From atlas with GNU General Public License v3.0 6 votes vote down vote up
// * Gracefully copied from https://github.com/apollographql/react-apollo/blob/master/packages/hooks/src/utils/useDeepMemo.ts

/**
 * Memoize a result using deep equality. This hook has two advantages over
 * React.useMemo: it uses deep equality to compare memo keys, and it guarantees
 * that the memo function will only be called if the keys are unequal.
 * React.useMemo cannot be relied on to do this, since it is only a performance
 * optimization (see https://reactjs.org/docs/hooks-reference.html#usememo).
 */
export function useDeepMemo<TKey, TValue>(memoFn: () => TValue, key: TKey): TValue {
  const ref = useRef<{ key: TKey; value: TValue }>()

  if (!ref.current || !isEqual(key, ref.current.key)) {
    ref.current = { key, value: memoFn() }
  }

  return ref.current.value
}
Example #9
Source File: IconGallery.tsx    From UUI with MIT License 6 votes vote down vote up
export function IconGallery<
  N extends string,
  P extends {
    [key in N]: Partial<Pick<IconProps, 'mode'>> & Pick<IconProps, 'source'>
  },
  M extends IconProps['mode'] | undefined,
  O extends IconGalleryOptions<M>,
  V extends {
    [key in keyof P]: LoadedIconByMode<P[key]['mode']> extends undefined
      ? (O extends undefined
          ? never
          : (LoadedIconByMode<O['mode']> extends undefined ? never : LoadedIconByMode<O['mode']>)
        )
      : LoadedIconByMode<P[key]['mode']>
  }
>(initials: P, options?: O): V {
  return mapValues(initials, (value) => {
    const MemoIcon = React.memo((props: IconProps) => {
      return <Icon {...options} {...value} {...props}></Icon>
    }, isEqual);
    (MemoIcon.type as React.SFC).displayName = `<UUI> IconGallery WrappedIcon`;
    return MemoIcon;
  }) as any
}
Example #10
Source File: Glider.tsx    From atlas with GNU General Public License v3.0 5 votes vote down vote up
export function useGlider<T extends HTMLElement>({
  onAdd,
  onAnimated,
  onDestroy,
  onLoaded,
  onRefresh,
  onRemove,
  onSlideHidden,
  onSlideVisible,
  ...gliderOptions
}: GliderProps) {
  const [glider, setGlider] = useState<Glider<HTMLElement>>()
  const element = useRef<T>(null)
  const gliderOptionsRef = useRef(gliderOptions)

  useLayoutEffect(() => {
    if (!element.current) {
      return
    }
    const newGlider = new Glider(element.current, { skipTrack: true })
    setGlider(newGlider)

    return () => {
      if (newGlider) {
        newGlider.destroy()
      }
    }
  }, [])

  /**
   * because gliderOptions changes it's reference through renders,
   * we need to avoid unnecessary glider refresh by comparing gliderOptions value
   */
  useLayoutEffect(() => {
    if (!glider || isEqual(gliderOptions, gliderOptionsRef.current)) {
      return
    }
    gliderOptionsRef.current = gliderOptions
    glider.setOption({ skipTrack: true, ...gliderOptions }, true)
    glider.refresh(true)
  }, [gliderOptions, glider])

  useEventListener(element.current, 'glider-add', onAdd)
  useEventListener(element.current, 'glider-animated', onAnimated)
  useEventListener(element.current, 'glider-destroy', onDestroy)
  useEventListener(element.current, 'glider-loaded', onLoaded)
  useEventListener(element.current, 'glider-refresh', onRefresh)
  useEventListener(element.current, 'glider-remove', onRemove)
  useEventListener(element.current, 'glider-slide-hidden', onSlideHidden)
  useEventListener(element.current, 'glider-slide-visible', onSlideVisible)
  return {
    ref: element,
    glider,
    getGliderProps,
    getTrackProps,
    getNextArrowProps,
    getPrevArrowProps,
    getContainerProps,
    getDotsProps,
  }
}
Example #11
Source File: useCacheRender.ts    From UUI with MIT License 5 votes vote down vote up
export function useArrayCacheRender<T>(
  data: T[],
  render: (data: T) => React.ReactNode,
  options: {
    id: (i: T) => string;
    comparator?: (previous: T, current: T) => boolean;
  },
) {
  const [list, setList] = useState<{
    id: string;
    rendered: React.ReactNode;
  }[]>([])
  const previous = usePrevious<T[]>(data) as T[]
  const current = data

  useEffect(() => {
    const isSameOne = (i: T, j: T) => options.id(i) === options.id(j)
    const intersected = intersectionWith(previous, current, isSameOne)
    const removing = xorWith(previous, intersected, isSameOne)
    const adding = xorWith(current, intersected, isSameOne)
    const updating = intersected.filter((i) => {
      const p = previous.find((j) => options.id(i) === options.id(j))
      const c = current.find((j) => options.id(i) === options.id(j))
      if (!p) return false
      if (!c) return false

      return options.comparator ? options.comparator(c, p) : !isEqual(c, p)
    })

    const newList = clone(list)

    for (const i of removing) {
      remove(newList, (r) => r.id === options.id(i))
    }
    for (const i of updating) {
      const index = list.findIndex((r) => r.id === options.id(i))
      const c = current.find((c) => options.id(c) === options.id(i))
      if (index > -1 && c) newList[index] = { id: options.id(c), rendered: render(c) }
    }
    for (const i of adding) {
      newList.push({ id: options.id(i), rendered: render(i) })
    }

    setList(newList)
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [previous, current])

  const rendered = useMemo(() => {
    return list.map((i) => i.rendered)
  }, [list])

  return rendered
}
Example #12
Source File: capacitor-storage-preferences.ts    From capture-lite with GNU General Public License v3.0 5 votes vote down vote up
get$(key: string, defaultValue: SupportedTypes): Observable<SupportedTypes> {
    return defer(() => this.initializeValue(key, defaultValue)).pipe(
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      concatMap(() => this.subjects.get(key)!.asObservable()),
      isNonNullable(),
      distinctUntilChanged(isEqual)
    );
  }
Example #13
Source File: proof-repository.service.ts    From capture-lite with GNU General Public License v3.0 5 votes vote down vote up
async add(proof: Proof, onConflict = OnConflictStrategy.ABORT) {
    await this.table.insert([proof.getIndexedProofView()], onConflict, (x, y) =>
      isEqual(x.indexedAssets, y.indexedAssets)
    );
    return proof;
  }
Example #14
Source File: proof-repository.service.ts    From capture-lite with GNU General Public License v3.0 5 votes vote down vote up
async remove(proof: Proof) {
    await Promise.all([
      this.table.delete([proof.getIndexedProofView()], (x, y) =>
        isEqual(x.indexedAssets, y.indexedAssets)
      ),
      proof.destroy(),
    ]);
  }
Example #15
Source File: proof-repository.service.ts    From capture-lite with GNU General Public License v3.0 5 votes vote down vote up
readonly all$ = this.table.queryAll$.pipe(
    distinctUntilChanged(isEqual),
    map((indexedProofViews: IndexedProofView[]) =>
      indexedProofViews.map(view =>
        Proof.fromIndexedProofView(this.mediaStore, view)
      )
    )
  );
Example #16
Source File: CategoryVideos.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
CategoryVideos: React.FC<{ categoryId: string }> = ({ categoryId }) => {
  const smMatch = useMediaMatch('sm')
  const mdMatch = useMediaMatch('md')
  const containerRef = useRef<HTMLDivElement>(null)
  const scrollWhenFilterChange = useRef(false)

  const filtersBarLogic = useFiltersBar()
  const {
    setVideoWhereInput,
    filters: { setIsFiltersOpen, isFiltersOpen, language, setLanguage },
    canClearFilters: { canClearAllFilters, clearAllFilters },
    videoWhereInput,
  } = filtersBarLogic

  const [sortVideosBy, setSortVideosBy] = useState<VideoOrderByInput>(VideoOrderByInput.CreatedAtDesc)

  const { videoCount } = useVideoCount({
    where: { ...videoWhereInput, category: { id_eq: categoryId } },
  })

  useEffect(() => {
    if (scrollWhenFilterChange.current) {
      containerRef.current?.scrollIntoView()
    }
    // account for videoWhereInput initialization
    if (!isEqual(videoWhereInput, {})) {
      scrollWhenFilterChange.current = true
    }
  }, [videoWhereInput])

  const handleSorting = (value?: VideoOrderByInput | null) => {
    if (value) {
      setSortVideosBy(value)
    }
  }

  const handleFilterClick = () => {
    setIsFiltersOpen((value) => !value)
  }

  const handleSelectLanguage = useCallback(
    (language: string | null | undefined) => {
      setLanguage(language)
      setVideoWhereInput((value) => ({
        ...value,
        language:
          language === 'undefined'
            ? undefined
            : {
                iso_eq: language,
              },
      }))
    },
    [setLanguage, setVideoWhereInput]
  )

  const topbarHeight = mdMatch ? 80 : 64

  const sortingNode = (
    <StyledSelect
      size="small"
      helperText={null}
      value={sortVideosBy}
      valueLabel="Sort by: "
      items={ADAPTED_SORT_OPTIONS}
      onChange={handleSorting}
    />
  )
  return (
    <>
      <Global styles={categoryGlobalStyles} />
      <Container ref={containerRef}>
        <StyledSticky style={{ top: topbarHeight - 1 }}>
          <ControlsContainer>
            <GridItem colSpan={{ base: 2, sm: 1 }}>
              <Text variant={mdMatch ? 'h500' : 'h400'}>
                All videos {videoCount !== undefined && `(${videoCount})`}
              </Text>
            </GridItem>
            {smMatch ? (
              <StyledSelect
                onChange={handleSelectLanguage}
                size="small"
                value={language}
                items={SELECT_LANGUAGE_ITEMS}
              />
            ) : (
              sortingNode
            )}
            <div>
              <Button
                badge={canClearAllFilters}
                variant="secondary"
                icon={<SvgActionFilters />}
                onClick={handleFilterClick}
              >
                Filters
              </Button>
            </div>
            {smMatch && sortingNode}
          </ControlsContainer>
          <FiltersBar {...filtersBarLogic} activeFilters={['date', 'length', 'other', 'language']} />
        </StyledSticky>

        <StyledVideoGrid
          isFiltersOpen={isFiltersOpen}
          emptyFallback={
            <FallbackWrapper>
              <EmptyFallback
                title="No videos found"
                subtitle="Please, try changing your filtering criteria"
                button={
                  <Button onClick={clearAllFilters} variant="secondary">
                    Clear all filters
                  </Button>
                }
              />
            </FallbackWrapper>
          }
          videoWhereInput={{ ...videoWhereInput, category: { id_eq: categoryId } }}
          orderBy={sortVideosBy}
          onDemandInfinite
        />
      </Container>
    </>
  )
}
Example #17
Source File: editor.tsx    From gosling.js with MIT License 4 votes vote down vote up
/**
 * React component for editing Gosling specs
 */
function Editor(props: RouteComponentProps) {
    // Determines whether the screen is too small (e.g., mobile)
    const IS_SMALL_SCREEN = window.innerWidth <= 500;

    // custom spec contained in the URL
    const urlParams = new URLSearchParams(props.location.search);
    const urlSpec = urlParams.has('spec') ? JSONCrush.uncrush(urlParams.get('spec')!) : null;
    const urlGist = urlParams.get('gist');
    const urlExampleId = urlParams.get('example') ?? '';

    const defaultCode =
        urlGist || urlExampleId ? emptySpec() : stringify(urlSpec ?? (INIT_DEMO.spec as gosling.GoslingSpec));
    const defaultJsCode = urlGist || urlExampleId || !INIT_DEMO.specJs ? json2js(defaultCode) : INIT_DEMO.specJs;

    const previewData = useRef<PreviewData[]>([]);
    const [refreshData, setRefreshData] = useState<boolean>(false);
    const [language, changeLanguage] = useState<EditorLangauge>('json');

    const [demo, setDemo] = useState(
        examples[urlExampleId] ? { id: urlExampleId, ...examples[urlExampleId] } : INIT_DEMO
    );
    const [isImportDemo, setIsImportDemo] = useState(false);
    const [theme, setTheme] = useState<gosling.Theme>('light');
    const [hg, setHg] = useState<HiGlassSpec>();
    const [code, setCode] = useState(defaultCode);
    const [jsCode, setJsCode] = useState(defaultJsCode); //[TO-DO: more js format examples]
    const [goslingSpec, setGoslingSpec] = useState<gosling.GoslingSpec>();
    const [log, setLog] = useState<ReturnType<typeof gosling.validateGoslingSpec>>({ message: '', state: 'success' });
    // const [mouseEventInfo, setMouseEventInfo] =
    //     useState<{ type: 'mouseOver' | 'click'; data: Datum[]; position: string }>();
    const [showExamples, setShowExamples] = useState(false);
    const [autoRun, setAutoRun] = useState(true);
    const [selectedPreviewData, setSelectedPreviewData] = useState<number>(0);
    const [gistTitle, setGistTitle] = useState<string>();
    const [description, setDescription] = useState<string | null>();
    const [expertMode, setExpertMode] = useState(false);

    // This parameter only matter when a markdown description was loaded from a gist but the user wants to hide it
    const [hideDescription, setHideDescription] = useState<boolean>(IS_SMALL_SCREEN || false);

    // Determine the size of description panel
    const [descPanelWidth, setDescPanelWidth] = useState(getDescPanelDefultWidth());

    // whether to show HiGlass' viewConfig on the left-bottom
    const [showVC, setShowVC] = useState<boolean>(false);

    // whether the code editor is read-only
    const [readOnly, setReadOnly] = useState<boolean>(urlGist ? true : false);

    // whether to hide source code on the left
    const [isHideCode, setIsHideCode] = useState<boolean>(IS_SMALL_SCREEN || urlParams.get('full') === 'true' || false);

    // whether to show widgets for responsive window
    const [isResponsive, setIsResponsive] = useState<boolean>(true);
    const [screenSize, setScreenSize] = useState<undefined | { width: number; height: number }>();
    const [visibleScreenSize, setVisibleScreenSize] = useState<undefined | { width: number; height: number }>();

    // whether to show data preview on the right-bottom
    const [isShowDataPreview, setIsShowDataPreview] = useState<boolean>(false);

    // whether to show a find box
    const [isFindCode, setIsFindCode] = useState<boolean | undefined>(undefined);

    // whether to use larger or smaller font
    const [isFontZoomIn, setIsfontZoomIn] = useState<boolean | undefined>(undefined);
    const [isFontZoomOut, setIsfontZoomOut] = useState<boolean | undefined>(undefined);

    // whether description panel is being dragged
    const [isDescResizing, setIsDescResizing] = useState(false);

    // whether to show "about" information
    const [isShowAbout, setIsShowAbout] = useState(false);

    // Resizer `div`
    const descResizerRef = useRef<any>();

    // Drag event for resizing description panel
    const dragX = useRef<any>();

    // for using HiGlass JS API
    // const hgRef = useRef<any>();
    const gosRef = useRef<gosling.GoslingRef>(null);

    const debounceCodeEdit = useRef(
        debounce((code: string, language: EditorLangauge) => {
            if (language == 'json') {
                setCode(code);
            } else {
                setJsCode(code);
            }
        }, 1500)
    );

    // publish event listeners to Gosling.js
    useEffect(() => {
        if (gosRef.current) {
            // gosRef.current.api.subscribe('rawdata', (type, data) => {
            // console.log('rawdata', data);
            // gosRef.current.api.zoomTo('bam-1', `chr${data.data.chr1}:${data.data.start1}-${data.data.end1}`, 2000);
            // gosRef.current.api.zoomTo('bam-2', `chr${data.data.chr2}:${data.data.start2}-${data.data.end2}`, 2000);
            // console.log('click', data.data);
            // TODO: show messages on the right-bottom of the editor
            // gosRef.current.api.subscribe('mouseOver', (type, eventData) => {
            //     setMouseEventInfo({ type: 'mouseOver', data: eventData.data, position: eventData.genomicPosition });
            // });
            // gosRef.current.api.subscribe('click', (type, eventData) => {
            //     setMouseEventInfo({ type: 'click', data: eventData.data, position: eventData.genomicPosition });
            // });
            // Range Select API
            // gosRef.current.api.subscribe('rangeSelect', (type, eventData) => {
            //     console.warn(type, eventData.id, eventData.genomicRange, eventData.data);
            // });
        }
        return () => {
            // gosRef.current.api.unsubscribe('mouseOver');
            // gosRef.current.api.unsubscribe('click');
            // gosRef.current?.api.unsubscribe('rangeSelect');
        };
    }, [gosRef.current]);

    /**
     * Editor mode
     */
    useEffect(() => {
        previewData.current = [];
        setSelectedPreviewData(0);
        if (isImportDemo) {
            const jsonCode = stringifySpec(demo.spec as gosling.GoslingSpec);
            setCode(jsonCode);
            setJsCode(demo.specJs ?? json2js(jsonCode));
        } else if (urlExampleId && !validateExampleId(urlExampleId)) {
            // invalida url example id
            setCode(emptySpec(`Example id "${urlExampleId}" does not exist.`));
            setJsCode(emptySpec(`Example id "${urlExampleId}" does not exist.`));
        } else if (urlSpec) {
            setCode(urlSpec);
            setJsCode(json2js(urlSpec));
        } else if (urlGist) {
            setCode(emptySpec('loading....'));
        } else {
            const jsonCode = stringifySpec(demo.spec as gosling.GoslingSpec);
            setCode(jsonCode);
            setJsCode(demo.specJs ?? json2js(jsonCode));
        }
        setHg(undefined);
    }, [demo]);

    const deviceToResolution = {
        Auto: undefined,
        UHD: { width: 3840, height: 2160 },
        FHD: { width: 1920, height: 1080 },
        'Google Nexus Tablet': { width: 1024, height: 768 },
        'iPhone X': { width: 375, height: 812 }
    };

    const ResponsiveWidget = useMemo(() => {
        return (
            <div
                style={{
                    width: screenSize ? screenSize.width - 20 : 'calc(100% - 20px)',
                    background: 'white',
                    marginBottom: '6px',
                    padding: '10px',
                    height: '20px',
                    lineHeight: '20px'
                }}
            >
                <span
                    style={{
                        marginRight: 10,
                        color: 'gray',
                        verticalAlign: 'middle',
                        display: 'inline-block',
                        marginTop: '2px'
                    }}
                >
                    {getIconSVG(ICONS.SCREEN, 16, 16)}
                </span>
                <span className="screen-size-dropdown">
                    <select
                        style={{ width: '80px' }}
                        onChange={e => {
                            const device = e.target.value;
                            if (Object.keys(deviceToResolution).includes(device)) {
                                setScreenSize((deviceToResolution as any)[device]);
                                setVisibleScreenSize((deviceToResolution as any)[device]);
                            }
                        }}
                    >
                        {[...Object.keys(deviceToResolution)].map(d =>
                            d !== '-' ? (
                                <option key={d} value={d}>
                                    {d}
                                </option>
                            ) : (
                                // separator (https://stackoverflow.com/questions/899148/html-select-option-separator)
                                <optgroup label="──────────"></optgroup>
                            )
                        )}
                    </select>
                </span>
                <span style={{ marginLeft: '20px', visibility: screenSize ? 'visible' : 'collapse' }}>
                    <span style={{ marginRight: 10, color: '#EEBF4D' }}>{getIconSVG(ICONS.RULER, 12, 12)}</span>
                    <input
                        type="number"
                        min="350"
                        max="3000"
                        value={visibleScreenSize?.width}
                        onChange={e => {
                            const width = +e.target.value >= 350 ? +e.target.value : 350;
                            setVisibleScreenSize({ width: +e.target.value, height: screenSize?.height ?? 1000 });
                            setScreenSize({ width, height: screenSize?.height ?? 1000 });
                        }}
                    />
                    {' x '}
                    <input
                        type="number"
                        min="100"
                        max="3000"
                        value={visibleScreenSize?.height}
                        onChange={e => {
                            const height = +e.target.value >= 100 ? +e.target.value : 100;
                            setVisibleScreenSize({ width: screenSize?.width ?? 1000, height: +e.target.value });
                            setScreenSize({ width: screenSize?.width ?? 1000, height });
                        }}
                    />
                    <span
                        style={{
                            marginLeft: 10,
                            color: 'gray',
                            verticalAlign: 'middle',
                            display: 'inline-block',
                            marginTop: '2px',
                            cursor: 'pointer'
                        }}
                        onClick={() => {
                            setVisibleScreenSize({
                                width: visibleScreenSize?.height ?? 1000,
                                height: visibleScreenSize?.width ?? 1000
                            });
                            setScreenSize({ width: screenSize?.height ?? 1000, height: screenSize?.width ?? 1000 });
                        }}
                    >
                        {getIconSVG(ICONS.REPEAT, 20, 20)}
                    </span>
                </span>
            </div>
        );
    }, [screenSize]);

    useEffect(() => {
        let active = true;

        if (!urlGist || typeof urlGist !== 'string') return undefined;

        fetchSpecFromGist(urlGist)
            .then(({ code, jsCode, language, description, title }) => {
                if (active) {
                    setReadOnly(false);
                    setJsCode(jsCode);
                    setCode(code);
                    changeLanguage(language);
                    setGistTitle(title);
                    setDescription(description);
                }
            })
            .catch(error => {
                if (active) {
                    setReadOnly(false);
                    setCode(emptySpec(error));
                    setJsCode(emptySpec(error));
                    setDescription(undefined);
                    setGistTitle('Error loading gist! See code for details.');
                }
            });

        return () => {
            setReadOnly(false);
            active = false;
        };
    }, [urlGist]);

    const runSpecUpdateVis = useCallback(
        (run?: boolean) => {
            if (isEqual(emptySpec(), code) && isEqual(emptySpec(), jsCode)) {
                // this means we do not have to compile. This is when we are in the middle of loading data from gist.
                return;
            }

            let editedGos;
            let valid;

            if (language === 'json') {
                try {
                    editedGos = JSON.parse(stripJsonComments(code));
                    valid = gosling.validateGoslingSpec(editedGos);
                    setLog(valid);
                } catch (e) {
                    const message = '✘ Cannnot parse the code.';
                    console.warn(message);
                    setLog({ message, state: 'error' });
                }
                if (!editedGos || valid?.state !== 'success' || (!autoRun && !run)) return;

                setGoslingSpec(editedGos);
            } else if (language === 'typescript') {
                transpile(jsCode)
                    .then(toJavaScriptDataURI)
                    .then(uri => import(/* @vite-ignore */ uri))
                    .then(ns => {
                        const editedGos = ns.spec;
                        if (urlGist && !isImportDemo) {
                            setCode(stringifySpec(editedGos));
                        }
                        valid = gosling.validateGoslingSpec(editedGos);
                        setLog(valid);
                        if (!editedGos || valid?.state !== 'success' || (!autoRun && !run)) return;
                        setGoslingSpec(editedGos);
                    })
                    .catch(e => {
                        const message = '✘ Cannnot parse the code.';
                        console.warn(message, e);
                        setLog({ message, state: 'error' });
                    });
            } else {
                setLog({ message: `${language} is not supported`, state: 'error' });
            }
        },
        [code, jsCode, autoRun, language, readOnly]
    );

    /**
     * Update theme of the editor based on the theme of Gosling visualizations
     */
    // useEffect(() => {
    //     const gosTheme = getTheme(goslingSpec?.theme);
    //     if (gosTheme.base !== theme) {
    //         setTheme(gosTheme.base);
    //     }
    // }, [goslingSpec]);

    /**
     * Things to do upon spec change
     */
    useEffect(() => {
        const newIsResponsive =
            typeof goslingSpec?.responsiveSize === 'undefined'
                ? false
                : typeof goslingSpec?.responsiveSize === 'boolean'
                ? goslingSpec?.responsiveSize === true
                : typeof goslingSpec?.responsiveSize === 'object'
                ? goslingSpec?.responsiveSize.width === true || goslingSpec?.responsiveSize.height === true
                : false;
        if (newIsResponsive !== isResponsive && newIsResponsive) {
            setScreenSize(undefined); // reset the screen
            setVisibleScreenSize(undefined);
        }
        setIsResponsive(newIsResponsive);
    }, [goslingSpec]);

    /**
     * Subscribe preview data that is being processed in the Gosling tracks.
     */
    useEffect(() => {
        // We want to show data preview in the editor.
        const token = PubSub.subscribe('data-preview', (_: string, data: PreviewData) => {
            // Data with different `dataConfig` is shown separately in data preview.
            const id = `${data.dataConfig}`;
            const newPreviewData = previewData.current.filter(d => d.id !== id);
            previewData.current = [...newPreviewData, { ...data, id }];
        });
        return () => {
            PubSub.unsubscribe(token);
        };
    });

    /**
     * Render visualization when edited
     */
    useEffect(() => {
        previewData.current = [];
        setSelectedPreviewData(0);
        runSpecUpdateVis();
    }, [code, jsCode, autoRun, language, theme]);

    // Uncommnet below to use HiGlass APIs
    // useEffect(() => {
    //     if(hgRef.current) {
    //         hgRef.current.api.activateTool('select');
    //     }
    // }, [hg, hgRef]); // TODO: should `hg` be here?

    function getDataPreviewInfo(dataConfig: string) {
        // Detailed information of data config to show in the editor
        const dataConfigObj = JSON.parse(dataConfig);
        if (!dataConfigObj.data?.type) {
            // We do not have enough information
            return '';
        }

        let info = '';
        if (dataConfigObj.data) {
            Object.keys(dataConfigObj.data).forEach(key => {
                if (typeof dataConfigObj.data[key] === 'object') {
                    info += `${JSON.stringify(dataConfigObj.data[key])} | `;
                } else {
                    info += `${dataConfigObj.data[key]} | `;
                }
            });
        }

        return info.slice(0, info.length - 2);
    }

    // Set up the d3-drag handler functions (started, ended, dragged).
    const started = useCallback(() => {
        if (!hideDescription) {
            // Drag is enabled only when the description panel is visible
            dragX.current = d3Event.sourceEvent.clientX;
            setIsDescResizing(true);
        }
    }, [dragX, descPanelWidth]);

    const dragged = useCallback(() => {
        if (dragX.current) {
            const diff = d3Event.sourceEvent.clientX - dragX.current;
            setDescPanelWidth(descPanelWidth - diff);
        }
    }, [dragX, descPanelWidth]);

    const ended = useCallback(() => {
        dragX.current = null;
        setIsDescResizing(false);
    }, [dragX, descPanelWidth]);

    // Detect drag events for the resize element.
    useEffect(() => {
        const resizer = descResizerRef.current;

        const drag = d3Drag().on('start', started).on('drag', dragged).on('end', ended);

        d3Select(resizer).call(drag);

        return () => {
            d3Select(resizer).on('.drag', null);
        };
    }, [descResizerRef, started, dragged, ended]);

    function openDescription() {
        setDescPanelWidth(getDescPanelDefultWidth());
        setHideDescription(false);
    }

    function closeDescription() {
        setHideDescription(true);
    }

    // console.log('editor.render()');
    return (
        <>
            <div
                className={`demo-navbar ${theme === 'dark' ? 'dark' : ''}`}
                onClick={() => {
                    if (!gosRef.current) return;

                    // To test APIs, uncomment the following code.
                    // // ! Be aware that the first view is for the title/subtitle track. So navigation API does not work.
                    // const id = gosRef.current.api.getViewIds()?.[1]; //'view-1';
                    // if(id) {
                    //     gosRef.current.api.zoomToExtent(id);
                    // }
                    //
                    // // Static visualization rendered in canvas
                    // const { canvas } = gosRef.current.api.getCanvas({
                    //     resolution: 1,
                    //     transparentBackground: true,
                    // });
                    // const testDiv = document.getElementById('preview-container');
                    // if(canvas && testDiv) {
                    //     testDiv.appendChild(canvas);
                    // }
                }}
            >
                <span
                    style={{ cursor: 'pointer', lineHeight: '40px' }}
                    onClick={() => window.open(`${window.location.pathname}`, '_blank')}
                >
                    <span className="logo">{GoslingLogoSVG(20, 20)}</span>
                    Gosling.js Editor
                </span>
                {urlSpec && <small> Displaying a custom spec contained in URL</small>}
                {gistTitle && !IS_SMALL_SCREEN && (
                    <>
                        <span className="gist-title">{gistTitle}</span>
                        <span
                            title="Open GitHub Gist"
                            style={{ marginLeft: 10 }}
                            className="description-github-button"
                            onClick={() => window.open(`https://gist.github.com/${urlGist}`, '_blank')}
                        >
                            {getIconSVG(ICONS.UP_RIGHT, 14, 14)}
                        </span>
                    </>
                )}
                <span className="demo-label" onClick={() => setShowExamples(true)}>
                    <b>{demo.group}</b>: {demo.name}
                </span>
                {/* <span className="demo-dropdown" hidden={urlSpec !== null || urlGist !== null || urlExampleId !== ''}>
                    <select
                        style={{ maxWidth: IS_SMALL_SCREEN ? window.innerWidth - 180 : 'none' }}
                        onChange={e => {
                            setDemo({ id: e.target.value, ...examples[e.target.value] } as any);
                        }}
                        value={demo.id}
                    >
                        {SHOWN_EXAMPLE_LIST.map(d => (
                            <option key={d.id} value={d.id}>
                                {d.name + (d.underDevelopment ? ' (under development)' : '')}
                            </option>
                        ))}
                    </select>
                </span> */}
                {expertMode ? (
                    <select
                        style={{ maxWidth: IS_SMALL_SCREEN ? window.innerWidth - 180 : 'none' }}
                        onChange={e => {
                            if (Object.keys(Themes).indexOf(e.target.value) !== -1) {
                                setTheme(e.target.value as any);
                            }
                        }}
                        defaultValue={theme as any}
                    >
                        {Object.keys(Themes).map((d: string) => (
                            <option key={d} value={d}>
                                {`Theme: ${d}`}
                            </option>
                        ))}
                    </select>
                ) : null}
                {demo.underDevelopment ? (
                    <span
                        style={{
                            paddingLeft: 12,
                            fontStyle: 'normal',
                            fontSize: 13
                        }}
                    >
                        ? This example is under development ?
                    </span>
                ) : null}
                <input type="hidden" id="spec-url-exporter" />
                {description ? (
                    <span title="Open Textual Description" className="description-button" onClick={openDescription}>
                        {getIconSVG(ICONS.INFO_CIRCLE, 23, 23)}
                    </span>
                ) : null}
            </div>
            {/* ------------------------ Main View ------------------------ */}
            <div className={`editor ${theme === 'dark' ? 'dark' : ''}`}>
                <SplitPane className="side-panel-spliter" split="vertical" defaultSize="50px" allowResize={false}>
                    <div className={`side-panel ${theme === 'dark' ? 'dark' : ''}`}>
                        <span
                            title="Example Gallery"
                            className="side-panel-button"
                            onClick={() => setShowExamples(!showExamples)}
                        >
                            {showExamples ? getIconSVG(ICONS.GRID, 20, 20, '#E18343') : getIconSVG(ICONS.GRID)}
                            <br />
                            EXAMPLE
                        </span>
                        <span
                            title="Automatically update visualization upon editing code"
                            className="side-panel-button"
                            onClick={() => setAutoRun(!autoRun)}
                        >
                            {autoRun
                                ? getIconSVG(ICONS.TOGGLE_ON, 23, 23, '#E18343')
                                : getIconSVG(ICONS.TOGGLE_OFF, 23, 23)}
                            <br />
                            AUTO
                            <br />
                            RUN
                        </span>
                        <span title="Run Code" className="side-panel-button" onClick={() => runSpecUpdateVis(true)}>
                            {getIconSVG(ICONS.PLAY, 23, 23)}
                            <br />
                            RUN
                        </span>
                        <span
                            title="Find"
                            className="side-panel-button"
                            onClick={() => {
                                setIsFindCode(!isFindCode);
                            }}
                        >
                            {getIconSVG(ICONS.FIND, 23, 23)}
                            <br />
                            FIND
                        </span>
                        <span
                            title="Use Larger Font"
                            className="side-panel-button"
                            onClick={() => {
                                setIsfontZoomIn(!isFontZoomIn);
                            }}
                        >
                            {getIconSVG(ICONS.TEXT, 23, 23)}
                            +
                            <br />
                            LARGER
                        </span>
                        <span
                            title="Use Larger Font"
                            className="side-panel-button"
                            onClick={() => {
                                setIsfontZoomOut(!isFontZoomOut);
                            }}
                        >
                            {getIconSVG(ICONS.TEXT, 15, 15)}
                            -
                            <br />
                            SMALLER
                        </span>
                        <span
                            title="Show or hide a code panel"
                            className="side-panel-button"
                            onClick={() => setIsHideCode(!isHideCode)}
                        >
                            {getIconSVG(ICONS.SPLIT, 23, 23)}
                            <br />
                            LAYOUT
                        </span>
                        <span
                            title="Show or hide a data preview"
                            className="side-panel-button"
                            onClick={() => setIsShowDataPreview(!isShowDataPreview)}
                        >
                            {getIconSVG(ICONS.TABLE, 23, 23)}
                            <br />
                            DATA
                            <br />
                            PREVIEW
                        </span>
                        <span
                            title="Save PNG file"
                            className="side-panel-button"
                            onClick={() => {
                                gosRef.current?.api.exportPng();
                            }}
                        >
                            {getIconSVG(ICONS.IMAGE, 23, 23)}
                            <br />
                            PNG
                        </span>
                        <span
                            title="Save PDF file"
                            className="side-panel-button"
                            onClick={() => {
                                gosRef.current?.api.exportPdf();
                            }}
                        >
                            {getIconSVG(ICONS.PDF, 23, 23)}
                            <br />
                            PDF
                        </span>
                        <span
                            title="Save HTML file"
                            className="side-panel-button"
                            onClick={() => {
                                // TODO (05-02-2022): Release a support of `responsiveSize` on `.embed()` first
                                const spec = { ...goslingSpec, responsiveSize: false } as gosling.GoslingSpec;

                                const a = document.createElement('a');
                                a.setAttribute(
                                    'href',
                                    `data:text/plain;charset=utf-8,${encodeURIComponent(
                                        getHtmlTemplate(stringifySpec(spec))
                                    )}`
                                );
                                a.download = 'gosling-visualization.html';
                                document.body.appendChild(a);
                                a.click();
                                document.body.removeChild(a);
                            }}
                        >
                            {getIconSVG(ICONS.HTML, 23, 23)}
                        </span>
                        <span
                            title={
                                stringifySpec(goslingSpec).length <= LIMIT_CLIPBOARD_LEN
                                    ? `Copy unique URL of current view to clipboard (limit: ${LIMIT_CLIPBOARD_LEN} characters)`
                                    : `The current code contains characters more than ${LIMIT_CLIPBOARD_LEN}`
                            }
                            className={
                                stringifySpec(goslingSpec).length <= LIMIT_CLIPBOARD_LEN
                                    ? 'side-panel-button'
                                    : 'side-panel-button side-panel-button-not-active'
                            }
                            onClick={() => {
                                if (stringifySpec(goslingSpec).length <= LIMIT_CLIPBOARD_LEN) {
                                    // copy the unique url to clipboard using `<input/>`
                                    const crushedSpec = encodeURIComponent(JSONCrush.crush(stringifySpec(goslingSpec)));
                                    const url = `${window.location.origin}${window.location.pathname}?full=${isHideCode}&spec=${crushedSpec}`;

                                    navigator.clipboard
                                        .writeText(url)
                                        .then(() =>
                                            // eslint-disable-next-line no-alert
                                            alert(`URL of the current visualization is copied to your clipboard! `)
                                        )
                                        .catch(
                                            // eslint-disable-next-line no-alert
                                            e => alert(`something went wrong ${e}`)
                                        );
                                }
                            }}
                        >
                            {getIconSVG(ICONS.LINK, 23, 23)}
                            <br />
                            SAVE
                            <br />
                            URL
                        </span>
                        <span
                            title="Expert mode that turns on additional features, such as theme selection"
                            className="side-panel-button"
                            onClick={() => setExpertMode(!expertMode)}
                        >
                            {expertMode ? getIconSVG(ICONS.TOGGLE_ON, 23, 23, '#E18343') : getIconSVG(ICONS.TOGGLE_OFF)}
                            <br />
                            EXPERT
                            <br />
                            MODE
                        </span>
                        <span
                            title="Open GitHub repository"
                            className="side-panel-button"
                            onClick={() => window.open('https://github.com/gosling-lang/gosling.js', '_blank')}
                        >
                            {getIconSVG(ICONS.GITHUB, 23, 23)}
                            <br />
                            GITHUB
                        </span>
                        <span
                            title="Open Docs"
                            className="side-panel-button"
                            onClick={() => window.open('http://gosling-lang.org/docs/', '_blank')}
                        >
                            {getIconSVG(ICONS.DOCS, 23, 23)}
                            <br />
                            DOCS
                        </span>
                        <span title="About" className="side-panel-button" onClick={() => setIsShowAbout(!isShowAbout)}>
                            {getIconSVG(ICONS.INFO_RECT_FILLED, 23, 23)}
                            <br />
                            ABOUT
                        </span>
                    </div>
                    <SplitPane
                        split="vertical"
                        defaultSize={'calc(40%)'}
                        size={isHideCode ? '0px' : 'calc(40%)'}
                        minSize="0px"
                    >
                        <SplitPane
                            split="horizontal"
                            defaultSize={`calc(100% - ${BOTTOM_PANEL_HEADER_HEIGHT}px)`}
                            maxSize={window.innerHeight - EDITOR_HEADER_HEIGHT - BOTTOM_PANEL_HEADER_HEIGHT}
                            onChange={(size: number) => {
                                const secondSize = window.innerHeight - EDITOR_HEADER_HEIGHT - size;
                                if (secondSize > BOTTOM_PANEL_HEADER_HEIGHT && !showVC) {
                                    setShowVC(true);
                                } else if (secondSize <= BOTTOM_PANEL_HEADER_HEIGHT && showVC) {
                                    // hide the viewConfig view when no enough space assigned
                                    setShowVC(false);
                                }
                            }}
                        >
                            {/* Gosling Editor */}
                            <>
                                <div className="tabEditor">
                                    <div className="tab">
                                        <button
                                            className={`tablinks ${language == 'json' && 'active'}`}
                                            onClick={() => {
                                                changeLanguage('json');
                                                setLog({ message: '', state: 'success' });
                                            }}
                                        >
                                            JSON {` `}
                                            <span className="tooltip">
                                                {getIconSVG(ICONS.INFO_CIRCLE, 10, 10)}
                                                <span className="tooltiptext">
                                                    In this JSON editor, the whole JSON object will be used to create
                                                    Gosling visualizations.
                                                </span>
                                            </span>
                                        </button>
                                        <button
                                            className={`tablinks ${language == 'typescript' && 'active'}`}
                                            onClick={() => {
                                                changeLanguage('typescript');
                                                setLog({ message: '', state: 'success' });
                                            }}
                                        >
                                            JavaScript{` `}
                                            <span className="tooltip">
                                                {getIconSVG(ICONS.INFO_CIRCLE, 10, 10)}
                                                <span className="tooltiptext">
                                                    In this JavaScript Editor, the variable{` `}
                                                    <code style={{ backgroundColor: '#e18343' }}>spec</code> will be
                                                    used to create Gosling visualizations.
                                                </span>
                                            </span>
                                        </button>
                                    </div>

                                    <div className={`tabContent ${language == 'json' ? 'show' : 'hide'}`}>
                                        <EditorPanel
                                            code={code}
                                            readOnly={readOnly}
                                            openFindBox={isFindCode}
                                            fontZoomIn={isFontZoomIn}
                                            fontZoomOut={isFontZoomOut}
                                            onChange={debounceCodeEdit.current}
                                            isDarkTheme={theme === 'dark'}
                                            language="json"
                                        />
                                    </div>
                                    <div className={`tabContent ${language == 'typescript' ? 'show' : 'hide'}`}>
                                        <EditorPanel
                                            code={jsCode}
                                            readOnly={readOnly}
                                            openFindBox={isFindCode}
                                            fontZoomIn={isFontZoomIn}
                                            fontZoomOut={isFontZoomOut}
                                            onChange={debounceCodeEdit.current}
                                            isDarkTheme={theme === 'dark'}
                                            language="typescript"
                                        />
                                    </div>
                                </div>
                                <div className={`compile-message compile-message-${log.state}`}>{log.message}</div>
                            </>
                            {/* HiGlass View Config */}
                            <SplitPane split="vertical" defaultSize="100%">
                                <>
                                    <div className={`editor-header ${theme === 'dark' ? 'dark' : ''}`}>
                                        Compiled HiGlass ViewConfig (Read Only)
                                    </div>
                                    <div style={{ height: '100%', visibility: showVC ? 'visible' : 'hidden' }}>
                                        <EditorPanel
                                            code={stringify(hg)}
                                            readOnly={true}
                                            isDarkTheme={theme === 'dark'}
                                            language="json"
                                        />
                                    </div>
                                </>
                                {/**
                                 * TODO: This is only for showing a scroll view for the higlass view config editor
                                 * Remove the below line and the nearest SplitPane after figuring out a better way
                                 * of showing the scroll view.
                                 */}
                                <></>
                            </SplitPane>
                        </SplitPane>
                        <ErrorBoundary>
                            <SplitPane
                                split="horizontal"
                                defaultSize={`calc(100% - ${BOTTOM_PANEL_HEADER_HEIGHT}px)`}
                                size={isShowDataPreview ? '40%' : `calc(100% - ${BOTTOM_PANEL_HEADER_HEIGHT}px)`}
                                maxSize={window.innerHeight - EDITOR_HEADER_HEIGHT - BOTTOM_PANEL_HEADER_HEIGHT}
                            >
                                <div
                                    id="preview-container"
                                    className={`preview-container ${theme === 'dark' ? 'dark' : ''}`}
                                >
                                    {isResponsive && !IS_SMALL_SCREEN ? ResponsiveWidget : null}
                                    <div
                                        style={{
                                            width: isResponsive && screenSize?.width ? screenSize.width : '100%',
                                            height:
                                                isResponsive && screenSize?.height
                                                    ? screenSize.height
                                                    : 'calc(100% - 50px)',
                                            background: isResponsive ? 'white' : 'none'
                                        }}
                                    >
                                        <gosling.GoslingComponent
                                            ref={gosRef}
                                            spec={goslingSpec}
                                            theme={theme}
                                            padding={60}
                                            margin={0}
                                            border={'none'}
                                            id={'goslig-component-root'}
                                            className={'goslig-component'}
                                            experimental={{ reactive: true }}
                                            compiled={(_, h) => {
                                                setHg(h);
                                            }}
                                        />
                                    </div>
                                    {/* {expertMode && false ? (
                                        <div
                                            style={{
                                                position: 'absolute',
                                                right: '2px',
                                                bottom: '2px',
                                                padding: '20px',
                                                background: '#FAFAFAAA',
                                                border: '1px solid black'
                                            }}
                                        >
                                            <div style={{ fontWeight: 'bold' }}>
                                                {`${mouseEventInfo?.data.length} Marks Selected By Mouse ${
                                                    mouseEventInfo?.type === 'click' ? 'Click' : 'Over'
                                                }`}
                                            </div>
                                            <div style={{}}>{`The event occurs at ${mouseEventInfo?.position}`}</div>
                                            <table>
                                                {mouseEventInfo?.data && mouseEventInfo?.data.length !== 0
                                                    ? Object.entries(mouseEventInfo?.data[0]).map(([k, v]) => (
                                                          <tr key={k}>
                                                              <td>{k}</td>
                                                              <td>{v}</td>
                                                          </tr>
                                                      ))
                                                    : null}
                                            </table>
                                        </div>
                                    ) : null} */}
                                </div>
                                <SplitPane split="vertical" defaultSize="100%">
                                    <>
                                        <div
                                            className={`editor-header ${theme === 'dark' ? 'dark' : ''}`}
                                            style={{ cursor: 'pointer' }}
                                            onClick={() => setIsShowDataPreview(!isShowDataPreview)}
                                        >
                                            Data Preview (~100 Rows, Data Before Transformation)
                                        </div>
                                        <div className="editor-data-preview-panel">
                                            <div
                                                title="Refresh preview data"
                                                className="data-preview-refresh-button"
                                                onClick={() => setRefreshData(!refreshData)}
                                            >
                                                {getIconSVG(ICONS.REFRESH, 23, 23)}
                                                <br />
                                                {'REFRESH DATA'}
                                            </div>
                                            {previewData.current.length > selectedPreviewData &&
                                            previewData.current[selectedPreviewData] &&
                                            previewData.current[selectedPreviewData].data.length > 0 ? (
                                                <>
                                                    <div className="editor-data-preview-tab">
                                                        {previewData.current.map((d: PreviewData, i: number) => (
                                                            <button
                                                                className={
                                                                    i === selectedPreviewData
                                                                        ? 'selected-tab'
                                                                        : 'unselected-tab'
                                                                }
                                                                key={JSON.stringify(d)}
                                                                onClick={() => setSelectedPreviewData(i)}
                                                            >
                                                                {`${(
                                                                    JSON.parse(d.dataConfig).data.type as string
                                                                ).toLocaleLowerCase()} `}
                                                                <small>{i}</small>
                                                            </button>
                                                        ))}
                                                    </div>
                                                    <div className="editor-data-preview-tab-info">
                                                        {getDataPreviewInfo(
                                                            previewData.current[selectedPreviewData].dataConfig
                                                        )}
                                                    </div>
                                                    <div className="editor-data-preview-table">
                                                        <table>
                                                            <tbody>
                                                                <tr>
                                                                    {Object.keys(
                                                                        previewData.current[selectedPreviewData].data[0]
                                                                    ).map((field: string, i: number) => (
                                                                        <th key={i}>{field}</th>
                                                                    ))}
                                                                </tr>
                                                                {previewData.current[selectedPreviewData].data.map(
                                                                    (row: Datum, i: number) => (
                                                                        <tr key={i}>
                                                                            {Object.keys(row).map(
                                                                                (field: string, j: number) => (
                                                                                    <td key={j}>
                                                                                        {row[field]?.toString()}
                                                                                    </td>
                                                                                )
                                                                            )}
                                                                        </tr>
                                                                    )
                                                                )}
                                                            </tbody>
                                                        </table>
                                                    </div>
                                                </>
                                            ) : null}
                                        </div>
                                    </>
                                    {/**
                                     * TODO: This is only for showing a scroll view for the higlass view config editor
                                     * Remove the below line and the nearest SplitPane after figuring out a better way
                                     * of showing the scroll view.
                                     */}
                                    <></>
                                </SplitPane>
                            </SplitPane>
                        </ErrorBoundary>
                    </SplitPane>
                </SplitPane>
                {/* Description Panel */}
                <div
                    className={`description ${hideDescription ? '' : 'description-shadow '}${
                        isDescResizing ? '' : 'description-transition'
                    } ${theme === 'dark' ? 'dark' : ''}`}
                    style={{ width: !description || hideDescription ? 0 : descPanelWidth }}
                >
                    <div
                        className={hideDescription ? 'description-resizer-disabled' : 'description-resizer'}
                        ref={descResizerRef}
                    />
                    <div className="description-wrapper">
                        <header>
                            <button className="hide-description-button" onClick={closeDescription}>
                                Close
                            </button>
                            <br />
                            <br />
                            <span
                                title="Open GitHub Gist"
                                className="description-github-button"
                                onClick={() => window.open(`https://gist.github.com/${urlGist}`, '_blank')}
                            >
                                {getIconSVG(ICONS.UP_RIGHT, 14, 14)} Open GitHub Gist to see raw files.
                            </span>
                        </header>
                        {description && <ReactMarkdown plugins={[gfm]} source={description} />}
                    </div>
                </div>
                {/* About Modal View */}
                <div
                    className={isShowAbout ? 'about-modal-container' : 'about-modal-container-hidden'}
                    onClick={() => setIsShowAbout(false)}
                ></div>
                <div className={isShowAbout ? 'about-modal' : 'about-modal-hidden'}>
                    <span
                        className="about-model-close-button"
                        onClick={() => {
                            setIsShowAbout(false);
                        }}
                    >
                        {getIconSVG(ICONS.CLOSE, 30, 30)}
                    </span>
                    <div>
                        <span className="logo">{GoslingLogoSVG(80, 80)}</span>
                    </div>
                    <h3>Gosling.js Editor</h3>
                    {`Gosling.js v${gosling.version}`}
                    <br />
                    <br />
                    <a
                        href="https://github.com/gosling-lang/gosling.js/blob/master/CHANGELOG.md"
                        target="_blank"
                        rel="noopener noreferrer"
                    >
                        Change Log
                    </a>
                    <br />
                    <br />
                    <a
                        href="https://github.com/gosling-lang/gosling.js/blob/master/LICENSE.md"
                        target="_blank"
                        rel="noopener noreferrer"
                    >
                        MIT License
                    </a>
                    <br />
                    <br />
                    <h4>Team</h4>
                    <span>
                        Sehi L&apos;Yi (
                        <a href="https://twitter.com/sehi_lyi" target="_blank" rel="noopener noreferrer">
                            @sehi_lyi
                        </a>
                        )
                        <br />
                        Qianwen Wang (
                        <a href="https://twitter.com/WangQianwenToo" target="_blank" rel="noopener noreferrer">
                            @WangQianwenToo
                        </a>
                        )
                        <br />
                        Fritz Lekschas (
                        <a href="https://twitter.com/flekschas" target="_blank" rel="noopener noreferrer">
                            @flekschas
                        </a>
                        )
                        <br />
                        Nils Gehlenborg (
                        <a href="https://twitter.com/gehlenborg" target="_blank" rel="noopener noreferrer">
                            @gehlenborg
                        </a>
                        )
                    </span>
                    <br />
                    <br />
                    <a href="http://gehlenborglab.org/" target="_blank" rel="noopener noreferrer">
                        Gehlenborg Lab
                    </a>
                    , Harvard Medical School
                </div>
            </div>
            {/* ---------------------- Example Gallery -------------------- */}
            <div
                className={showExamples ? 'about-modal-container' : 'about-modal-container-hidden'}
                onClick={() => setShowExamples(false)}
            />
            <div
                className="example-gallery-container"
                style={{
                    visibility: showExamples ? 'visible' : 'collapse'
                }}
            >
                <div
                    className="example-gallery-sidebar"
                    style={{
                        opacity: showExamples ? 1 : 0
                    }}
                >
                    {ExampleGroups.filter(_ => _.name !== 'Doc' && _.name !== 'Unassigned').map(group => {
                        return (
                            <>
                                <a className="siderbar-group" key={group.name} href={`#${group.name}`}>
                                    {group.name}
                                </a>
                                {Object.entries(examples)
                                    .filter(d => !d[1].hidden)
                                    .filter(d => d[1].group === group.name)
                                    .map(d => (
                                        <a key={d[1].name} href={`#${d[1].group}_${d[1].name}`}>
                                            {d[1].name}
                                        </a>
                                    ))}
                            </>
                        );
                    })}
                </div>
                <div
                    className="example-gallery"
                    style={{
                        opacity: showExamples ? 1 : 0
                    }}
                >
                    <h1>Gosling.js Examples</h1>
                    {ExampleGroups.filter(_ => _.name !== 'Doc' && _.name !== 'Unassigned').map(group => {
                        return (
                            <>
                                <h2 id={`${group.name}`}>{group.name}</h2>
                                <h5>{group.description}</h5>
                                <div className="example-group" key={group.name}>
                                    {Object.entries(examples)
                                        .filter(d => !d[1].hidden)
                                        .filter(d => d[1].group === group.name)
                                        .map(d => {
                                            return (
                                                <div
                                                    id={`${d[1].group}_${d[1].name}`}
                                                    title={d[1].name}
                                                    key={d[0]}
                                                    className="example-card"
                                                    onClick={() => {
                                                        setShowExamples(false);
                                                        closeDescription();
                                                        setIsImportDemo(true);
                                                        setDemo({ id: d[0], ...examples[d[0]] } as any);
                                                    }}
                                                >
                                                    <div
                                                        className="example-card-bg"
                                                        style={{
                                                            backgroundImage: d[1].image ? `url(${d[1].image})` : 'none'
                                                        }}
                                                    />
                                                    <div className="example-card-name">{d[1].name}</div>
                                                </div>
                                            );
                                        })}
                                </div>
                            </>
                        );
                    })}
                    {/* Just an margin on the bottom */}
                    <div style={{ height: '40px' }}></div>
                </div>
            </div>
        </>
    );
}
Example #18
Source File: Select.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
_Select = <T extends unknown>(
  {
    label = '',
    labelTextProps,
    labelPosition = 'top',
    items,
    placeholder = 'Select option',
    error,
    value,
    valueLabel,
    disabled,
    onChange,
    containerRef,
    size = 'regular',
    iconLeft,
    ...inputBaseProps
  }: SelectProps<T>,
  ref: React.ForwardedRef<HTMLDivElement>
) => {
  const itemsValues = items.map((item) => item.value)

  const handleItemSelect = (changes: UseSelectStateChange<T>) => {
    onChange?.(changes.selectedItem)
  }

  const {
    isOpen,
    selectedItem: selectedItemValue,
    getToggleButtonProps,
    getLabelProps,
    getMenuProps,
    highlightedIndex,
    getItemProps,
  } = useSelect({
    items: itemsValues,
    selectedItem: value !== undefined ? value : null,
    onSelectedItemChange: handleItemSelect,
  })
  const selectedItem = useMemo(
    () => items.find((item) => isEqual(item.value, selectedItemValue)),
    [items, selectedItemValue]
  )

  return (
    <InputBase error={error} disabled={disabled} {...inputBaseProps} isSelect={true}>
      <SelectWrapper labelPosition={labelPosition}>
        <SelectLabel {...getLabelProps()} ref={ref} tabIndex={disabled ? -1 : 0}>
          {label && (
            <StyledLabelText variant="t200" {...labelTextProps} labelPosition={labelPosition}>
              {label}
            </StyledLabelText>
          )}
        </SelectLabel>
        <SelectMenuWrapper>
          <SelectButton
            disabled={disabled}
            error={error}
            filled={selectedItemValue != null}
            isOpen={isOpen}
            type="button"
            {...getToggleButtonProps()}
            tabIndex={disabled ? -1 : 0}
            size={size}
          >
            {iconLeft && <NodeContainer>{iconLeft}</NodeContainer>}
            <ValueContainer hasIconLeft={!!iconLeft}>
              {(valueLabel ?? '') + (selectedItem?.name || placeholder)}
            </ValueContainer>
            {selectedItem?.badgeText && <StyledPill label={selectedItem.badgeText} />}
            <SvgActionChevronB className="chevron-bottom" />
          </SelectButton>
          <SelectMenu isOpen={isOpen} {...getMenuProps()}>
            {isOpen &&
              items.map((item, index) => {
                const itemProps = { ...getItemProps({ item: item.value, index }) }
                if (item.hideInMenu) return null
                return (
                  <SelectOption
                    isSelected={highlightedIndex === index}
                    key={`${item.name}-${index}`}
                    {...itemProps}
                    onClick={(e) => {
                      item.onClick?.()
                      itemProps.onClick(e)
                    }}
                  >
                    {item.tooltipText && (
                      <Tooltip
                        headerText={item.tooltipHeaderText}
                        text={item.tooltipText}
                        placement="top-end"
                        offsetX={6}
                        offsetY={12}
                      >
                        <StyledSvgGlyphInfo />
                      </Tooltip>
                    )}
                    {item?.menuName ?? item?.name}
                  </SelectOption>
                )
              })}
          </SelectMenu>
        </SelectMenuWrapper>
      </SelectWrapper>
    </InputBase>
  )
}
Example #19
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);
}
Example #20
Source File: useInfiniteGrid.ts    From atlas with GNU General Public License v3.0 4 votes vote down vote up
useInfiniteGrid = <
  TRawData,
  TPaginatedData extends PaginatedData<unknown>,
  TArgs extends PaginatedDataArgs
>({
  query,
  dataAccessor,
  isReady,
  targetRowsCount,
  itemsPerRow,
  skipCount,
  onScrollToBottom,
  onError,
  queryVariables,
  onDemand,
  onDemandInfinite,
  activatedInfinteGrid,
}: UseInfiniteGridParams<TRawData, TPaginatedData, TArgs>): UseInfiniteGridReturn<TPaginatedData> => {
  const targetDisplayedItemsCount = targetRowsCount * itemsPerRow
  const targetLoadedItemsCount = targetDisplayedItemsCount + skipCount

  const queryVariablesRef = useRef(queryVariables)

  const {
    loading,
    data: rawData,
    error,
    fetchMore,
    refetch,
    networkStatus,
  } = useQuery<TRawData, TArgs>(query, {
    notifyOnNetworkStatusChange: true,
    skip: !isReady,
    variables: {
      ...queryVariables,
      first: targetDisplayedItemsCount + PREFETCHED_ITEMS_COUNT,
    },
    onError,
  })

  const data = dataAccessor(rawData)

  const loadedItemsCount = data?.edges.length ?? 0
  const allItemsLoaded = data ? !data.pageInfo.hasNextPage : false
  const endCursor = data?.pageInfo.endCursor

  // handle fetching more items
  useEffect(() => {
    if (loading || error || !isReady || !fetchMore || allItemsLoaded) {
      return
    }

    const missingItemsCount = targetLoadedItemsCount - loadedItemsCount

    if (missingItemsCount <= 0) {
      return
    }

    fetchMore({
      variables: { ...queryVariables, first: missingItemsCount + PREFETCHED_ITEMS_COUNT, after: endCursor },
    })
  }, [
    loading,
    error,
    fetchMore,
    allItemsLoaded,
    queryVariables,
    targetLoadedItemsCount,
    loadedItemsCount,
    endCursor,
    isReady,
  ])

  useEffect(() => {
    if (!isEqual(queryVariablesRef.current, queryVariables)) {
      queryVariablesRef.current = queryVariables
      refetch()
    }
  }, [queryVariables, refetch])

  // handle scroll to bottom
  useEffect(() => {
    if (onDemand || (onDemandInfinite && !activatedInfinteGrid)) {
      return
    }
    if (error) return

    const scrollHandler = debounce(() => {
      const scrolledToBottom =
        window.innerHeight + document.documentElement.scrollTop >= document.documentElement.offsetHeight
      if (onScrollToBottom && scrolledToBottom && isReady && !loading && !allItemsLoaded) {
        onScrollToBottom()
      }
    }, 100)

    window.addEventListener('scroll', scrollHandler)
    return () => window.removeEventListener('scroll', scrollHandler)
  }, [error, isReady, loading, allItemsLoaded, onScrollToBottom, onDemand, onDemandInfinite, activatedInfinteGrid])

  const edges = data?.edges

  const isRefetching = networkStatus === NetworkStatus.refetch

  const displayedEdges = edges?.slice(skipCount, targetLoadedItemsCount) ?? []
  const displayedItems = isRefetching ? [] : displayedEdges.map((edge) => edge.node)

  const displayedItemsCount = data
    ? Math.min(targetDisplayedItemsCount, data.totalCount - skipCount)
    : targetDisplayedItemsCount
  const placeholdersCount = isRefetching ? targetDisplayedItemsCount : displayedItemsCount - displayedItems.length

  return {
    displayedItems,
    placeholdersCount,
    allItemsLoaded,
    error,
    loading,
    totalCount: data?.totalCount || 0,
  }
}