mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 10:08:52 +08:00
export and background
This commit is contained in:
543
package-lock.json
generated
543
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@
|
||||
"format": "prettier . --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sigma/export-image": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"codemirror": "^5.65.18",
|
||||
"codemirror-editor-vue3": "^2.8.0",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { nanoid } from 'nanoid'
|
||||
import { COLOR_PICKER_CONSTANTS } from 'react-colorscales'
|
||||
import tinycolor from 'tinycolor2'
|
||||
|
||||
@@ -17,7 +16,8 @@ export function buildNodes(graph, dataSources, options) {
|
||||
|
||||
nodes.forEach(node => {
|
||||
graph.addNode(node[nodeId], {
|
||||
data: node
|
||||
data: node,
|
||||
labelColor: options.style.nodes.label.color
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -37,7 +37,8 @@ export function buildEdges(graph, dataSources, options) {
|
||||
const target = edge[edgeTarget]
|
||||
if (graph.hasNode(source) && graph.hasNode(target)) {
|
||||
graph.addEdge(source, target, {
|
||||
data: edge
|
||||
data: edge,
|
||||
labelColor: options.style.edges.label.color
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -97,10 +98,11 @@ export function updateEdges(graph, attributeUpdates) {
|
||||
}
|
||||
|
||||
function getUpdateLabelMethod(labelSettings) {
|
||||
const { source } = labelSettings
|
||||
const { source, color } = labelSettings
|
||||
return attributes => {
|
||||
const label = attributes.data[source] ?? ''
|
||||
attributes.label = label.toString()
|
||||
attributes.labelColor = color
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,64 +303,6 @@ export function getOptionsFromDataSources(dataSources) {
|
||||
}))
|
||||
}
|
||||
|
||||
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
|
||||
getOptionsFromDataSources
|
||||
}
|
||||
|
||||
@@ -45,11 +45,16 @@ export default {
|
||||
props: {
|
||||
dataSources: Object,
|
||||
initOptions: Object,
|
||||
importToPngEnabled: Boolean,
|
||||
importToSvgEnabled: Boolean,
|
||||
exportToPngEnabled: Boolean,
|
||||
exportToSvgEnabled: Boolean,
|
||||
forPivot: Boolean
|
||||
},
|
||||
emits: ['update:importToSvgEnabled', 'update', 'loadingImageCompleted'],
|
||||
emits: [
|
||||
'update:exportToSvgEnabled',
|
||||
'update:exportToHtmlEnabled',
|
||||
'update',
|
||||
'loadingImageCompleted'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
plotly,
|
||||
@@ -102,7 +107,8 @@ export default {
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
this.$emit('update:importToSvgEnabled', true)
|
||||
this.$emit('update:exportToSvgEnabled', true)
|
||||
this.$emit('update:exportToHtmlEnabled', true)
|
||||
},
|
||||
mounted() {
|
||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||
|
||||
@@ -39,6 +39,16 @@
|
||||
</Field>
|
||||
</Fold>
|
||||
</Panel>
|
||||
<Panel group="Style" name="General">
|
||||
<Fold name="General">
|
||||
<Field label="Background color">
|
||||
<ColorPicker
|
||||
:selectedColor="settings.style.backgroundColor"
|
||||
@color-change="settings.style.backgroundColor = $event"
|
||||
/>
|
||||
</Field>
|
||||
</Fold>
|
||||
</Panel>
|
||||
<Panel group="Style" name="Nodes">
|
||||
<Fold name="Nodes">
|
||||
<Field label="Label">
|
||||
@@ -49,6 +59,13 @@
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Label color">
|
||||
<ColorPicker
|
||||
:selectedColor="settings.style.nodes.label.color"
|
||||
@color-change="updateNodes('label.color', $event)"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<NodeSizeSettings
|
||||
v-model="settings.style.nodes.size"
|
||||
:keyOptions="keysOptions"
|
||||
@@ -80,6 +97,13 @@
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Label color">
|
||||
<ColorPicker
|
||||
:selectedColor="settings.style.edges.label.color"
|
||||
@color-change="updateEdges('label.color', $event)"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<EdgeSizeSettings
|
||||
v-model="settings.style.edges.size"
|
||||
:keyOptions="keysOptions"
|
||||
@@ -132,7 +156,8 @@
|
||||
ref="graph"
|
||||
:style="{
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
width: '100%',
|
||||
backgroundColor: settings.style.backgroundColor
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
@@ -146,6 +171,7 @@ import { PanelMenuWrapper, Panel, Fold, Section } from 'react-chart-editor'
|
||||
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
||||
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
||||
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
|
||||
import Button from 'react-chart-editor/lib/components/widgets/Button'
|
||||
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||
import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue'
|
||||
@@ -155,6 +181,7 @@ import FA2Layout from 'graphology-layout-forceatlas2/worker'
|
||||
import forceAtlas2 from 'graphology-layout-forceatlas2'
|
||||
import RunIcon from '@/components/svg/run.vue'
|
||||
import StopIcon from '@/components/svg/stop.vue'
|
||||
import { downloadAsPNG, drawOnCanvas } from '@sigma/export-image'
|
||||
import {
|
||||
buildNodes,
|
||||
buildEdges,
|
||||
@@ -181,6 +208,7 @@ export default {
|
||||
Field: applyPureReactInVue(Field),
|
||||
Fold: applyPureReactInVue(Fold),
|
||||
Button: applyPureReactInVue(Button),
|
||||
ColorPicker: applyPureReactInVue(ColorPicker),
|
||||
RunIcon,
|
||||
StopIcon,
|
||||
RandomLayoutSettings,
|
||||
@@ -190,9 +218,12 @@ export default {
|
||||
EdgeSizeSettings,
|
||||
EdgeColorSettings
|
||||
},
|
||||
inject: ['tabLayout'],
|
||||
props: {
|
||||
dataSources: Object
|
||||
dataSources: Object,
|
||||
initOptions: Object
|
||||
},
|
||||
emits: ['update'],
|
||||
data() {
|
||||
return {
|
||||
graph: new Graph(),
|
||||
@@ -215,7 +246,7 @@ export default {
|
||||
forceAtlas2: ForceAtlasLayoutSettings
|
||||
}),
|
||||
|
||||
settings: {
|
||||
settings: this.initOptions || {
|
||||
structure: {
|
||||
nodeId: null,
|
||||
objectType: null,
|
||||
@@ -223,50 +254,34 @@ export default {
|
||||
edgeTarget: null
|
||||
},
|
||||
style: {
|
||||
backgroundColor: 'white',
|
||||
nodes: {
|
||||
size: {
|
||||
type: 'constant',
|
||||
value: 16,
|
||||
source: null,
|
||||
scale: 1,
|
||||
mode: 'diameter',
|
||||
method: 'degree',
|
||||
min: 0
|
||||
value: 4
|
||||
},
|
||||
color: {
|
||||
type: 'constant',
|
||||
value: '#1F77B4',
|
||||
source: null,
|
||||
sourceUsage: 'map_to',
|
||||
colorscale: null,
|
||||
colorscaleDirection: 'normal',
|
||||
method: 'degree',
|
||||
mode: 'continious'
|
||||
value: '#1F77B4'
|
||||
},
|
||||
label: {
|
||||
source: null
|
||||
source: null,
|
||||
color: '#444444'
|
||||
}
|
||||
},
|
||||
edges: {
|
||||
showDirection: true,
|
||||
size: {
|
||||
type: 'constant',
|
||||
value: 2,
|
||||
source: null,
|
||||
scale: 1,
|
||||
min: 0
|
||||
value: 2
|
||||
},
|
||||
color: {
|
||||
type: 'constant',
|
||||
value: '#a2b1c6',
|
||||
source: null,
|
||||
sourceUsage: 'map_to',
|
||||
colorscale: null,
|
||||
colorscaleDirection: 'normal',
|
||||
mode: 'continious'
|
||||
value: '#a2b1c6'
|
||||
},
|
||||
label: {
|
||||
source: null
|
||||
source: null,
|
||||
color: '#a2b1c6'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -287,9 +302,15 @@ export default {
|
||||
if (!this.dataSources) {
|
||||
return []
|
||||
}
|
||||
return this.dataSources[Object.keys(this.dataSources)[0] || 'doc'].map(
|
||||
json => JSON.parse(json)
|
||||
)
|
||||
try {
|
||||
return (
|
||||
this.dataSources[Object.keys(this.dataSources)[0] || 'doc'].map(
|
||||
json => JSON.parse(json)
|
||||
) || []
|
||||
)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
},
|
||||
keysOptions() {
|
||||
if (!this.dataSources) {
|
||||
@@ -309,11 +330,30 @@ export default {
|
||||
this.buildGraph()
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.$emit('update')
|
||||
}
|
||||
},
|
||||
'settings.structure': {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.buildGraph()
|
||||
}
|
||||
},
|
||||
tabLayout: {
|
||||
deep: true,
|
||||
handler() {
|
||||
if (this.tabLayout.dataView !== 'hidden' && this.renderer) {
|
||||
this.renderer.scheduleRender()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.dataSources) {
|
||||
this.buildGraph()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -333,7 +373,9 @@ export default {
|
||||
circular.assign(this.graph)
|
||||
this.renderer = new Sigma(this.graph, this.$refs.graph, {
|
||||
renderEdgeLabels: true,
|
||||
allowInvalidContainer: true
|
||||
allowInvalidContainer: true,
|
||||
labelColor: { attribute: 'labelColor', color: '#444444' },
|
||||
edgeLabelColor: { attribute: 'labelColor', color: '#a2b1c6' }
|
||||
})
|
||||
},
|
||||
updateStructure(attributeName, value) {
|
||||
@@ -469,6 +511,16 @@ export default {
|
||||
this.fa2Running = true
|
||||
this.fa2Layout.start()
|
||||
}
|
||||
},
|
||||
saveAsPng() {
|
||||
return downloadAsPNG(this.renderer, {
|
||||
backgroundColor: this.settings.style.backgroundColor
|
||||
})
|
||||
},
|
||||
prepareCopy() {
|
||||
return drawOnCanvas(this.renderer, {
|
||||
backgroundColor: this.settings.style.backgroundColor
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,20 +7,22 @@
|
||||
<div
|
||||
class="graph"
|
||||
:style="{
|
||||
height: !dataSources ? 'calc(100% - 40px)' : '100%',
|
||||
'background-color': 'white'
|
||||
height: !dataSources ? 'calc(100% - 40px)' : '100%'
|
||||
}"
|
||||
>
|
||||
<GraphEditor :dataSources="dataSources" />
|
||||
<GraphEditor
|
||||
ref="graphEditor"
|
||||
:dataSources="dataSources"
|
||||
:initOptions="initOptions"
|
||||
@update="$emit('update')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
import events from '@/lib/utils/events'
|
||||
|
||||
import GraphEditor from './GraphEditor.vue'
|
||||
|
||||
export default {
|
||||
@@ -29,30 +31,49 @@ export default {
|
||||
props: {
|
||||
dataSources: Object,
|
||||
initOptions: Object,
|
||||
importToPngEnabled: Boolean,
|
||||
importToSvgEnabled: Boolean
|
||||
exportToPngEnabled: Boolean,
|
||||
exportToSvgEnabled: Boolean,
|
||||
exportToHtmlEnabled: Boolean
|
||||
},
|
||||
emits: [
|
||||
'update:exportToSvgEnabled',
|
||||
'update:exportToHtmlEnabled',
|
||||
'update',
|
||||
'loadingImageCompleted'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
resizeObserver: null
|
||||
}
|
||||
},
|
||||
emits: ['update:importToSvgEnabled', 'update', 'loadingImageCompleted'],
|
||||
|
||||
created() {
|
||||
this.$emit('update:importToSvgEnabled', true)
|
||||
this.$emit('update:exportToSvgEnabled', false)
|
||||
this.$emit('update:exportToHtmlEnabled', false)
|
||||
},
|
||||
mounted() {
|
||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||
this.resizeObserver.observe(this.$refs.graphContainer)
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.resizeObserver.unobserve(this.$refs.graphContainer)
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
getOptionsForSave() {},
|
||||
getOptionsForSave() {
|
||||
return this.$refs.graphEditor.settings
|
||||
},
|
||||
async saveAsPng() {
|
||||
const url = await this.prepareCopy()
|
||||
await this.$refs.graphEditor.saveAsPng()
|
||||
this.$emit('loadingImageCompleted')
|
||||
fIo.downloadFromUrl(url, 'chart')
|
||||
},
|
||||
|
||||
async saveAsSvg() {
|
||||
const url = await this.prepareCopy('svg')
|
||||
fIo.downloadFromUrl(url, 'chart')
|
||||
async prepareCopy() {
|
||||
return await this.$refs.graphEditor.prepareCopy()
|
||||
},
|
||||
|
||||
saveAsHtml() {},
|
||||
async prepareCopy(type = 'png') {}
|
||||
async handleResize() {
|
||||
const renderer = this.$refs.graphEditor.renderer
|
||||
renderer.refresh()
|
||||
renderer.getCamera().setState({ x: 0.5, y: 0.5 })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -46,14 +46,15 @@ export default {
|
||||
props: {
|
||||
dataSources: Object,
|
||||
initOptions: Object,
|
||||
importToPngEnabled: Boolean,
|
||||
importToSvgEnabled: Boolean
|
||||
exportToPngEnabled: Boolean,
|
||||
exportToSvgEnabled: Boolean
|
||||
},
|
||||
emits: [
|
||||
'loadingImageCompleted',
|
||||
'update',
|
||||
'update:importToSvgEnabled',
|
||||
'update:importToPngEnabled'
|
||||
'update:exportToSvgEnabled',
|
||||
'update:exportToPngEnabled',
|
||||
'update:exportToHtmlEnabled'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
@@ -110,11 +111,11 @@ export default {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.$emit(
|
||||
'update:importToPngEnabled',
|
||||
'update:exportToPngEnabled',
|
||||
this.pivotOptions.rendererName !== 'TSV Export'
|
||||
)
|
||||
this.$emit(
|
||||
'update:importToSvgEnabled',
|
||||
'update:exportToSvgEnabled',
|
||||
this.viewStandartChart || this.viewCustomChart
|
||||
)
|
||||
events.send('viz_pivot.render', null, {
|
||||
@@ -126,6 +127,9 @@ export default {
|
||||
this.show()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$emit('update:exportToHtmlEnabled', true)
|
||||
},
|
||||
mounted() {
|
||||
this.show()
|
||||
// We need to detect resizing because plotly doesn't resize when resize its container
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
<component
|
||||
:is="mode"
|
||||
ref="viewComponent"
|
||||
v-model:importToPngEnabled="importToPngEnabled"
|
||||
v-model:importToSvgEnabled="importToSvgEnabled"
|
||||
:initOptions="mode === initMode ? initOptions : undefined"
|
||||
v-model:exportToPngEnabled="exportToPngEnabled"
|
||||
v-model:exportToSvgEnabled="exportToSvgEnabled"
|
||||
v-model:exportToHtmlEnabled="exportToHtmlEnabled"
|
||||
:initOptions="initOptionsByMode[mode]"
|
||||
:data-sources="dataSource"
|
||||
@loading-image-completed="loadingImage = false"
|
||||
@update="$emit('update')"
|
||||
@@ -42,7 +43,7 @@
|
||||
<div class="side-tool-bar-divider" />
|
||||
|
||||
<icon-button
|
||||
:disabled="!importToPngEnabled || loadingImage"
|
||||
:disabled="!exportToPngEnabled || loadingImage"
|
||||
:loading="loadingImage"
|
||||
tooltip="Save as PNG image"
|
||||
tooltipPosition="top-left"
|
||||
@@ -52,7 +53,7 @@
|
||||
</icon-button>
|
||||
<icon-button
|
||||
ref="svgExportBtn"
|
||||
:disabled="!importToSvgEnabled"
|
||||
:disabled="!exportToSvgEnabled"
|
||||
tooltip="Save as SVG"
|
||||
tooltipPosition="top-left"
|
||||
@click="saveAsSvg"
|
||||
@@ -62,6 +63,7 @@
|
||||
|
||||
<icon-button
|
||||
ref="htmlExportBtn"
|
||||
:disabled="!exportToHtmlEnabled"
|
||||
tooltip="Save as HTML"
|
||||
tooltipPosition="top-left"
|
||||
@click="saveAsHtml"
|
||||
@@ -136,12 +138,18 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
mode: this.initMode || 'chart',
|
||||
importToPngEnabled: true,
|
||||
importToSvgEnabled: true,
|
||||
exportToPngEnabled: true,
|
||||
exportToSvgEnabled: true,
|
||||
exportToHtmlEnabled: true,
|
||||
loadingImage: false,
|
||||
copyingImage: false,
|
||||
preparingCopy: false,
|
||||
dataToCopy: null,
|
||||
initOptionsByMode: {
|
||||
chart: this.initMode === 'chart' ? this.initOptions : null,
|
||||
pivot: this.initMode === 'pivot' ? this.initOptions : null,
|
||||
graph: this.initMode === 'graph' ? this.initOptions : null
|
||||
},
|
||||
showLoadingDialog: false
|
||||
}
|
||||
},
|
||||
@@ -151,9 +159,10 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
mode() {
|
||||
mode(newMode, oldMode) {
|
||||
this.$emit('update')
|
||||
this.importToPngEnabled = true
|
||||
this.exportToPngEnabled = true
|
||||
this.initOptionsByMode[oldMode] = this.getOptionsForSave()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -68,7 +68,7 @@ import Splitpanes from '@/components/Splitpanes'
|
||||
import SqlEditor from './SqlEditor'
|
||||
import DataView from './DataView'
|
||||
import RunResult from './RunResult'
|
||||
import { nextTick } from 'vue'
|
||||
import { nextTick, computed } from 'vue'
|
||||
|
||||
import events from '@/lib/utils/events'
|
||||
|
||||
@@ -80,6 +80,11 @@ export default {
|
||||
RunResult,
|
||||
Splitpanes
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
tabLayout: computed(() => this.tab.layout)
|
||||
}
|
||||
},
|
||||
props: {
|
||||
tab: Object
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user