1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2026-03-24 23:16:18 +08:00

Compare commits

...

26 Commits

Author SHA1 Message Date
lana-k
a59946c09d remove karma config 2025-12-24 22:14:48 +01:00
lana-k
7b06b3d9c8 uninstall mesa 2025-12-24 22:06:39 +01:00
lana-k
ced933f497 revert firefox base and env 2025-12-24 21:59:23 +01:00
lana-k
cda368f109 xvfb 2025-12-24 21:49:53 +01:00
lana-k
df67466c2f firefox base 2025-12-24 21:41:29 +01:00
lana-k
528549ae5a LIBGL_ALWAYS_SOFTWARE 2025-12-24 21:36:47 +01:00
lana-k
20f4dcc645 fix package names 2025-12-24 17:51:40 +01:00
lana-k
b8353ef0ce install mesa 2025-12-24 17:49:07 +01:00
lana-k
7975f419c9 anoter settings 2025-12-24 17:40:18 +01:00
lana-k
72aa0dd80b another settings 2025-12-24 17:30:05 +01:00
lana-k
e000ee71fc ensure webgl is enabled infirefox 2025-12-24 17:25:25 +01:00
lana-k
b6a12668d3 #43 fix lint errors 2025-12-24 16:17:49 +01:00
lana-k
713f5ac768 #43 graph 0.28.0 2025-12-23 21:41:17 +01:00
lana-k
5492609c3a update readme 2025-12-23 21:28:32 +01:00
lana-k
8bfd0f5944 tests 2025-12-23 21:15:44 +01:00
lana-k
a8006bcf52 tests 2025-12-17 21:26:57 +01:00
lana-k
1463f93bb0 tests for layouts 2025-12-13 17:57:48 +01:00
lana-k
5108495430 test for canvas 2025-12-13 13:00:13 +01:00
lana-k
d28968e539 tests 2025-12-07 19:56:16 +01:00
lana-k
68221cba6d tests 2025-11-15 14:29:41 +01:00
lana-k
65c1c18fcb tests 2025-11-08 22:23:38 +01:00
lana-k
d7db6a0f5d fix tests 2025-11-02 12:31:32 +01:00
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
34 changed files with 6531 additions and 22098 deletions

View File

@@ -23,7 +23,10 @@ jobs:
export DEBIAN_FRONTEND=noninteractive
sudo add-apt-repository -y ppa:mozillateam/ppa
sudo apt-get update
sudo apt-get install -y chromium-browser firefox-esr
sudo apt-get install -y \
chromium-browser \
firefox-esr \
xvfb
- name: Update npm
run: npm install -g npm@10
@@ -35,4 +38,4 @@ jobs:
run: npm run lint -- --no-fix
- name: Run karma tests
run: npm run test
run: xvfb-run -a npm test

View File

@@ -9,7 +9,7 @@ of SQLite databases, CSV, JSON or NDJSON files.
With sqliteviz you can:
- run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets
- run SQL queries against a SQLite database and create [Plotly][11] charts, graphs and pivot tables based on the result sets
- import a CSV/JSON/NDJSON file into a SQLite database and visualize imported data
- export result set to CSV file
- manage inquiries and run them against different databases
@@ -33,7 +33,9 @@ It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
## Components
It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4] and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9].
It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4]
and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9].
Graphs are visualized with [Sigma.js][13] and [Graphology][14].
[1]: https://github.com/plotly/falcon
[2]: https://github.com/getredash/redash
@@ -47,3 +49,5 @@ It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4]
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
[11]: https://github.com/plotly/plotly.js
[12]: https://github.com/nicolaskruchten/pivottable
[13]: https://www.sigmajs.org/
[14]: https://graphology.github.io/

23766
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "sqliteviz",
"version": "0.27.1",
"version": "0.28.0",
"license": "Apache-2.0",
"private": true,
"type": "module",

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,15 @@
<template>
<Field label="Adjust sizes">
<Field label="Scaling ratio" fieldContainerClassName="test_fa2_scaling">
<NumericInput
:value="modelValue.scalingRatio"
@update="update('scalingRatio', $event)"
/>
</Field>
<Field
label="Prevent overlapping"
fieldContainerClassName="test_fa2_adjustSizes"
>
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.adjustSizes"
@@ -7,7 +17,10 @@
/>
</Field>
<Field label="Barnes-Hut optimize">
<Field
label="Barnes-Hut optimize"
fieldContainerClassName="test_fa2_barnes_hut"
>
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.barnesHutOptimize"
@@ -15,21 +28,21 @@
/>
</Field>
<Field v-show="modelValue.barnesHutOptimize" label="Barnes-Hut Theta">
<Field
v-show="modelValue.barnesHutOptimize"
label="Barnes-Hut Theta"
fieldContainerClassName="test_fa2_barnes_theta"
>
<NumericInput
:value="modelValue.barnesHutTheta"
@update="update('barnesHutTheta', $event)"
/>
</Field>
<Field label="Gravity">
<NumericInput
:value="modelValue.gravity"
@update="update('gravity', $event)"
/>
</Field>
<Field label="Strong gravity mode">
<Field
label="Strong gravity mode"
fieldContainerClassName="test_fa2_strong_gravity"
>
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.strongGravityMode"
@@ -37,7 +50,10 @@
/>
</Field>
<Field label="Noack's LinLog model">
<Field
label="Noack's LinLog model"
fieldContainerClassName="test_fa2_lin_log"
>
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.linLogMode"
@@ -45,7 +61,10 @@
/>
</Field>
<Field label="Out bound attraction distribution">
<Field
label="Outbound attraction distribution"
fieldContainerClassName="test_fa2_outbound_attraction"
>
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.outboundAttractionDistribution"
@@ -53,7 +72,7 @@
/>
</Field>
<Field label="Slow down">
<Field label="Slow down" fieldContainerClassName="test_fa2_slow_down">
<NumericInput
:value="modelValue.slowDown"
:min="0"
@@ -65,10 +84,15 @@
<Dropdown
:options="keyOptions"
:value="modelValue.weightSource"
className="test_fa2_weight_source"
@change="update('weightSource', $event)"
/>
</Field>
<Field v-show="modelValue.weightSource" label="Edge weight influence">
<Field
v-show="modelValue.weightSource"
label="Edge weight influence"
fieldContainerClassName="test_fa2_weight_influence"
>
<NumericInput
:value="modelValue.edgeWeightInfluence"
@update="update('edgeWeightInfluence', $event)"

View File

@@ -40,7 +40,6 @@
import { applyPureReactInVue } from 'veaury'
import Field from 'react-chart-editor/lib/components/fields/Field'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import Multiselect from 'vue-multiselect'
import 'react-chart-editor/lib/react-chart-editor.css'
@@ -48,7 +47,6 @@ export default {
components: {
Field: applyPureReactInVue(Field),
NumericInput: applyPureReactInVue(NumericInput),
Dropdown: applyPureReactInVue(Dropdown),
Multiselect
},
props: {

View File

@@ -1,18 +1,21 @@
<template>
<Field label="Color">
<Field label="Color" fieldContainerClassName="test_edge_color">
<RadioBlocks
:options="edgeColorTypeOptions"
:activeOption="modelValue.type"
@option-change="updateColorType"
/>
<Field v-if="modelValue.type === 'constant'">
<Field
v-if="modelValue.type === 'constant'"
fieldContainerClassName="test_edge_color_value"
>
<ColorPicker
:selectedColor="modelValue.value"
@color-change="updateSettings('value', $event)"
/>
</Field>
<template v-else>
<Field>
<Field fieldContainerClassName="test_edge_color_value">
<Dropdown
v-if="modelValue.type === 'variable'"
:options="keyOptions"
@@ -21,7 +24,7 @@
/>
</Field>
<Field>
<Field fieldContainerClassName="test_edge_color_mapping_mode">
<RadioBlocks
:options="colorSourceUsageOptions"
:activeOption="modelValue.sourceUsage"
@@ -39,7 +42,11 @@
</template>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Color as">
<Field
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
label="Color as"
fieldContainerClassName="test_edge_color_as"
>
<RadioBlocks
:options="сolorAsOptions"
:activeOption="modelValue.mode"
@@ -47,7 +54,11 @@
/>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Colorscale direction">
<Field
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
label="Colorscale direction"
fieldContainerClassName="test_edge_color_colorscale_direction"
>
<RadioBlocks
:options="сolorscaleDirections"
:activeOption="modelValue.colorscaleDirection"

View File

@@ -1,12 +1,12 @@
<template>
<Field label="Size">
<Field label="Size" fieldContainerClassName="test_edge_size">
<RadioBlocks
:options="edgeSizeTypeOptions"
:activeOption="modelValue.type"
@option-change="updateSizeType"
/>
<Field>
<Field fieldContainerClassName="test_edge_size_value">
<NumericInput
v-if="modelValue.type === 'constant'"
:value="modelValue.value"
@@ -23,14 +23,14 @@
</Field>
<template v-if="modelValue.type !== 'constant'">
<Field label="Size scale">
<Field label="Size scale" fieldContainerClassName="test_edge_size_scale">
<NumericInput
:value="modelValue.scale"
@update="updateSettings('scale', $event)"
/>
</Field>
<Field label="Minimum size">
<Field label="Minimum size" fieldContainerClassName="test_edge_size_min">
<NumericInput
:value="modelValue.min"
@update="updateSettings('min', $event)"

View File

@@ -1,5 +1,8 @@
<template>
<Field label="Initial iterations">
<Field
label="Initial iterations"
fieldContainerClassName="test_fa2_iteration_amount"
>
<NumericInput
:value="modelValue.initialIterationsAmount"
:min="1"
@@ -7,10 +10,10 @@
/>
</Field>
<Field label="Scaling ratio">
<Field label="Gravity" fieldContainerClassName="test_fa2_gravity">
<NumericInput
:value="modelValue.scalingRatio"
@update="update('scalingRatio', $event)"
:value="modelValue.gravity"
@update="update('gravity', $event)"
/>
</Field>
</template>

View File

@@ -1,41 +1,61 @@
<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 label="Object type">
<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 ref="objectTypeField" label="Object type">
<Dropdown
:options="keysOptions"
:value="settings.structure.objectType"
className="test_object_type_select"
@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">
<Dropdown
:options="keysOptions"
:value="settings.structure.nodeId"
className="test_node_id_select"
@change="updateStructure('nodeId', $event)"
/>
<Field> A field keeping unique node identifier. </Field>
</Field>
<Field label="Edge source">
<Dropdown
:options="keysOptions"
:value="settings.structure.edgeSource"
className="test_edge_source_select"
@change="updateStructure('edgeSource', $event)"
/>
<Field>
A field keeping a node identifier where the edge starts.
</Field>
</Field>
<Field label="Edge target">
<Dropdown
:options="keysOptions"
:value="settings.structure.edgeTarget"
className="test_edge_target_select"
@change="updateStructure('edgeTarget', $event)"
/>
<Field>
A field keeping a node identifier where the edge ends.
</Field>
</Field>
</Fold>
</Panel>
@@ -55,6 +75,7 @@
<Dropdown
:options="keysOptions"
:value="settings.style.nodes.label.source"
className="test_label_select"
@change="updateNodes('label.source', $event)"
/>
</Field>
@@ -81,7 +102,10 @@
<Panel group="Style" name="Edges">
<Fold name="Edges">
<Field label="Direction">
<Field
label="Direction"
fieldContainerClassName="test_edge_direction"
>
<RadioBlocks
:options="visibilityOptions"
:activeOption="settings.style.edges.showDirection"
@@ -93,6 +117,7 @@
<Dropdown
:options="keysOptions"
:value="settings.style.edges.label.source"
className="test_edge_label_select"
@change="updateEdges('label.source', $event)"
/>
</Field>
@@ -123,6 +148,8 @@
<Dropdown
:options="layoutOptions"
:value="settings.layout.type"
:clearable="false"
className="test_layout_algorithm_select"
@change="updateLayout($event)"
/>
</Field>
@@ -143,10 +170,18 @@
/>
</Fold>
<div class="force-atlas-buttons">
<Button variant="secondary" @click="resetFA2LayoutSettings">
<Button
variant="secondary"
class="test_fa2_reset"
@click="resetFA2LayoutSettings"
>
Reset
</Button>
<Button variant="primary" @click="toggleFA2Layout">
<Button
variant="primary"
class="test_fa2_toggle"
@click="toggleFA2Layout"
>
<template #node:icon>
<div
:style="{
@@ -164,8 +199,10 @@
</Panel>
</PanelMenuWrapper>
</GraphEditorControls>
<div
ref="graph"
class="test_graph_output"
:style="{
height: '100%',
width: '100%',
@@ -188,10 +225,11 @@ 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'
import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue'
// eslint-disable-next-line max-len
import AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue'
import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue'
import FA2Layout from 'graphology-layout-forceatlas2/worker'
import forceAtlas2 from 'graphology-layout-forceatlas2'
import * as 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'
@@ -209,6 +247,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 +274,8 @@ export default {
inject: ['tabLayout'],
props: {
dataSources: Object,
initOptions: Object
initOptions: Object,
showViewSettings: Boolean
},
emits: ['update'],
data() {
@@ -275,7 +315,7 @@ export default {
nodes: {
size: {
type: 'constant',
value: 4
value: 10
},
color: {
type: 'constant',
@@ -319,11 +359,10 @@ export default {
if (!this.dataSources) {
return []
}
const firstColumnName = Object.keys(this.dataSources)[0]
try {
return (
this.dataSources[Object.keys(this.dataSources)[0] || 'doc'].map(
json => JSON.parse(json)
) || []
this.dataSources[firstColumnName].map(json => JSON.parse(json)) || []
)
} catch {
return []
@@ -359,6 +398,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() {
@@ -390,9 +437,7 @@ export default {
this.updateLayout(this.settings.layout.type)
this.renderer = new Sigma(this.graph, this.$refs.graph, {
renderEdgeLabels: true,
allowInvalidContainer: true,
labelColor: { attribute: 'labelColor', color: '#444444' },
edgeLabelColor: { attribute: 'labelColor', color: '#a2b1c6' }
allowInvalidContainer: true
})
if (this.settings.layout.type === 'forceAtlas2') {
this.autoRunFA2Layout()
@@ -464,7 +509,7 @@ export default {
if (layoutType === 'random') {
random.assign(this.graph, {
rng: seedrandom(this.settings.layout.options.seedValue || 1)
rng: seedrandom(this.settings.layout.options.seedValue)
})
return
}
@@ -497,7 +542,7 @@ export default {
this.settings.layout.options.hierarchyAttributes?.map(
(_, index) => 'hierarchyAttribute' + index
) || [],
rng: seedrandom(this.settings.layout.options.seedValue || 1)
rng: seedrandom(this.settings.layout.options.seedValue)
})
return
}
@@ -547,10 +592,6 @@ export default {
}
},
autoRunFA2Layout() {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
}
let iteration = 1
this.checkIteration = () => {
if (
@@ -565,7 +606,7 @@ export default {
this.fa2Layout.start()
},
setRecommendedFA2Settings() {
const sensibleSettings = forceAtlas2.inferSettings(this.graph)
const sensibleSettings = forceAtlas2.default.inferSettings(this.graph)
this.settings.layout.options = {
initialIterationsAmount: 50,
adjustSizes: false,
@@ -611,7 +652,7 @@ export default {
</script>
<style scoped>
.plotly_editor > div {
.plotly_editor.with_controls > div {
display: flex !important;
}

View File

@@ -1,18 +1,21 @@
<template>
<Field label="Color">
<Field label="Color" fieldContainerClassName="test_node_color">
<RadioBlocks
:options="nodeColorTypeOptions"
:activeOption="modelValue.type"
@option-change="updateColorType"
/>
<Field v-if="modelValue.type === 'constant'">
<Field
v-if="modelValue.type === 'constant'"
fieldContainerClassName="test_node_color_value"
>
<ColorPicker
:selectedColor="modelValue.value"
@color-change="updateSettings('value', $event)"
/>
</Field>
<template v-else>
<Field>
<Field fieldContainerClassName="test_node_color_value">
<Dropdown
v-if="modelValue.type === 'variable'"
:options="keyOptions"
@@ -23,11 +26,15 @@
v-if="modelValue.type === 'calculated'"
:options="nodeCalculatedColorMethodOptions"
:value="modelValue.method"
:clearable="false"
@change="updateSettings('method', $event)"
/>
</Field>
<Field>
<Field
v-if="modelValue.type === 'variable'"
fieldContainerClassName="test_node_color_mapping_mode"
>
<RadioBlocks
:options="colorSourceUsageOptions"
:activeOption="modelValue.sourceUsage"
@@ -35,7 +42,12 @@
/>
</Field>
<Field v-if="modelValue.sourceUsage === 'map_to'">
<Field
v-if="
modelValue.sourceUsage === 'map_to' ||
modelValue.type === 'calculated'
"
>
<ColorscalePicker
:selected="modelValue.colorscale"
className="colorscale-picker"
@@ -45,7 +57,13 @@
</template>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Color as">
<Field
v-if="
modelValue.sourceUsage === 'map_to' || modelValue.type === 'calculated'
"
label="Color as"
fieldContainerClassName="test_node_color_as"
>
<RadioBlocks
:options="сolorAsOptions"
:activeOption="modelValue.mode"
@@ -53,7 +71,13 @@
/>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Colorscale direction">
<Field
v-if="
modelValue.sourceUsage === 'map_to' || modelValue.type === 'calculated'
"
label="Colorscale direction"
fieldContainerClassName="test_node_color_colorscale_direction"
>
<RadioBlocks
:options="сolorscaleDirections"
:activeOption="modelValue.colorscaleDirection"
@@ -120,7 +144,6 @@ export default {
},
calculated: {
method: 'degree',
sourceUsage: 'map_to',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'normal'

View File

@@ -1,16 +1,17 @@
<template>
<Field label="Size">
<Field label="Size" fieldContainerClassName="test_node_size">
<RadioBlocks
:options="nodeSizeTypeOptions"
:activeOption="modelValue.type"
@option-change="updateSizeType"
/>
<Field>
<Field fieldContainerClassName="test_node_size_value">
<NumericInput
v-if="modelValue.type === 'constant'"
:value="modelValue.value"
:min="1"
class="test_node_size_value"
@update="updateSettings('value', $event)"
/>
<Dropdown
@@ -23,20 +24,21 @@
v-if="modelValue.type === 'calculated'"
:options="nodeCalculatedSizeMethodOptions"
:value="modelValue.method"
:clearable="false"
@change="updateSettings('method', $event)"
/>
</Field>
</Field>
<template v-if="modelValue.type !== 'constant'">
<Field label="Size scale">
<Field label="Size scale" fieldContainerClassName="test_node_size_scale">
<NumericInput
:value="modelValue.scale"
@update="updateSettings('scale', $event)"
/>
</Field>
<Field label="Size mode">
<Field label="Size mode" fieldContainerClassName="test_node_size_mode">
<RadioBlocks
:options="nodeSizeModeOptions"
:activeOption="modelValue.mode"
@@ -44,7 +46,7 @@
/>
</Field>
<Field label="Minimum size">
<Field label="Minimum size" fieldContainerClassName="test_node_size_min">
<NumericInput
:value="modelValue.min"
@update="updateSettings('min', $event)"

View File

@@ -7,27 +7,33 @@
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 4C5 5.10457 4.10457 6 3 6C1.89543 6 1 5.10457 1 4C1 2.89543 1.89543 2 3 2C4.10457 2 5 2.89543 5 4Z"
d="M5 4C5 5.10457 4.10457 6 3 6C1.89543 6 1 5.10457 1 4C1 2.89543
1.89543 2 3 2C4.10457 2 5 2.89543 5 4Z"
fill="#A2B1C6"
/>
<path
d="M17 7.5C17 8.88071 15.8807 10 14.5 10C13.1193 10 12 8.88071 12 7.5C12 6.11929 13.1193 5 14.5 5C15.8807 5 17 6.11929 17 7.5Z"
d="M17 7.5C17 8.88071 15.8807 10 14.5 10C13.1193 10 12 8.88071 12
7.5C12 6.11929 13.1193 5 14.5 5C15.8807 5 17 6.11929 17 7.5Z"
fill="#A2B1C6"
/>
<path
d="M8 13.5C8 14.8807 6.88071 16 5.5 16C4.11929 16 3 14.8807 3 13.5C3 12.1193 4.11929 11 5.5 11C6.88071 11 8 12.1193 8 13.5Z"
d="M8 13.5C8 14.8807 6.88071 16 5.5 16C4.11929 16 3 14.8807 3 13.5C3
12.1193 4.11929 11 5.5 11C6.88071 11 8 12.1193 8 13.5Z"
fill="#A2B1C6"
/>
<path
d="M2.93128 5.31436L3.90527 5.08778L5.48693 11.8867L4.51294 12.1133L2.93128 5.31436Z"
d="M2.93128 5.31436L3.90527 5.08778L5.48693 11.8867L4.51294
12.1133L2.93128 5.31436Z"
fill="#A2B1C6"
/>
<path
d="M12.9447 7.79159L13.5548 8.58392L7.30516 13.3962L6.69507 12.6038L12.9447 7.79159Z"
d="M12.9447 7.79159L13.5548 8.58392L7.30516 13.3962L6.69507
12.6038L12.9447 7.79159Z"
fill="#A2B1C6"
/>
<path
d="M14.1316 6.51712L3.13166 3.51723L2.86844 4.48202L13.8684 7.48191L14.1316 6.51712Z"
d="M14.1316 6.51712L3.13166 3.51723L2.86844 4.48202L13.8684
7.48191L14.1316 6.51712Z"
fill="#A2B1C6"
/>
</svg>

View File

@@ -0,0 +1,130 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_2236_2707)">
<path
d="M8.11441 0.0274963C7.73011 0.0274918 7.35916 0.168422 7.0718
0.423594C6.78445 0.678766 6.60065 1.03046 6.55522 1.41207L7.89167
1.57122C7.89815 1.51669 7.9244 1.46644 7.96546 1.42997C8.00651 1.39351
8.05951 1.37337 8.11441 1.37337V0.0274963ZM9.88559
0.0274963H8.11441V1.37337H9.88559V0.0274963ZM11.4448
1.41207C11.3994 1.03046 11.2156 0.678766 10.9282 0.423594C10.6408
0.168422 10.2699 0.0274918 9.88559 0.0274963V1.37337C9.94049 1.37337
9.99349 1.39351 10.0345 1.42997C10.0756 1.46644 10.1018 1.51669
10.1083 1.57122L11.4448 1.41207ZM11.5815 2.56055L11.4448
1.41207L10.1083 1.57122L10.2449 2.7197L11.5815 2.56055ZM14.349
3.0888L13.282 3.5464L13.8123 4.78326L14.8793 4.32566L14.349
3.0888ZM16.3276 3.74682C16.1355 3.41401 15.8279 3.16322 15.4633
3.04195C15.0986 2.92067 14.7021 2.93735 14.349 3.0888L14.8793
4.32566C14.9298 4.304 14.9865 4.30172 15.0386 4.31904C15.0907
4.33636 15.1346 4.3722 15.1621 4.41976L16.3276 3.74682ZM17.2132
5.28067L16.3276 3.74682L15.1621 4.41976L16.0477 5.95361L17.2132
5.28067ZM16.7937 7.32326C17.1015 7.09311 17.3142 6.75809 17.3915
6.38165C17.4688 6.00521 17.4054 5.61348 17.2132 5.28067L16.0477
5.95361C16.0751 6.00117 16.0842 6.05714 16.0731 6.11093C16.0621
6.16471 16.0317 6.21257 15.9877 6.24544L16.7937 7.32326ZM15.7861
8.07673L16.7937 7.32326L15.9877 6.24544L14.9801 6.99891L15.7861
8.07673ZM16.7937 10.6767L15.7861 9.92327L14.9802 11.0011L15.9877
11.7546L16.7937 10.6767ZM17.2132 12.7193C17.4054 12.3865 17.4688
11.9948 17.3915 11.6183C17.3142 11.2419 17.1015 10.9069 16.7937
10.6767L15.9877 11.7546C16.0317 11.7874 16.0621 11.8353 16.0731
11.8891C16.0842 11.9429 16.0751 11.9988 16.0477 12.0464L17.2132
12.7193ZM16.3276 14.2532L17.2132 12.7193L16.0477 12.0464L15.1621
13.5802L16.3276 14.2532ZM14.349 14.9112C14.7021 15.0626 15.0986
15.0793 15.4633 14.958C15.8279 14.8368 16.1355 14.586 16.3276
14.2532L15.1621 13.5802C15.1346 13.6278 15.0907 13.6636 15.0386
13.6809C14.9865 13.6982 14.9298 13.6959 14.8793 13.6742L14.349
14.9112ZM13.282 14.4536L14.349 14.9112L14.8793 13.6742L13.8125
13.2167L13.282 14.4536ZM11.4448 16.5879L11.5814 15.4394L10.245
15.2803L10.1083 16.4288L11.4448 16.5879ZM9.88559 17.9725C10.2699
17.9725 10.6408 17.8316 10.9282 17.5764C11.2156 17.3212 11.3994
16.9695 11.4448 16.5879L10.1083 16.4288C10.1018 16.4833 10.0756
16.5336 10.0345 16.57C9.99349 16.6065 9.94049 16.6266 9.88559
16.6266V17.9725ZM8.11441
17.9725H9.88559V16.6266H8.11441V17.9725ZM6.55522
16.5879C6.60065 16.9695 6.78445 17.3212 7.0718 17.5764C7.35916
17.8316 7.73011 17.9725 8.11441 17.9725V16.6266C8.05951 16.6266
8.00651 16.6065 7.96546 16.57C7.9244 16.5336 7.89815 16.4833
7.89167 16.4288L6.55522 16.5879ZM6.44172 15.6342L6.55522 1
6.5879L7.89167 16.4288L7.77817 15.475L6.44172 15.6342ZM4.00097
13.2967L3.12066 13.6742L3.65104 14.9112L4.53136 14.5337L4.00097
13.2967ZM3.12066 13.6742C3.07021 13.6959 3.01346 13.6982 2.96138
13.6809C2.90929 13.6636 2.86536 13.6278 2.83791 13.5802L1.67238
14.2532C1.86453 14.586 2.17205 14.8368 2.53672 14.958C2.90138
15.0793 3.29785 15.0626 3.65104 14.9112L3.12066 13.6742ZM2.83791
13.5802L1.95233 12.0464L0.786798 12.7193L1.67238 14.2532L2.83791
13.5802ZM1.95233 12.0464C1.92487 11.9988 1.91582 11.9429 1.92688
11.8891C1.93794 11.8353 1.96834 11.7874 2.01233 11.7546L1.20626
10.6767C0.898499 10.9069 0.685823 11.2419 0.608517 11.6183C0.531211
11.9948 0.594643 12.3865 0.786798 12.7193L1.95233 12.0464ZM2.01233
11.7546L2.70007 11.2403L1.89389 10.1625L1.20626 10.6767L2.01233
11.7546ZM1.20626 7.32326L1.894 7.83761L2.70007 6.75979L2.01233
6.24544L1.20626 7.32326ZM0.786798 5.28067C0.594643 5.61348
0.531211 6.00521 0.608517 6.38165C0.685823 6.75809 0.898499
7.09311 1.20626 7.32326L2.01233 6.24544C1.96834 6.21257
1.93794 6.16471 1.92688 6.11093C1.91582 6.05714 1.92487
6.00117 1.95233 5.95361L0.786798 5.28067ZM1.67238
3.74682L0.786798 5.28067L1.95233 5.95361L2.83791
4.41976L1.67238 3.74682ZM3.65104 3.0888C3.29785
2.93735 2.90138 2.92067 2.53672 3.04195C2.17205 3.16322 1.86453
3.41401 1.67238 3.74682L2.83791 4.41976C2.86536 4.37223 2.90929
4.33641 2.96138 4.31909C3.01346 4.30176 3.07021 4.30414 3.12066
4.32577L3.65104 3.0888ZM4.53136 3.46632L3.65104 3.0888L3.12066
4.32577L4.00097 4.70329L4.53136 3.46632ZM6.55522 1.41207L6.44172
2.36584L7.77817 2.52499L7.89167 1.57122L6.55522 1.41207ZM6.3585
4.5022C7.04018 4.11795 7.6696 3.4366 7.77817 2.52499L6.44172
2.36584C6.39887 2.72575 6.13116 3.08544 5.69756 3.32994L6.3585
4.5022ZM4.00097 4.70329C4.81792 5.05367 5.68948 4.87938 6.3585
4.5022L5.69756 3.32994C5.28213 3.56412 4.85852 3.60663 4.53136
3.46632L4.00097 4.70329ZM3.72866 9C3.72866 8.20369 3.44288 7.3153
2.70007 6.75979L1.894 7.83761C2.19884 8.06562 2.38278 8.48834 2.38278
9H3.72866ZM6.3585 13.4978C5.68948 13.1206 4.81792 12.9463 4.00097
13.2967L4.53136 14.5337C4.85852 14.3934 5.28213 14.436 5.69756
14.6703L6.3585 13.4978ZM2.70007 11.2403C3.44299 10.6848 3.72866
9.79631 3.72866 9H2.38278C2.38278 9.51177 2.19873 9.9346 1.89389
10.1625L2.70007 11.2403ZM11.5243 13.4358C10.9034 13.8057 10.3448
14.4432 10.245 15.2803L11.5814 15.4394C11.6183 15.1293 11.8415
14.8134 12.2132 14.5919L11.5243 13.4358ZM13.8125 13.2167C13.0169
12.8756 12.1673 13.0527 11.5243 13.4358L12.2132 14.5919C12.5925
14.3659 12.9839 14.3258 13.282 14.4536L13.8125 13.2167ZM7.77817
15.475C7.6696 14.5635 7.04018 13.8822 6.3585 13.4978L5.69756
14.6703C6.13116 14.9147 6.39887 15.2742 6.44172 15.6342L7.77817
15.475ZM14.047 9C14.047 9.71656 14.316 10.5045 14.9802
11.0011L15.7861 9.92327C15.547 9.74449 15.3929 9.41139 15.3929
9H14.047ZM14.9801 6.99891C14.3159 7.49553 14.047 8.28332 14.047
9H15.3929C15.3929 8.58861 15.547 8.2555 15.7861 8.07673L14.9801
6.99891ZM11.5243 4.56422C12.1673 4.94734 13.0168 5.12444 13.8123
4.78326L13.282 3.5464C12.9839 3.67426 12.5925 3.63399 12.2132
3.408L11.5243 4.56422ZM10.2449 2.7197C10.3446 3.55683 10.9034
4.19433 11.5243 4.56422L12.2132 3.408C11.8415 3.1866 11.6184
2.87077 11.5815 2.56055L10.2449 2.7197Z"
fill="#A2B1C6"
/>
<path
d="M11.0935 9C11.0935 7.84373 10.1562 6.90642 8.99988 6.90642C7.84361
6.90642 6.90629 7.84373 6.90629 9C6.90629 10.1563 7.84361 11.0936
8.99988 11.0936C10.1562 11.0936 11.0935 10.1563 11.0935 9ZM12.2898
9C12.2898 10.817 10.8169 12.2899 8.99988 12.2899C7.18289 12.2899
5.70996 10.817 5.70996 9C5.70996 7.18301 7.18289 5.71008 8.99988
5.71008C10.8169 5.71008 12.2898 7.18301 12.2898 9Z"
fill="#A2B1C6"
/>
</g>
<defs>
<clipPath id="clip0_2236_2707">
<rect width="18" height="18" fill="white" />
</clipPath>
</defs>
</svg>
</template>
<script>
export default {
name: 'SettingsIcon'
}
</script>

View File

@@ -7,7 +7,8 @@
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 4C3 3.44772 3.44772 3 4 3H14C14.5523 3 15 3.44772 15 4V14C15 14.5523 14.5523 15 14 15H4C3.44772 15 3 14.5523 3 14V4Z"
d="M3 4C3 3.44772 3.44772 3 4 3H14C14.5523 3 15 3.44772 15 4V14C15
14.5523 14.5523 15 14 15H4C3.44772 15 3 14.5523 3 14V4Z"
fill="#A2B1C6"
/>
</svg>

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,16 +45,17 @@ 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
data: node
})
}
})
}
}
export function buildEdges(graph, dataSources, options) {
const docColumn = Object.keys(dataSources)[0] || 'doc'
const docColumn = Object.keys(dataSources)[0]
const { objectType, edgeSource, edgeTarget } = options.structure
if (objectType && edgeSource && edgeTarget) {
@@ -36,8 +68,7 @@ export function buildEdges(graph, dataSources, options) {
const target = edge[edgeTarget]
if (graph.hasNode(source) && graph.hasNode(target)) {
graph.addEdge(source, target, {
data: edge,
labelColor: options.style.edges.label.color
data: edge
})
}
})
@@ -110,10 +141,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 +228,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)
}
}
@@ -224,7 +259,6 @@ function getColorMethod(
colorscale[index % colorscale.length]
])
)
return (attributes, nodeId) => {
const category = sourceGetter(nodeId, attributes)
attributes.color = colorMap[category]
@@ -240,6 +274,7 @@ function getColorMethod(
const value = sourceGetter(nodeId, attributes)
const normalizedValue = (value - min) / (max - min)
if (isNaN(normalizedValue)) {
attributes.color = '#000000'
return
}
const exactMatch = normalizedColorscale.find(
@@ -251,9 +286,9 @@ function getColorMethod(
}
const rightColorIndex = normalizedColorscale.findIndex(
([value]) => value >= normalizedValue
([value]) => value > normalizedValue
)
const leftColorIndex = (rightColorIndex || 1) - 1
const leftColorIndex = rightColorIndex - 1
const right = normalizedColorscale[rightColorIndex]
const left = normalizedColorscale[leftColorIndex]
const interpolationFactor =
@@ -290,18 +325,3 @@ function getEdgeValueScale(graph, sourceGetter) {
}, 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 default {
getOptionsFromDataSources
}

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() {
@@ -115,8 +120,8 @@ export default {
this.resizeObserver.observe(this.$refs.chartContainer)
if (this.dataSources) {
dereference.default(this.state.data, this.dataSources)
this.updatePlotly()
}
this.handleResize()
},
activated() {
this.useResizeHandler = true
@@ -129,6 +134,10 @@ export default {
},
methods: {
async handleResize() {
// Call updatePlotly twice because there is a small gap (for scrolling?)
// on right and bottom of the plot.
// After the second call it's good.
this.updatePlotly()
this.updatePlotly()
},
onRender() {
@@ -179,13 +188,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,19 +1,30 @@
<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 no-data">
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 invalid-data"
>
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 +33,8 @@
<script>
import 'react-chart-editor/lib/react-chart-editor.css'
import events from '@/lib/utils/events'
import GraphEditor from './GraphEditor.vue'
import GraphEditor from '@/components/Graph/GraphEditor.vue'
import { dataSourceIsValid } from '@/lib/graphHelper'
export default {
name: 'Graph',
@@ -33,11 +44,14 @@ export default {
initOptions: Object,
exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean,
exportToHtmlEnabled: Boolean
exportToHtmlEnabled: Boolean,
showViewSettings: Boolean
},
emits: [
'update:exportToSvgEnabled',
'update:exportToHtmlEnabled',
'update:exportToPngEnabled',
'update:exportToClipboardEnabled',
'update',
'loadingImageCompleted'
],
@@ -46,10 +60,26 @@ export default {
resizeObserver: null
}
},
computed: {
dataSourceIsValid() {
return !this.dataSources || dataSourceIsValid(this.dataSources)
}
},
watch: {
async showViewSettings() {
await this.$nextTick()
this.handleResize()
},
dataSources() {
this.$emit('update:exportToPngEnabled', !!this.dataSources)
this.$emit('update:exportToClipboardEnabled', !!this.dataSources)
}
},
created() {
this.$emit('update:exportToSvgEnabled', false)
this.$emit('update:exportToHtmlEnabled', false)
this.$emit('update:exportToPngEnabled', !!this.dataSources)
this.$emit('update:exportToClipboardEnabled', !!this.dataSources)
},
mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize)
@@ -66,14 +96,14 @@ export default {
await this.$refs.graphEditor.saveAsPng()
this.$emit('loadingImageCompleted')
},
async prepareCopy() {
return await this.$refs.graphEditor.prepareCopy()
prepareCopy() {
return this.$refs.graphEditor.prepareCopy()
},
async handleResize() {
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 +111,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')"
@@ -30,7 +31,7 @@ import fIo from '@/lib/utils/fileIo'
import $ from 'jquery'
import 'pivottable'
import 'pivottable/dist/pivot.css'
import PivotUi from './PivotUi'
import PivotUi from './PivotUi/index.vue'
import pivotHelper from './pivotHelper'
import Chart from '@/views/MainView/Workspace/Tabs/Tab/DataView/Chart'
import chartHelper from '@/lib/chartHelper'
@@ -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() {
@@ -222,12 +227,12 @@ export default {
async prepareCopy() {
if (this.viewCustomChart) {
return await this.$refs.customChart.prepareCopy()
return this.$refs.customChart.prepareCopy()
}
if (this.viewStandartChart) {
return await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
return chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
}
return await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
return pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
},
async saveAsSvg() {
@@ -321,4 +326,8 @@ export default {
.pivot-output:empty {
flex-grow: 0;
}
:deep(.js-plotly-plot) {
height: 100%;
}
</style>

View File

@@ -7,14 +7,17 @@
v-model:exportToPngEnabled="exportToPngEnabled"
v-model:exportToSvgEnabled="exportToSvgEnabled"
v-model:exportToHtmlEnabled="exportToHtmlEnabled"
v-model:exportToClipboardEnabled="exportToClipboardEnabled"
:initOptions="initOptionsByMode[mode]"
:data-sources="dataSource"
:showViewSettings="showViewSettings"
@loading-image-completed="loadingImage = false"
@update="$emit('update')"
/>
</div>
<side-tool-bar panel="dataView" @switch-to="$emit('switchTo', $event)">
<icon-button
ref="chartBtn"
:active="mode === 'chart'"
tooltip="Switch to chart"
tooltipPosition="top-left"
@@ -32,6 +35,7 @@
<pivot-icon />
</icon-button>
<icon-button
ref="graphBtn"
:active="mode === 'graph'"
tooltip="Switch to graph"
tooltipPosition="top-left"
@@ -43,6 +47,19 @@
<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
ref="pngExportBtn"
:disabled="!exportToPngEnabled || loadingImage"
:loading="loadingImage"
tooltip="Save as PNG image"
@@ -72,6 +89,7 @@
</icon-button>
<icon-button
ref="copyToClipboardBtn"
:disabled="!exportToClipboardEnabled"
:loading="copyingImage"
tooltip="Copy visualisation to clipboard"
tooltipPosition="top-left"
@@ -103,6 +121,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 +142,7 @@ export default {
ChartIcon,
PivotIcon,
GraphIcon,
SettingsIcon,
ExportToSvgIcon,
PngIcon,
HtmlIcon,
@@ -141,6 +161,7 @@ export default {
exportToPngEnabled: true,
exportToSvgEnabled: true,
exportToHtmlEnabled: true,
exportToClipboardEnabled: true,
loadingImage: false,
copyingImage: false,
preparingCopy: false,
@@ -150,7 +171,8 @@ export default {
pivot: this.initMode === 'pivot' ? this.initOptions : null,
graph: this.initMode === 'graph' ? this.initOptions : null
},
showLoadingDialog: false
showLoadingDialog: false,
showViewSettings: true
}
},
computed: {
@@ -162,6 +184,7 @@ export default {
mode(newMode, oldMode) {
this.$emit('update')
this.exportToPngEnabled = true
this.exportToClipboardEnabled = true
this.initOptionsByMode[oldMode] = this.getOptionsForSave()
}
},
@@ -238,6 +261,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 })

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

20
tests/testUtils.js Normal file
View File

@@ -0,0 +1,20 @@
export function waitCondition(condition, timeoutMs = 5000) {
return new Promise((resolve, reject) => {
if (condition()) {
resolve()
return
}
const start = new Date().getTime()
const interval = setInterval(() => {
if (condition()) {
clearInterval(interval)
resolve()
} else {
if (new Date().getTime() - start > timeoutMs) {
clearInterval(interval)
reject()
}
}
}, 500)
})
}

View File

@@ -15,7 +15,6 @@ describe('Chart.vue', () => {
})
it('getOptionsForSave called with proper arguments', () => {
// mount the component
const wrapper = mount(Chart, {
global: {
mocks: { $store }
@@ -30,7 +29,6 @@ describe('Chart.vue', () => {
})
it('emits update when plotly updates', async () => {
// mount the component
const wrapper = mount(Chart, {
global: {
mocks: { $store }
@@ -48,7 +46,6 @@ describe('Chart.vue', () => {
points: [80]
}
// mount the component
const wrapper = mount(Chart, {
props: {
dataSources,
@@ -187,7 +184,8 @@ describe('Chart.vue', () => {
const wrapper = mount(Chart, {
attachTo: document.body,
props: {
dataSources
dataSources,
showViewSettings: true
},
global: {
mocks: { $store }
@@ -207,4 +205,41 @@ describe('Chart.vue', () => {
expect(wrapper.find('.Select__menu').text()).to.contain('name' + 'points')
wrapper.unmount()
})
it('hides and shows controls depending on showViewSettings and resizes the plot', async () => {
const wrapper = mount(Chart, {
attachTo: document.body,
props: {
dataSources: null,
showViewSettings: false
},
global: {
mocks: { $store }
}
})
// don't call flushPromises here, otherwize resize observer will be call to often
// which causes ResizeObserver loop completed with undelivered notifications.
await nextTick()
const plot = wrapper.find('.svg-container').wrapperElement
await flushPromises()
const initialPlotWidth = plot.scrollWidth
const initialPlotHeight = plot.scrollHeight
expect(wrapper.find('.plotly_editor .editor_controls').exists()).to.equal(
false
)
await wrapper.setProps({ showViewSettings: true })
await flushPromises()
expect(plot.scrollWidth).not.to.equal(initialPlotWidth)
expect(plot.scrollHeight).to.equal(initialPlotHeight)
expect(wrapper.find('.plotly_editor .editor_controls').exists()).to.equal(
true
)
wrapper.unmount()
})
})

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { mount } from '@vue/test-utils'
import DataView from '@/views/MainView/Workspace/Tabs/Tab/DataView'
import DataView from '@/views/MainView/Workspace/Tabs/Tab/DataView/index.vue'
import sinon from 'sinon'
import { nextTick } from 'vue'
import cIo from '@/lib/utils/clipboardIo'
@@ -15,7 +15,7 @@ describe('DataView.vue', () => {
it('emits update on mode changing', async () => {
const wrapper = mount(DataView, {
global: {
stubs: { chart: true }
mocks: { $store }
}
})
@@ -29,7 +29,10 @@ describe('DataView.vue', () => {
it('method getOptionsForSave calls the same method of the current view component', async () => {
const wrapper = mount(DataView, {
global: {
mocks: { $store }
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
@@ -53,13 +56,28 @@ describe('DataView.vue', () => {
expect(wrapper.vm.getOptionsForSave()).to.eql({
here_are: 'pivot_settings'
})
const graphBtn = wrapper.findComponent({ ref: 'graphBtn' })
await graphBtn.trigger('click')
const graph = wrapper.findComponent({ name: 'graph' }).vm
sinon
.stub(graph, 'getOptionsForSave')
.returns({ here_are: 'graph_settings' })
expect(wrapper.vm.getOptionsForSave()).to.eql({
here_are: 'graph_settings'
})
wrapper.unmount()
})
it('method saveAsSvg calls the same method of the current view component', async () => {
const wrapper = mount(DataView, {
global: {
mocks: { $store }
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
@@ -87,13 +105,22 @@ describe('DataView.vue', () => {
// Export to svg
await svgBtn.trigger('click')
expect(pivot.saveAsSvg.calledOnce).to.equal(true)
// Switch to graph - svg disabled
const graphBtn = wrapper.findComponent({ ref: 'graphBtn' })
await graphBtn.trigger('click')
expect(svgBtn.attributes('disabled')).to.not.equal(undefined)
wrapper.unmount()
})
it('method saveAsHtml calls the same method of the current view component', async () => {
const wrapper = mount(DataView, {
global: {
mocks: { $store }
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
@@ -114,9 +141,76 @@ describe('DataView.vue', () => {
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
sinon.spy(pivot, 'saveAsHtml')
// Export to svg
// Export to html
await htmlBtn.trigger('click')
expect(pivot.saveAsHtml.calledOnce).to.equal(true)
// Switch to graph - htmlBtn disabled
const graphBtn = wrapper.findComponent({ ref: 'graphBtn' })
await graphBtn.trigger('click')
expect(htmlBtn.attributes('disabled')).to.not.equal(undefined)
wrapper.unmount()
})
it('method saveAsPng calls the same method of the current view component', async () => {
const clock = sinon.useFakeTimers()
const wrapper = mount(DataView, {
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
// Find chart and stub the method
const chart = wrapper.findComponent({ name: 'Chart' }).vm
sinon.stub(chart, 'saveAsPng').callsFake(() => {
chart.$emit('loadingImageCompleted')
})
// Export to png
const pngBtn = wrapper.findComponent({ ref: 'pngExportBtn' })
await pngBtn.trigger('click')
await clock.tick(0)
expect(chart.saveAsPng.calledOnce).to.equal(true)
// Switch to pivot
const pivotBtn = wrapper.findComponent({ ref: 'pivotBtn' })
await pivotBtn.trigger('click')
// Find pivot and stub the method
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
sinon.stub(pivot, 'saveAsPng').callsFake(() => {
pivot.$emit('loadingImageCompleted')
})
// Export to png
await pngBtn.trigger('click')
await clock.tick(0)
expect(pivot.saveAsPng.calledOnce).to.equal(true)
// Switch to graph
const graphBtn = wrapper.findComponent({ ref: 'graphBtn' })
await graphBtn.trigger('click')
// Save as png is disabled because there is no data
expect(pngBtn.attributes('disabled')).to.not.equal(undefined)
await wrapper.setProps({ dataSource: { doc: [] } })
// Find graph and stub the method
const graph = wrapper.findComponent({ name: 'graph' }).vm
sinon.stub(graph, 'saveAsPng').callsFake(() => {
graph.$emit('loadingImageCompleted')
})
// Export to png
await pngBtn.trigger('click')
await clock.tick(0)
expect(graph.saveAsPng.calledOnce).to.equal(true)
wrapper.unmount()
})
@@ -276,4 +370,52 @@ describe('DataView.vue', () => {
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false)
wrapper.unmount()
})
it('saves visualisation options and restores them when switch between modes', async () => {
const wrapper = mount(DataView, {
props: {
initMode: 'graph',
initOptions: { test_options: 'graph_options_from_inquiery' }
},
global: {
mocks: { $store },
stubs: {
chart: true,
graph: true,
pivot: true
}
}
})
const getOptionsForSaveStub = sinon.stub(wrapper.vm, 'getOptionsForSave')
expect(
wrapper.findComponent({ name: 'graph' }).props('initOptions')
).to.eql({ test_options: 'graph_options_from_inquiery' })
getOptionsForSaveStub.returns({ test_options: 'latest_graph_options' })
const chartBtn = wrapper.findComponent({ ref: 'chartBtn' })
await chartBtn.trigger('click')
getOptionsForSaveStub.returns({ test_options: 'chart_settings' })
const pivotBtn = wrapper.findComponent({ ref: 'pivotBtn' })
await pivotBtn.trigger('click')
getOptionsForSaveStub.returns({ test_options: 'pivot_settings' })
await chartBtn.trigger('click')
expect(
wrapper.findComponent({ name: 'chart' }).props('initOptions')
).to.eql({ test_options: 'chart_settings' })
await pivotBtn.trigger('click')
expect(
wrapper.findComponent({ name: 'pivot' }).props('initOptions')
).to.eql({ test_options: 'pivot_settings' })
const graphBtn = wrapper.findComponent({ ref: 'graphBtn' })
await graphBtn.trigger('click')
expect(
wrapper.findComponent({ name: 'graph' }).props('initOptions')
).to.eql({ test_options: 'latest_graph_options' })
})
})

View File

@@ -0,0 +1,504 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { mount, flushPromises } from '@vue/test-utils'
import Graph from '@/views/MainView/Workspace/Tabs/Tab/DataView/Graph/index.vue'
function getPixels(canvas) {
const context = canvas.getContext('webgl2')
const width = context.canvas.width
const height = context.canvas.height
// Create arrays to hold the pixel data
const pixels = new Uint8Array(width * height * 4)
// Read pixels from canvas
context.readPixels(
0,
0,
width,
height,
context.RGBA,
context.UNSIGNED_BYTE,
pixels
)
return pixels.join(' ')
}
describe('Graph.vue', () => {
const $store = { state: { isWorkspaceVisible: true } }
afterEach(() => {
sinon.restore()
})
it('shows message when no data', () => {
const wrapper = mount(Graph, {
global: {
stubs: {
GraphEditor: true
}
},
attachTo: document.body
})
expect(wrapper.find('.no-data').isVisible()).to.equal(true)
expect(wrapper.find('.invalid-data').isVisible()).to.equal(false)
wrapper.unmount()
})
it('shows message when data is invalid', () => {
const wrapper = mount(Graph, {
props: {
dataSources: {
column1: ['value1', 'value2']
}
},
global: {
stubs: {
GraphEditor: true
}
},
attachTo: document.body
})
expect(wrapper.find('.no-data').isVisible()).to.equal(false)
expect(wrapper.find('.invalid-data').isVisible()).to.equal(true)
wrapper.unmount()
})
it('emits update when graph editor updates', async () => {
const wrapper = mount(Graph, {
global: {
stubs: {
GraphEditor: true
}
}
})
wrapper.findComponent({ ref: 'graphEditor' }).vm.$emit('update')
expect(wrapper.emitted('update')).to.have.lengthOf(1)
})
it('the graph resizes when the container resizes', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,
props: {
dataSources: {
doc: [
'{"object_type":0,"node_id":"Gryffindor"}',
'{"object_type":0,"node_id":"Hufflepuff"}'
]
},
initOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
}
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const container =
wrapper.find('.graph-container').wrapperElement.parentElement
const canvas = wrapper.find('canvas.sigma-nodes').wrapperElement
const initialContainerWidth = container.scrollWidth
const initialContainerHeight = container.scrollHeight
const initialCanvasWidth = canvas.scrollWidth
const initialCanvasHeight = canvas.scrollHeight
const newContainerWidth = initialContainerWidth * 2 || 1000
const newContainerHeight = initialContainerHeight * 2 || 2000
container.style.width = `${newContainerWidth}px`
container.style.height = `${newContainerHeight}px`
await flushPromises()
expect(canvas.scrollWidth).not.to.equal(initialCanvasWidth)
expect(canvas.scrollHeight).not.to.equal(initialCanvasHeight)
wrapper.unmount()
})
it('nodes and edges are rendered', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,
props: {
showViewSettings: true,
dataSources: {
doc: [
'{"object_type": 0, "node_id": 1}',
'{"object_type": 0, "node_id": 2}',
'{"object_type": 1, "source": 1, "target": 2}'
]
}
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const container =
wrapper.find('.graph-container').wrapperElement.parentElement
container.style.height = '400px'
await wrapper
.find('.test_object_type_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
let options = wrapper.findAll('.Select__menu .Select__option')
await options[0].trigger('click')
const nodeCanvasPixelsBefore = getPixels(
wrapper.find('.test_graph_output canvas.sigma-nodes').wrapperElement
)
const edgeCanvasPixelsBefore = getPixels(
wrapper.find('.test_graph_output canvas.sigma-edges').wrapperElement
)
await wrapper
.find('.test_node_id_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
options = wrapper.findAll('.Select__menu .Select__option')
await options[1].trigger('click')
await wrapper
.find('.test_edge_source_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
options = wrapper.findAll('.Select__menu .Select__option')
await options[2].trigger('click')
await wrapper
.find('.test_edge_target_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
options = wrapper.findAll('.Select__menu .Select__option')
await options[3].trigger('click')
const nodeCanvasPixelsAfter = getPixels(
wrapper.find('.test_graph_output canvas.sigma-nodes').wrapperElement
)
const edgeCanvasPixelsAfter = getPixels(
wrapper.find('.test_graph_output canvas.sigma-edges').wrapperElement
)
expect(nodeCanvasPixelsBefore).not.equal(nodeCanvasPixelsAfter)
expect(edgeCanvasPixelsBefore).not.equal(edgeCanvasPixelsAfter)
wrapper.unmount()
})
it('rerenders when dataSource changes but does not rerender if dataSources is empty', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,
props: {
showViewSettings: true,
dataSources: {
doc: [
'{"object_type": 0, "node_id": 1}',
'{"object_type": 0, "node_id": 2}',
'{"object_type": 1, "source": 1, "target": 2}'
]
}
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const container =
wrapper.find('.graph-container').wrapperElement.parentElement
container.style.height = '400px'
await wrapper
.find('.test_object_type_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
let options = wrapper.findAll('.Select__menu .Select__option')
await options[0].trigger('click')
await wrapper
.find('.test_node_id_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
options = wrapper.findAll('.Select__menu .Select__option')
await options[1].trigger('click')
await wrapper
.find('.test_edge_source_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
options = wrapper.findAll('.Select__menu .Select__option')
await options[2].trigger('click')
await wrapper
.find('.test_edge_target_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
options = wrapper.findAll('.Select__menu .Select__option')
await options[3].trigger('click')
const nodeCanvasPixelsBefore = getPixels(
wrapper.find('.test_graph_output canvas.sigma-nodes').wrapperElement
)
const edgeCanvasPixelsBefore = getPixels(
wrapper.find('.test_graph_output canvas.sigma-edges').wrapperElement
)
await wrapper.setProps({
dataSources: {
doc: [
'{"object_type": 0, "node_id": 1}',
'{"object_type": 0, "node_id": 2}',
'{"object_type": 0, "node_id": 3}',
'{"object_type": 1, "source": 1, "target": 2}',
'{"object_type": 1, "source": 1, "target": 3}'
]
}
})
const nodeCanvasPixelsAfter = getPixels(
wrapper.find('.test_graph_output canvas.sigma-nodes').wrapperElement
)
const edgeCanvasPixelsAfter = getPixels(
wrapper.find('.test_graph_output canvas.sigma-edges').wrapperElement
)
expect(nodeCanvasPixelsBefore).not.equal(nodeCanvasPixelsAfter)
expect(edgeCanvasPixelsBefore).not.equal(edgeCanvasPixelsAfter)
await wrapper.setProps({
dataSources: null
})
const nodeCanvasPixelsAfterEmtyData = getPixels(
wrapper.find('.test_graph_output canvas.sigma-nodes').wrapperElement
)
const edgeCanvasPixelsAfterEmtyData = getPixels(
wrapper.find('.test_graph_output canvas.sigma-edges').wrapperElement
)
expect(nodeCanvasPixelsAfterEmtyData).equal(nodeCanvasPixelsAfter)
expect(edgeCanvasPixelsAfterEmtyData).equal(edgeCanvasPixelsAfter)
wrapper.unmount()
})
it('saveAsPng', async () => {
const wrapper = mount(Graph, {
props: {
dataSources: {
doc: [
'{"object_type":0,"node_id":"Gryffindor"}',
'{"object_type":0,"node_id":"Hufflepuff"}'
]
},
initOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
}
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
sinon.stub(wrapper.vm.$refs.graphEditor, 'saveAsPng')
await wrapper.vm.saveAsPng()
expect(wrapper.emitted().loadingImageCompleted.length).to.equal(1)
})
it('hides and shows controls depending on showViewSettings and resizes the graph', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,
props: {
dataSources: {
doc: [
'{"object_type":0,"node_id":"Gryffindor"}',
'{"object_type":0,"node_id":"Hufflepuff"}'
]
},
initOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
}
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const canvas = wrapper.find('canvas.sigma-nodes').wrapperElement
const initialPlotWidth = canvas.scrollWidth
const initialPlotHeight = canvas.scrollHeight
expect(
wrapper.find('.plotly_editor .editor_controls').isVisible()
).to.equal(false)
await wrapper.setProps({ showViewSettings: true })
await flushPromises()
expect(canvas.scrollWidth).not.to.equal(initialPlotWidth)
expect(canvas.scrollHeight).to.equal(initialPlotHeight)
expect(
wrapper.find('.plotly_editor .editor_controls').isVisible()
).to.equal(true)
wrapper.unmount()
})
})

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { mount, flushPromises } from '@vue/test-utils'
import Pivot from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot'
import Pivot from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/index.vue'
import chartHelper from '@/lib/chartHelper'
import fIo from '@/lib/utils/fileIo'
import $ from 'jquery'
@@ -586,4 +586,48 @@ describe('Pivot.vue', () => {
wrapper.unmount()
})
it('hides or shows controlls depending on showViewSettings and resizes standart chart', async () => {
const wrapper = mount(Pivot, {
global: {
mocks: { $store: { state: { isWorkspaceVisible: true } } }
},
props: {
dataSources: {
item: ['foo', 'bar', 'bar', 'bar'],
year: [2021, 2021, 2020, 2020]
},
initOptions: {
rows: ['item'],
cols: ['year'],
colOrder: 'key_a_to_z',
rowOrder: 'key_a_to_z',
aggregatorName: 'Count',
vals: [],
renderer: $.pivotUtilities.renderers['Bar Chart'],
rendererName: 'Bar Chart'
}
},
attachTo: container
})
expect(wrapper.find('.pivot-ui').isVisible()).to.equal(false)
await flushPromises()
const plot = wrapper.find('.svg-container').wrapperElement
const initialPlotHeight = plot.scrollHeight
await wrapper.setProps({ showViewSettings: true })
expect(wrapper.find('.pivot-ui').isVisible()).to.equal(true)
await flushPromises()
const plotAfterResize = wrapper.find('.svg-container').wrapperElement
expect(plotAfterResize.scrollWidth.scrollHeight).not.to.equal(
initialPlotHeight
)
wrapper.unmount()
})
})