1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-06 18:18:53 +08:00
This commit is contained in:
lana-k
2025-06-01 19:16:26 +02:00
parent b59c21c14e
commit d2969de127
12 changed files with 19114 additions and 24 deletions

364
src/lib/graphHelper.js Normal file
View File

@@ -0,0 +1,364 @@
import { nanoid } from 'nanoid'
import { COLOR_PICKER_CONSTANTS } from 'react-colorscales'
import tinycolor from 'tinycolor2'
const TYPE_NODE = 0
const TYPE_EDGE = 1
const DEFAULT_SCALE = COLOR_PICKER_CONSTANTS.DEFAULT_SCALE
export function buildNodes(graph, dataSources, options) {
const docColumn = Object.keys(dataSources)[0] || 'doc'
const { objectType, nodeId } = options.structure
if (objectType && nodeId) {
const nodes = dataSources[docColumn]
.map(json => JSON.parse(json))
.filter(item => item[objectType] === TYPE_NODE)
nodes.forEach(node => {
graph.addNode(node[nodeId], {
data: node
})
})
}
}
export function buildEdges(graph, dataSources, options) {
const docColumn = Object.keys(dataSources)[0] || 'doc'
const { objectType, edgeSource, edgeTarget } = options.structure
if (objectType && edgeSource && edgeTarget) {
const edges = dataSources[docColumn]
.map(json => JSON.parse(json))
.filter(item => item[objectType] === TYPE_EDGE)
edges.forEach(edge => {
const source = edge[edgeSource]
const target = edge[edgeTarget]
if (graph.hasNode(source) && graph.hasNode(target)) {
graph.addEdge(source, target, {
data: edge
})
}
})
}
}
export function updateNodes(graph, attributeUpdates) {
const changeMethods = []
if (attributeUpdates.label) {
changeMethods.push(getUpdateLabelMethod(attributeUpdates.label))
}
if (attributeUpdates.size) {
changeMethods.push(getUpdateSizeMethod(graph, attributeUpdates.size))
}
if (attributeUpdates.color) {
changeMethods.push(getUpdateNodeColorMethod(graph, attributeUpdates.color))
}
graph.forEachNode(nodeId => {
graph.updateNode(nodeId, attributes => {
const newAttributes = { ...attributes }
changeMethods.forEach(method => method(newAttributes, nodeId))
return newAttributes
})
})
}
export function updateEdges(graph, attributeUpdates) {
const changeMethods = []
if (attributeUpdates.label) {
changeMethods.push(getUpdateLabelMethod(attributeUpdates.label))
}
if (attributeUpdates.size) {
changeMethods.push(getUpdateSizeMethod(graph, attributeUpdates.size))
}
if (attributeUpdates.color) {
changeMethods.push(getUpdateEdgeColorMethod(graph, attributeUpdates.color))
}
if ('showDirection' in attributeUpdates) {
changeMethods.push(
attributes =>
(attributes.type = attributeUpdates.showDirection ? 'arrow' : 'line')
)
}
graph.forEachEdge((edgeId, attributes, source, target) => {
graph.updateEdge(source, target, attributes => {
const newAttributes = { ...attributes }
changeMethods.forEach(method => method(newAttributes, edgeId))
return newAttributes
})
})
}
function getUpdateLabelMethod(labelSettings) {
const { source } = labelSettings
return attributes => {
const label = attributes.data[source] ?? ''
attributes.label = label.toString()
}
}
function getUpdateSizeMethod(graph, sizeSettings) {
const { type, value, source, scale, mode, min, method } = sizeSettings
if (type === 'constant') {
return attributes => (attributes.size = value)
} else if (type === 'variable') {
return getVariabledSizeMethod(mode, source, scale, min)
} else {
return (attributes, nodeId) =>
(attributes.size = Math.max(graph[method](nodeId) * scale, min))
}
}
function getDirectVariableColorUpdateMethod(source) {
return attributes =>
(attributes.color = tinycolor(attributes.data[source]).toHexString())
}
function getUpdateNodeColorMethod(graph, colorSettings) {
const {
type,
value,
source,
sourceUsage,
colorscale,
colorscaleDirection,
mode,
method
} = colorSettings
if (type === 'constant') {
return attributes => (attributes.color = value)
} else if (type === 'variable') {
return sourceUsage === 'map_to'
? getColorMethod(
graph,
mode,
(nodeId, attributes) => attributes.data[source],
colorscale,
colorscaleDirection,
getNodeValueScale
)
: getDirectVariableColorUpdateMethod(source)
} else {
return getColorMethod(
graph,
mode,
nodeId => graph[method](nodeId),
colorscale,
colorscaleDirection,
getNodeValueScale
)
}
}
function getUpdateEdgeColorMethod(graph, colorSettings) {
const {
type,
value,
source,
sourceUsage,
colorscale,
colorscaleDirection,
mode
} = colorSettings
if (type === 'constant') {
return attributes => (attributes.color = value)
} else {
return sourceUsage === 'map_to'
? getColorMethod(
graph,
mode,
(edgeId, attributes) => attributes.data[source],
colorscale,
colorscaleDirection,
getEdgeValueScale
)
: getDirectVariableColorUpdateMethod(source)
}
}
function getVariabledSizeMethod(mode, source, scale, min) {
if (mode === 'diameter') {
return attributes =>
(attributes.size = Math.max(
(attributes.data[source] / 2) * scale,
min / 2
))
} else if (mode === 'area') {
return attributes =>
(attributes.size = Math.max(
Math.sqrt((attributes.data[source] / 2) * scale),
min / 2
))
} else {
return attributes =>
(attributes.size = Math.max(attributes.data[source] * scale, min))
}
}
function getColorMethod(
graph,
mode,
sourceGetter,
selectedColorscale,
colorscaleDirection,
valueScaleGetter
) {
const valueScale = valueScaleGetter(graph, sourceGetter)
let colorscale = selectedColorscale || DEFAULT_SCALE
if (colorscaleDirection === 'reversed') {
colorscale = [...colorscale].reverse()
}
if (mode === 'categorical') {
const colorMap = Object.fromEntries(
valueScale.map((value, index) => [
value,
colorscale[index % colorscale.length]
])
)
return (attributes, nodeId) => {
const category = sourceGetter(nodeId, attributes)
attributes.color = colorMap[category]
}
} else {
const min = valueScale[0]
const max = valueScale[valueScale.length - 1]
const normalizedColorscale = colorscale.map((color, index) => [
index / (colorscale.length - 1),
tinycolor(color).toRgb()
])
return (attributes, nodeId) => {
const value = sourceGetter(nodeId, attributes)
const normalizedValue = (value - min) / (max - min)
if (isNaN(normalizedValue)) {
return
}
const exactMatch = normalizedColorscale.find(
([value]) => value === normalizedValue
)
if (exactMatch) {
attributes.color = tinycolor(exactMatch[1]).toHexString()
return
}
const rightColorIndex = normalizedColorscale.findIndex(
([value]) => value >= normalizedValue
)
const leftColorIndex = (rightColorIndex || 1) - 1
const right = normalizedColorscale[rightColorIndex]
const left = normalizedColorscale[leftColorIndex]
const interpolationFactor =
(normalizedValue - left[0]) / (right[0] - left[0])
const r0 = left[1].r
const g0 = left[1].g
const b0 = left[1].b
const r1 = right[1].r
const g1 = right[1].g
const b1 = right[1].b
attributes.color = tinycolor({
r: r0 + interpolationFactor * (r1 - r0),
g: g0 + interpolationFactor * (g1 - g0),
b: b0 + interpolationFactor * (b1 - b0)
}).toHexString()
}
}
}
function getNodeValueScale(graph, sourceGetter) {
const scaleSet = graph.reduceNodes((res, nodeId, attributes) => {
res.add(sourceGetter(nodeId, attributes))
return res
}, new Set())
return Array.from(scaleSet).sort((a, b) => a - b)
}
function getEdgeValueScale(graph, sourceGetter) {
const scaleSet = graph.reduceEdges((res, edgeId, attributes) => {
res.add(sourceGetter(edgeId, attributes))
return res
}, new Set())
return Array.from(scaleSet).sort((a, b) => a - b)
}
export function getOptionsFromDataSources(dataSources) {
if (!dataSources) {
return []
}
return Object.keys(dataSources).map(name => ({
value: name,
label: name
}))
}
export function getOptionsForSave(state, dataSources) {
// we don't need to save the data, only settings
// so we modify state.data using dereference
const stateCopy = JSON.parse(JSON.stringify(state))
const emptySources = {}
for (const key in dataSources) {
emptySources[key] = []
}
dereference.default(stateCopy.data, emptySources)
return stateCopy
}
export async function getImageDataUrl(element, type) {
const chartElement = element.querySelector('.js-plotly-plot')
return await plotly.toImage(chartElement, {
format: type,
width: null,
height: null
})
}
export function getChartData(element) {
const chartElement = element.querySelector('.js-plotly-plot')
return {
data: chartElement.data,
layout: chartElement.layout
}
}
export function getHtml(options) {
const chartId = nanoid()
return `
<script src="https://cdn.plot.ly/plotly-latest.js" charset="UTF-8"></script>
<div id="${chartId}"></div>
<script>
const el = document.getElementById("${chartId}")
let timeout
function debounceResize() {
clearTimeout(timeout)
timeout = setTimeout(() => {
var r = el.getBoundingClientRect()
Plotly.relayout(el, {width: r.width, height: r.height})
}, 200)
}
const resizeObserver = new ResizeObserver(debounceResize)
resizeObserver.observe(el)
Plotly.newPlot(el, ${JSON.stringify(options.data)}, ${JSON.stringify(options.layout)})
</script>
`
}
export default {
getOptionsFromDataSources,
getOptionsForSave,
getImageDataUrl,
getHtml,
getChartData
}