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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
// * 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
/**
* 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'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 |
_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 |
/**
* 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 |
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,
}
}