import {
CloseIcon,
RepeatClockIcon,
ChevronDownIcon,
SettingsIcon,
InfoOutlineIcon,
RepeatIcon,
ArrowRightIcon,
AddIcon,
DeleteIcon,
CheckCircleIcon,
} from '@chakra-ui/icons'
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Button,
Flex,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
MenuOptionGroup,
MenuItemOption,
Select,
Slider,
SliderFilledTrack,
SliderThumb,
SliderTrack,
StackDivider,
Switch,
Text,
Tooltip,
VStack,
Heading,
Collapse,
Grid,
Portal,
SlideFade,
Input,
} from '@chakra-ui/react'
import { CUIAutoComplete } from 'chakra-ui-autocomplete'
import React, { useState, useContext, useEffect, useCallback } from 'react'
import Scrollbars from 'react-custom-scrollbars-2'
import {
initialPhysics,
initialFilter,
initialVisuals,
initialMouse,
initialBehavior,
TagColors,
colorList,
} from './config'
import { ThemeContext } from '../util/themecontext'
import { usePersistantState } from '../util/persistant-state'
export interface TweakProps {
physics: typeof initialPhysics
setPhysics: any
threeDim: boolean
setThreeDim: (newValue: boolean) => void
filter: typeof initialFilter
setFilter: any
visuals: typeof initialVisuals
setVisuals: any
mouse: typeof initialMouse
setMouse: any
behavior: typeof initialBehavior
setBehavior: any
tags: string[]
tagColors: TagColors
setTagColors: any
}
export const Tweaks = (props: TweakProps) => {
const {
physics,
setPhysics,
threeDim,
setThreeDim,
filter,
setFilter,
visuals,
setVisuals,
mouse,
setMouse,
behavior,
setBehavior,
tags,
tagColors,
setTagColors,
} = props
const [showTweaks, setShowTweaks] = usePersistantState('showTweaks', false)
const { highlightColor, setHighlightColor } = useContext(ThemeContext)
const setVisualsCallback = useCallback((val) => setVisuals(val), [])
const setPhysicsCallback = useCallback((val: number, phys: string, scale: number) => {
setPhysics((curr: typeof initialPhysics) => {
return { ...curr, [phys]: val / scale }
})
}, [])
return !showTweaks ? (
}
onClick={() => setShowTweaks(true)}
/>
) : (
}
onClick={() => {
setVisuals(initialVisuals)
setFilter(initialFilter)
setMouse(initialMouse)
setPhysics(initialPhysics)
setBehavior(initialBehavior)
}}
variant="none"
size="sm"
/>
}
aria-label="Close Tweak Panel"
variant="ghost"
onClick={() => setShowTweaks(false)}
/>
(
)}
>
Filter
}
align="stretch"
paddingLeft={7}
color="gray.800"
>
Link children to...
Orphans
{
setFilter((curr: typeof initialFilter) => {
return { ...curr, orphans: !curr.orphans }
})
}}
isChecked={filter.orphans}
>
Citations without note files
{
setFilter({ ...filter, filelessCites: !filter.filelessCites })
}}
isChecked={filter.filelessCites}
>
Non-existant nodes
{
setTagColors({ ...tagColors, bad: 'white' })
setFilter({ ...filter, bad: !filter.bad })
}}
isChecked={filter.bad}
>
Tag filters
Tag Colors
Physics
{/* setPhysics({ ...physics, enabled: !physics.enabled })}
isChecked={physics.enabled}
/> */}
}
align="stretch"
paddingLeft={7}
color="gray.800"
>
setPhysics({ ...physics, gravityOn: !physics.gravityOn })}
>
Also in local
{
setPhysics((curr: typeof initialPhysics) => {
return { ...curr, gravityLocal: !curr.gravityLocal }
})
}}
isChecked={physics.gravityLocal}
>
setPhysicsCallback(v, 'gravity', 10)}
/>
setPhysicsCallback(v, 'charge', -1 / 100)}
label="Repulsive Force"
/>
setPhysics({ ...physics, collision: !physics.collision })}
>
setPhysicsCallback(v, 'collisionStrength', 1 / 5)}
label="Collision Radius"
infoText="Easy with this one, high values can lead to a real jiggly mess"
/>
setPhysicsCallback(v, 'linkStrength', 5)}
label="Link Force"
/>
setPhysicsCallback(v, 'linkIts', 1)}
min={0}
max={6}
step={1}
infoText="How many links down the line the physics of a single node affects (Slow)"
/>
setPhysicsCallback(v, 'velocityDecay', 10)}
/>
Advanced
}
align="stretch"
paddingLeft={3}
color="gray.800"
>
setPhysicsCallback(v, 'alphaDecay', 50)}
/>
setPhysics({ ...physics, centering: !physics.centering })}
infoText="Keeps the nodes in the center of the viewport. If disabled you can drag the nodes anywhere you want."
>
setPhysicsCallback(v, 'centeringStrength', 1)}
/>
Visual
Colors
}
align="stretch"
color="gray.800"
>
Nodes
}
variant="ghost"
onClick={() => {
const arr = visuals.nodeColorScheme ?? []
setVisuals({
...visuals,
//shuffle that guy
//definitely thought of this myself
nodeColorScheme: arr
.map((x: any) => [Math.random(), x])
.sort(([a], [b]) => a - b)
.map(([_, x]) => x),
})
}}
/>
}
size="sm"
variant="ghost"
onClick={() => {
const arr = visuals.nodeColorScheme ?? []
setVisuals({
...visuals,
nodeColorScheme: [...arr.slice(1, arr.length), arr[0]],
})
}}
/>
Links
Accent
Nodes & Links
}
align="stretch"
color="gray.800"
>
setVisuals({ ...visuals, nodeRel: value })}
/>
setVisuals({ ...visuals, nodeSizeLinks: value })}
/>
setVisuals((prev: typeof initialVisuals) => ({
...prev,
nodeZoomSize: value,
}))
}
/>
{threeDim && (
<>
setVisuals({ ...visuals, nodeOpacity: value })}
/>
setVisuals({ ...visuals, nodeResolution: value })
}
/>
>
)}
setVisuals({ ...visuals, linkWidth: value })}
/>
{threeDim && (
setVisuals({ ...visuals, linkOpacity: value })}
/>
)}
setVisuals({ ...visuals, arrows: !visuals.arrows })}
>
setVisuals({ ...visuals, arrowsLength: 10 * value })
}
/>
setVisuals({ ...visuals, arrowsPos: value })}
/>
setVisuals({ ...visuals, particles: !visuals.particles })
}
>
setVisuals({ ...visuals, particlesNumber: value })
}
/>
setVisuals({ ...visuals, particlesWidth: value })
}
/>
{/* }
align="stretch"
paddingLeft={7}
color="gray.800"
> */}
Labels
}
align="stretch"
color="gray.800"
>
Show labels
}
align="stretch"
paddingLeft={2}
color="gray.800"
>
setVisuals({ ...visuals, labelFontSize: value })}
/>
setVisuals({ ...visuals, labelLength: value })}
/>
setVisuals({ ...visuals, labelWordWrap: value })}
/>
setVisuals({ ...visuals, labelLineSpace: value })
}
/>
{
console.log(visuals.labelBackgroundOpacity)
setVisuals({ ...visuals, labelBackgroundOpacity: value })
}}
min={0}
max={1}
step={0.01}
/>
1} animateOpacity>
setVisuals({ ...visuals, labelScale: value / 5 })
}
/>
Highlighting
}
align="stretch"
color="gray.800"
>
setVisuals({ ...visuals, highlight: !visuals.highlight })
}
value={visuals.highlight}
>
}
align="stretch"
paddingLeft={0}
>
setVisuals({ ...visuals, highlightLinkSize: value })
}
/>
setVisuals({ ...visuals, highlightNodeSize: value })
}
/>
setVisuals({ ...visuals, highlightFade: value })
}
/>
{/*
Highlight node color
Highlight link color
*/}
{
setVisuals({ ...visuals, highlightAnim: !visuals.highlightAnim })
}}
value={visuals.highlightAnim}
>
setVisuals({ ...visuals, animationSpeed: v })}
value={visuals.animationSpeed}
infoText="Slower speed has a chance of being buggy"
min={50}
max={1000}
step={10}
/>
Citations
}
align="stretch"
color="gray.800"
>
setVisuals({ ...visuals, citeDashes: !visuals.citeDashes })
}
>
setVisuals({ ...visuals, citeDashLength: value * 10 })
}
/>
setVisuals({ ...visuals, citeGapLength: value * 5 })
}
/>
setVisuals({ ...visuals, refDashes: !visuals.refDashes })
}
>
setVisuals({ ...visuals, refDashLength: value * 10 })
}
/>
setVisuals({ ...visuals, refGapLength: value * 5 })
}
/>
Behavior
}
align="stretch"
paddingLeft={7}
color="gray.800"
>
Expand Node
Open in Emacs
Follow Emacs by...
Local graph
setBehavior({ ...behavior, zoomSpeed: value })}
/>
setBehavior({ ...behavior, zoomPadding: value })}
infoText="How much to zoom out to accomodate all nodes when changing the view."
/>
)
}
export interface InfoTooltipProps {
infoText?: string | boolean
}
export const InfoTooltip = (props: InfoTooltipProps) => {
const { infoText } = props
return (
)
}
export interface SliderWithInfoProps {
min?: number
max?: number
step?: number
value: number
onChange: (arg0: number) => void
label: string
infoText?: string
}
export const SliderWithInfo = ({
min = 0,
max = 10,
step = 0.1,
value = 1,
...rest
}: SliderWithInfoProps) => {
const { onChange, label, infoText } = rest
const { highlightColor } = useContext(ThemeContext)
return (
{label}
{infoText && }
)
}
export interface EnableSectionProps {
label: string
value: boolean | number
onChange: () => void
infoText?: string
children: React.ReactNode
}
export const EnableSection = (props: EnableSectionProps) => {
const { value, onChange, label, infoText, children } = props
return (
{label}
{infoText && }
{children}
)
}
export interface DropDownMenuProps {
textArray: string[]
onClickArray: (() => void)[]
displayValue: string
}
export const DropDownMenu = (props: DropDownMenuProps) => {
const { textArray, onClickArray, displayValue } = props
return (
)
}
export interface ColorMenuProps {
label: string
colorList: string[]
value: string
visValue: string
setVisuals?: any
}
export const ColorMenu = (props: ColorMenuProps) => {
const { label, colorList, value, visValue, setVisuals } = props
const clickCallback = useCallback(
(color) =>
setVisuals((curr: typeof initialVisuals) => {
return {
...curr,
[value]: color,
}
}),
[],
)
return (
{label}
)
}
export interface TagPanelProps {
tags: string[]
filter: typeof initialFilter
setFilter: any
highlightColor: string
mode: string
}
export const TagPanel = (props: TagPanelProps) => {
const { filter, setFilter, tags, highlightColor, mode } = props
const tagArray = tags.map((tag) => {
return { value: tag, label: tag }
})
const currentTags = mode === 'blacklist' ? 'tagsBlacklist' : 'tagsWhitelist'
const [selectedItems, setSelectedItems] = useState(
filter[currentTags].map((tag) => {
return { value: tag, label: tag }
}),
)
return (
null}
disableCreateItem={true}
selectedItems={selectedItems}
onSelectedItemsChange={(changes) => {
if (changes.selectedItems) {
setSelectedItems(changes.selectedItems)
setFilter({ ...filter, [currentTags]: changes.selectedItems.map((item) => item.value) })
}
}}
listItemStyleProps={{ overflow: 'hidden' }}
highlightItemBg="gray.400"
toggleButtonStyleProps={{ variant: 'outline' }}
inputStyleProps={{
focusBorderColor: highlightColor,
color: 'gray.800',
borderColor: 'gray.600',
}}
tagStyleProps={{
rounded: 'full',
bg: highlightColor,
height: 8,
paddingLeft: 4,
fontWeight: 'bold',
}}
hideToggleButton
itemRenderer={(selected) => selected.label}
/>
)
}
export interface TagColorPanelProps {
tags: string[]
highlightColor: string
colorList: string[]
tagColors: TagColors
setTagColors: any
}
export const TagColorPanel = (props: TagColorPanelProps) => {
const { colorList, tagColors, setTagColors, highlightColor, tags } = props
const tagArray = tags.map((tag) => {
return { value: tag, label: tag }
})
const [selectedItems, setSelectedItems] = useState(
Object.keys(tagColors).map((tag) => {
return { value: tag, label: tag }
}),
)
return (
{
if (changes.selectedItems) {
setSelectedItems(Array.from(new Set(changes.selectedItems)))
setTagColors(
Object.fromEntries(
Array.from(new Set(changes.selectedItems)).map((item) => {
return [item.label, tagColors[item.label] ?? 'gray.600']
}),
),
)
}
}}
listItemStyleProps={{ overflow: 'hidden' }}
highlightItemBg="gray.400"
toggleButtonStyleProps={{ variant: 'outline' }}
inputStyleProps={{
focusBorderColor: highlightColor,
color: 'gray.800',
borderColor: 'gray.600',
}}
tagStyleProps={{
display: 'none',
rounded: 'full',
bg: highlightColor,
height: 8,
paddingLeft: 4,
fontWeight: 'bold',
}}
hideToggleButton
itemRenderer={(selected) => selected.label}
/>
}
align="stretch"
color="gray.800"
>
{Object.keys(tagColors).map((tag) => {
return (
{tag}
}
onClick={() => {
setTagColors(
Object.fromEntries(
Array.from(new Set(selectedItems)).map((item) => {
return [item.label, tagColors[item.label] ?? 'gray.600']
}),
),
)
setSelectedItems(selectedItems.filter((item) => item.value !== tag))
}}
/>
)
})}
)
}