1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-06 18:18:53 +08:00

4 Commits

Author SHA1 Message Date
lana-k
0a2af0bba3 events 2025-11-01 21:25:56 +01:00
lana-k
e4b35bac0a skip node if there is no node id 2025-11-01 19:48:22 +01:00
lana-k
3d1e822cdc link to docs, disable some settings, check result set 2025-11-01 15:49:34 +01:00
lana-k
3d6479be7a visualisation settings toggle 2025-10-28 22:51:13 +01:00
19 changed files with 2427 additions and 22000 deletions

23766
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -33,48 +33,6 @@ export default {
</script>
<style>
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Regular.woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-SemiBold.woff2');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Bold.woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Italic.woff2');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-SemiBoldItalic.woff2');
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-BoldItalic.woff2');
font-weight: 700;
font-style: italic;
}
#app,
.dialog,
input,

View File

@@ -4,3 +4,10 @@
font-size: 13px;
padding: 0 24px;
}
.data-view-warning {
height: 40px;
line-height: 40px;
border-bottom: 1px solid var(--color-border);
box-sizing: border-box;
}

View File

@@ -0,0 +1,45 @@
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Regular.woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-SemiBold.woff2');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Bold.woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Italic.woff2');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-SemiBoldItalic.woff2');
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-BoldItalic.woff2');
font-weight: 700;
font-style: italic;
}
a {
color: var(--color-accent-shade);
}

View File

@@ -1,5 +1,12 @@
<template>
<Field label="Adjust sizes">
<Field label="Scaling ratio">
<NumericInput
:value="modelValue.scalingRatio"
@update="update('scalingRatio', $event)"
/>
</Field>
<Field label="Prevent overlapping">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.adjustSizes"
@@ -22,13 +29,6 @@
/>
</Field>
<Field label="Gravity">
<NumericInput
:value="modelValue.gravity"
@update="update('gravity', $event)"
/>
</Field>
<Field label="Strong gravity mode">
<RadioBlocks
:options="booleanOptions"

View File

@@ -39,7 +39,10 @@
</template>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Color as">
<Field
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
label="Color as"
>
<RadioBlocks
:options="сolorAsOptions"
:activeOption="modelValue.mode"
@@ -47,7 +50,10 @@
/>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Colorscale direction">
<Field
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
label="Colorscale direction"
>
<RadioBlocks
:options="сolorscaleDirections"
:activeOption="modelValue.colorscaleDirection"

View File

@@ -7,10 +7,10 @@
/>
</Field>
<Field label="Scaling ratio">
<Field label="Gravity">
<NumericInput
:value="modelValue.scalingRatio"
@update="update('scalingRatio', $event)"
:value="modelValue.gravity"
@update="update('gravity', $event)"
/>
</Field>
</template>

View File

@@ -27,7 +27,7 @@
/>
</Field>
<Field>
<Field v-if="modelValue.type === 'variable'">
<RadioBlocks
:options="colorSourceUsageOptions"
:activeOption="modelValue.sourceUsage"
@@ -45,7 +45,10 @@
</template>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Color as">
<Field
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
label="Color as"
>
<RadioBlocks
:options="сolorAsOptions"
:activeOption="modelValue.mode"
@@ -53,7 +56,10 @@
/>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Colorscale direction">
<Field
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
label="Colorscale direction"
>
<RadioBlocks
:options="сolorscaleDirections"
:activeOption="modelValue.colorscaleDirection"

File diff suppressed because one or more lines are too long

View File

@@ -5,8 +5,39 @@ const TYPE_NODE = 0
const TYPE_EDGE = 1
const DEFAULT_SCALE = COLOR_PICKER_CONSTANTS.DEFAULT_SCALE
export function dataSourceIsValid(dataSources) {
const docColumn = Object.keys(dataSources)[0]
if (!docColumn) {
return false
}
try {
const records = dataSources[docColumn].slice(0, 10)
records.forEach(record => {
const parsedRec = JSON.parse(record)
if (Object.keys(parsedRec).length < 2) {
throw new Error('The records must have at least 2 keys')
}
})
const firstRecord = JSON.parse(records[0])
if (
!Object.keys(firstRecord).some(key => {
return records
.map(record => JSON.parse(record)[key])
.every(value => value === 0 || value === 1)
})
) {
throw new Error(
'There must be a common key used as object type: 0 - node, 1 - edge'
)
}
return true
} catch (err) {
return false
}
}
export function buildNodes(graph, dataSources, options) {
const docColumn = Object.keys(dataSources)[0] || 'doc'
const docColumn = Object.keys(dataSources)[0]
const { objectType, nodeId } = options.structure
if (objectType && nodeId) {
@@ -14,10 +45,12 @@ export function buildNodes(graph, dataSources, options) {
.map(json => JSON.parse(json))
.filter(item => item[objectType] === TYPE_NODE)
nodes.forEach(node => {
if (node[nodeId]) {
graph.addNode(node[nodeId], {
data: node,
labelColor: options.style.nodes.label.color
})
}
})
}
}
@@ -110,10 +143,23 @@ function getUpdateSizeMethod(graph, sizeSettings) {
if (type === 'constant') {
return attributes => (attributes.size = value)
} else if (type === 'variable') {
return getVariabledSizeMethod(mode, source, scale, min)
return attributes => {
attributes.size = getVariabledSize(
mode,
attributes.data[source],
scale,
min
)
}
} else {
return (attributes, nodeId) =>
(attributes.size = Math.max(graph[method](nodeId) * scale, min))
return (attributes, nodeId) => {
attributes.size = getVariabledSize(
mode,
graph[method](nodeId),
scale,
min
)
}
}
}
@@ -184,22 +230,13 @@ function getUpdateEdgeColorMethod(graph, colorSettings) {
}
}
function getVariabledSizeMethod(mode, source, scale, min) {
function getVariabledSize(mode, value, scale, min) {
if (mode === 'diameter') {
return attributes =>
(attributes.size = Math.max(
(attributes.data[source] / 2) * scale,
min / 2
))
return Math.max((value / 2) * scale, min / 2)
} else if (mode === 'area') {
return attributes =>
(attributes.size = Math.max(
Math.sqrt((attributes.data[source] / 2) * scale),
min / 2
))
return Math.max(Math.sqrt((value / 2) * scale), min / 2)
} else {
return attributes =>
(attributes.size = Math.max(attributes.data[source] * scale, min))
return Math.max(value * scale, min)
}
}

View File

@@ -5,6 +5,7 @@ import store from '@/store'
import { createVfm, VueFinalModal, useVfm } from 'vue-final-modal'
import '@/assets/styles/variables.css'
import '@/assets/styles/typography.css'
import '@/assets/styles/buttons.css'
import '@/assets/styles/tables.css'
import '@/assets/styles/dialogs.css'

View File

@@ -1,6 +1,6 @@
<template>
<div ref="chartContainer" class="chart-container">
<div v-show="!dataSources" class="warning chart-warning">
<div v-show="!dataSources" class="warning data-view-warning">
There is no data to build a chart. Run your SQL query and make sure the
result is not empty.
</div>
@@ -20,6 +20,7 @@
:useResizeHandler="useResizeHandler"
:debug="true"
:advancedTraceTypeSelector="true"
:hideControls="!showViewSettings"
@update="update"
@render="onRender"
/>
@@ -47,7 +48,8 @@ export default {
initOptions: Object,
exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean,
forPivot: Boolean
forPivot: Boolean,
showViewSettings: Boolean
},
emits: [
'update:exportToSvgEnabled',
@@ -85,6 +87,9 @@ export default {
dereference.default(this.state.data, this.dataSources)
this.updatePlotly()
}
},
showViewSettings() {
this.handleResize()
}
},
created() {
@@ -179,13 +184,6 @@ export default {
height: 100%;
}
.chart-warning {
height: 40px;
line-height: 40px;
border-bottom: 1px solid var(--color-border);
box-sizing: border-box;
}
.chart {
min-height: 242px;
}

View File

@@ -1,17 +1,26 @@
<template>
<div class="plotly_editor">
<GraphEditorControls>
<div :class="['plotly_editor', { with_controls: showViewSettings }]">
<GraphEditorControls v-show="showViewSettings">
<PanelMenuWrapper>
<Panel group="Structure" name="Graph">
<Fold name="Graph">
<Field>Choose keys explanation...</Field>
<Field>
Map your result set records to node and edge properties required
to build a graph. Learn more about result set requirements in the
<a href="https://sqliteviz.com/docs/graph/" target="_blank">
documentation</a
>.
</Field>
<Field label="Object type">
<Dropdown
:options="keysOptions"
:value="settings.structure.objectType"
@change="updateStructure('objectType', $event)"
/>
<Field>0 - node; 1 - edge</Field>
<Field>
A field indicating if the record is node (value&nbsp;0) or edge
(value&nbsp;1).
</Field>
</Field>
<Field label="Node Id">
@@ -20,6 +29,7 @@
:value="settings.structure.nodeId"
@change="updateStructure('nodeId', $event)"
/>
<Field> A field keeping unique node identifier. </Field>
</Field>
<Field label="Edge source">
@@ -28,6 +38,9 @@
:value="settings.structure.edgeSource"
@change="updateStructure('edgeSource', $event)"
/>
<Field>
A field keeping a node identifier where the edge starts.
</Field>
</Field>
<Field label="Edge target">
@@ -36,6 +49,9 @@
:value="settings.structure.edgeTarget"
@change="updateStructure('edgeTarget', $event)"
/>
<Field>
A field keeping a node identifier where the edge ends.
</Field>
</Field>
</Fold>
</Panel>
@@ -164,6 +180,7 @@
</Panel>
</PanelMenuWrapper>
</GraphEditorControls>
<div
ref="graph"
:style="{
@@ -209,6 +226,7 @@ import NodeColorSettings from '@/components/Graph/NodeColorSettings.vue'
import NodeSizeSettings from '@/components/Graph/NodeSizeSettings.vue'
import EdgeSizeSettings from '@/components/Graph/EdgeSizeSettings.vue'
import EdgeColorSettings from '@/components/Graph/EdgeColorSettings.vue'
import events from '@/lib/utils/events'
export default {
components: {
@@ -235,7 +253,8 @@ export default {
inject: ['tabLayout'],
props: {
dataSources: Object,
initOptions: Object
initOptions: Object,
showViewSettings: Boolean
},
emits: ['update'],
data() {
@@ -275,7 +294,7 @@ export default {
nodes: {
size: {
type: 'constant',
value: 4
value: 10
},
color: {
type: 'constant',
@@ -359,6 +378,14 @@ export default {
this.buildGraph()
}
},
'settings.layout.type': {
immediate: true,
handler() {
events.send('viz_graph.render', null, {
layout: this.settings.layout.type
})
}
},
tabLayout: {
deep: true,
handler() {
@@ -611,7 +638,7 @@ export default {
</script>
<style scoped>
.plotly_editor > div {
.plotly_editor.with_controls > div {
display: flex !important;
}

View File

@@ -1,19 +1,27 @@
<template>
<div ref="graphContainer" class="chart-container">
<div v-show="!dataSources" class="warning chart-warning">
<div ref="graphContainer" class="graph-container">
<div v-show="!dataSources" class="warning data-view-warning">
There is no data to build a graph. Run your SQL query and make sure the
result is not empty.
</div>
<div v-show="!dataSourceIsValid" class="warning data-view-warning">
Result set is invalid for graph visualisation. Learn more in
<a href="https://sqliteviz.com/docs/graph/" target="_blank">
documentation</a
>.
</div>
<div
class="graph"
:style="{
height: !dataSources ? 'calc(100% - 40px)' : '100%'
height:
!dataSources || !dataSourceIsValid ? 'calc(100% - 40px)' : '100%'
}"
>
<GraphEditor
ref="graphEditor"
:dataSources="dataSources"
:initOptions="initOptions"
:showViewSettings="showViewSettings"
@update="$emit('update')"
/>
</div>
@@ -22,8 +30,8 @@
<script>
import 'react-chart-editor/lib/react-chart-editor.css'
import events from '@/lib/utils/events'
import GraphEditor from './GraphEditor.vue'
import { dataSourceIsValid } from '@/lib/graphHelper'
export default {
name: 'Graph',
@@ -33,7 +41,8 @@ export default {
initOptions: Object,
exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean,
exportToHtmlEnabled: Boolean
exportToHtmlEnabled: Boolean,
showViewSettings: Boolean
},
emits: [
'update:exportToSvgEnabled',
@@ -46,7 +55,6 @@ export default {
resizeObserver: null
}
},
created() {
this.$emit('update:exportToSvgEnabled', false)
this.$emit('update:exportToHtmlEnabled', false)
@@ -58,6 +66,17 @@ export default {
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.graphContainer)
},
watch: {
async showViewSettings() {
await this.$nextTick()
this.handleResize()
}
},
computed: {
dataSourceIsValid() {
return !this.dataSources || dataSourceIsValid(this.dataSources)
}
},
methods: {
getOptionsForSave() {
return this.$refs.graphEditor.settings
@@ -73,7 +92,7 @@ export default {
const renderer = this.$refs.graphEditor.renderer
if (renderer) {
renderer.refresh()
renderer.getCamera().setState({ x: 0.5, y: 0.5 })
renderer.getCamera().animatedReset({ duration: 600 })
}
}
}
@@ -81,18 +100,11 @@ export default {
</script>
<style scoped>
.chart-container {
.graph-container {
height: 100%;
}
.chart-warning {
height: 40px;
line-height: 40px;
border-bottom: 1px solid var(--color-border);
box-sizing: border-box;
}
.chart {
.graph {
min-height: 242px;
}

View File

@@ -1,6 +1,5 @@
<template>
<div class="pivot-ui">
<div :class="{ collapsed }">
<div class="row">
<label>Columns</label>
<multiselect
@@ -129,10 +128,6 @@
</multiselect>
</div>
</div>
<span class="switcher" @click="collapsed = !collapsed">
{{ collapsed ? 'Show pivot settings' : 'Hide pivot settings' }}
</span>
</div>
</template>
<script>
@@ -163,7 +158,6 @@ export default {
const rendererName =
(this.modelValue && this.modelValue.rendererName) || 'Table'
return {
collapsed: false,
renderer: {
name: rendererName,
fun: $.pivotUtilities.renderers[rendererName]
@@ -291,9 +285,6 @@ export default {
margin-left: 12px;
flex-shrink: 0;
}
.collapsed {
display: none;
}
.switcher {
display: block;

View File

@@ -5,6 +5,7 @@
result is not empty.
</div>
<pivot-ui
v-show="showViewSettings"
v-model="pivotOptions"
:keyNames="columns"
@update="$emit('update')"
@@ -47,7 +48,8 @@ export default {
dataSources: Object,
initOptions: Object,
exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean
exportToSvgEnabled: Boolean,
showViewSettings: Boolean
},
emits: [
'loadingImageCompleted',
@@ -125,6 +127,9 @@ export default {
},
pivotOptions() {
this.show()
},
showViewSettings() {
this.handleResize()
}
},
created() {
@@ -321,4 +326,8 @@ export default {
.pivot-output:empty {
flex-grow: 0;
}
:deep(.js-plotly-plot) {
height: 100%;
}
</style>

View File

@@ -9,6 +9,7 @@
v-model:exportToHtmlEnabled="exportToHtmlEnabled"
:initOptions="initOptionsByMode[mode]"
:data-sources="dataSource"
:showViewSettings="showViewSettings"
@loading-image-completed="loadingImage = false"
@update="$emit('update')"
/>
@@ -42,6 +43,18 @@
<div class="side-tool-bar-divider" />
<icon-button
ref="settingsBtn"
:active="showViewSettings"
tooltip="Toggle visualisation settings visibility"
tooltipPosition="top-left"
@click="showViewSettings = !showViewSettings"
>
<settings-icon />
</icon-button>
<div class="side-tool-bar-divider" />
<icon-button
:disabled="!exportToPngEnabled || loadingImage"
:loading="loadingImage"
@@ -103,6 +116,7 @@ import IconButton from '@/components/IconButton'
import ChartIcon from '@/components/svg/chart'
import PivotIcon from '@/components/svg/pivot'
import GraphIcon from '@/components/svg/graph.vue'
import SettingsIcon from '@/components/svg/settings.vue'
import HtmlIcon from '@/components/svg/html'
import ExportToSvgIcon from '@/components/svg/exportToSvg'
import PngIcon from '@/components/svg/png'
@@ -123,6 +137,7 @@ export default {
ChartIcon,
PivotIcon,
GraphIcon,
SettingsIcon,
ExportToSvgIcon,
PngIcon,
HtmlIcon,
@@ -150,7 +165,8 @@ export default {
pivot: this.initMode === 'pivot' ? this.initOptions : null,
graph: this.initMode === 'graph' ? this.initOptions : null
},
showLoadingDialog: false
showLoadingDialog: false,
showViewSettings: true
}
},
computed: {
@@ -238,6 +254,8 @@ export default {
events.send(
this.mode === 'chart' || this.plotlyInPivot
? 'viz_plotly.export'
: this.mode === 'graph'
? 'viz_graph.export'
: 'viz_pivot.export',
null,
eventLabels

View File

@@ -63,7 +63,9 @@ export default {
border-left: 1px solid var(--color-border-light);
padding: 6px;
}
</style>
<style>
.side-tool-bar-divider {
width: 26px;
height: 1px;

View File

@@ -42,8 +42,8 @@ export default {
) {
const stmt = [
'/*',
' * Your database is empty. In order to start building charts',
' * you should create a table and insert data into it.',
' * Your database is empty. In order to start building data visualisations',
' * you should create tables and insert data into them.',
' */',
'CREATE TABLE house',
'(',
@@ -54,7 +54,20 @@ export default {
"('Gryffindor', 100),",
"('Hufflepuff', 90),",
"('Ravenclaw', 95),",
"('Slytherin', 80);"
"('Slytherin', 80);",
'',
'CREATE TABLE student',
'(',
' id INTEGER,',
' name TEXT,',
' house TEXT',
');',
'INSERT INTO student VALUES',
"(1, 'Harry Potter', 'Gryffindor'),",
"(2, 'Ron Weasley', 'Gryffindor'),",
"(3, 'Draco Malfoy', 'Slytherin'),",
"(4, 'Luna Lovegood', 'Ravenclaw'),",
"(5, 'Cedric Diggory', 'Hufflepuff');"
].join('\n')
const tabId = await this.$store.dispatch('addTab', { query: stmt })