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

Compare commits

..

29 Commits

Author SHA1 Message Date
lana-k
85b5a200e2 fix tinycolor2 bundle 2025-12-27 21:11:01 +01:00
lana-k
a0ef93921f #131 fix label color 2025-12-26 20:56:08 +01:00
lana-k
859cd2ccfc #129 fix icon 2025-12-25 12:29:28 +01:00
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 6543 additions and 22098 deletions

View File

@@ -23,7 +23,10 @@ jobs:
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
sudo add-apt-repository -y ppa:mozillateam/ppa sudo add-apt-repository -y ppa:mozillateam/ppa
sudo apt-get update 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 - name: Update npm
run: npm install -g npm@10 run: npm install -g npm@10
@@ -35,4 +38,4 @@ jobs:
run: npm run lint -- --no-fix run: npm run lint -- --no-fix
- name: Run karma tests - 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: 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 - import a CSV/JSON/NDJSON file into a SQLite database and visualize imported data
- export result set to CSV file - export result set to CSV file
- manage inquiries and run them against different databases - 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 ## 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 [1]: https://github.com/plotly/falcon
[2]: https://github.com/getredash/redash [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 [10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
[11]: https://github.com/plotly/plotly.js [11]: https://github.com/plotly/plotly.js
[12]: https://github.com/nicolaskruchten/pivottable [12]: https://github.com/nicolaskruchten/pivottable
[13]: https://www.sigmajs.org/
[14]: https://graphology.github.io/

23782
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.27.1", "version": "0.28.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -36,6 +36,7 @@
"sigma": "^3.0.1", "sigma": "^3.0.1",
"sql.js": "file:./lib/sql-js", "sql.js": "file:./lib/sql-js",
"tiny-emitter": "^2.1.0", "tiny-emitter": "^2.1.0",
"tinycolor2": "^1.4.2",
"veaury": "^2.5.1", "veaury": "^2.5.1",
"vue": "^3.5.11", "vue": "^3.5.11",
"vue-final-modal": "^4.5.5", "vue-final-modal": "^4.5.5",

View File

@@ -33,48 +33,6 @@ export default {
</script> </script>
<style> <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, #app,
.dialog, .dialog,
input, input,

View File

@@ -4,3 +4,10 @@
font-size: 13px; font-size: 13px;
padding: 0 24px; 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> <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 <RadioBlocks
:options="booleanOptions" :options="booleanOptions"
:activeOption="modelValue.adjustSizes" :activeOption="modelValue.adjustSizes"
@@ -7,7 +17,10 @@
/> />
</Field> </Field>
<Field label="Barnes-Hut optimize"> <Field
label="Barnes-Hut optimize"
fieldContainerClassName="test_fa2_barnes_hut"
>
<RadioBlocks <RadioBlocks
:options="booleanOptions" :options="booleanOptions"
:activeOption="modelValue.barnesHutOptimize" :activeOption="modelValue.barnesHutOptimize"
@@ -15,21 +28,21 @@
/> />
</Field> </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 <NumericInput
:value="modelValue.barnesHutTheta" :value="modelValue.barnesHutTheta"
@update="update('barnesHutTheta', $event)" @update="update('barnesHutTheta', $event)"
/> />
</Field> </Field>
<Field label="Gravity"> <Field
<NumericInput label="Strong gravity mode"
:value="modelValue.gravity" fieldContainerClassName="test_fa2_strong_gravity"
@update="update('gravity', $event)" >
/>
</Field>
<Field label="Strong gravity mode">
<RadioBlocks <RadioBlocks
:options="booleanOptions" :options="booleanOptions"
:activeOption="modelValue.strongGravityMode" :activeOption="modelValue.strongGravityMode"
@@ -37,7 +50,10 @@
/> />
</Field> </Field>
<Field label="Noack's LinLog model"> <Field
label="Noack's LinLog model"
fieldContainerClassName="test_fa2_lin_log"
>
<RadioBlocks <RadioBlocks
:options="booleanOptions" :options="booleanOptions"
:activeOption="modelValue.linLogMode" :activeOption="modelValue.linLogMode"
@@ -45,7 +61,10 @@
/> />
</Field> </Field>
<Field label="Out bound attraction distribution"> <Field
label="Outbound attraction distribution"
fieldContainerClassName="test_fa2_outbound_attraction"
>
<RadioBlocks <RadioBlocks
:options="booleanOptions" :options="booleanOptions"
:activeOption="modelValue.outboundAttractionDistribution" :activeOption="modelValue.outboundAttractionDistribution"
@@ -53,7 +72,7 @@
/> />
</Field> </Field>
<Field label="Slow down"> <Field label="Slow down" fieldContainerClassName="test_fa2_slow_down">
<NumericInput <NumericInput
:value="modelValue.slowDown" :value="modelValue.slowDown"
:min="0" :min="0"
@@ -65,10 +84,15 @@
<Dropdown <Dropdown
:options="keyOptions" :options="keyOptions"
:value="modelValue.weightSource" :value="modelValue.weightSource"
className="test_fa2_weight_source"
@change="update('weightSource', $event)" @change="update('weightSource', $event)"
/> />
</Field> </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 <NumericInput
:value="modelValue.edgeWeightInfluence" :value="modelValue.edgeWeightInfluence"
@update="update('edgeWeightInfluence', $event)" @update="update('edgeWeightInfluence', $event)"

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,41 +1,61 @@
<template> <template>
<div class="plotly_editor"> <div :class="['plotly_editor', { with_controls: showViewSettings }]">
<GraphEditorControls> <GraphEditorControls v-show="showViewSettings">
<PanelMenuWrapper> <PanelMenuWrapper>
<Panel group="Structure" name="Graph"> <Panel group="Structure" name="Graph">
<Fold name="Graph"> <Fold name="Graph">
<Field>Choose keys explanation...</Field> <Field>
<Field label="Object type"> 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 <Dropdown
:options="keysOptions" :options="keysOptions"
:value="settings.structure.objectType" :value="settings.structure.objectType"
className="test_object_type_select"
@change="updateStructure('objectType', $event)" @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>
<Field label="Node Id"> <Field label="Node Id">
<Dropdown <Dropdown
:options="keysOptions" :options="keysOptions"
:value="settings.structure.nodeId" :value="settings.structure.nodeId"
className="test_node_id_select"
@change="updateStructure('nodeId', $event)" @change="updateStructure('nodeId', $event)"
/> />
<Field> A field keeping unique node identifier. </Field>
</Field> </Field>
<Field label="Edge source"> <Field label="Edge source">
<Dropdown <Dropdown
:options="keysOptions" :options="keysOptions"
:value="settings.structure.edgeSource" :value="settings.structure.edgeSource"
className="test_edge_source_select"
@change="updateStructure('edgeSource', $event)" @change="updateStructure('edgeSource', $event)"
/> />
<Field>
A field keeping a node identifier where the edge starts.
</Field>
</Field> </Field>
<Field label="Edge target"> <Field label="Edge target">
<Dropdown <Dropdown
:options="keysOptions" :options="keysOptions"
:value="settings.structure.edgeTarget" :value="settings.structure.edgeTarget"
className="test_edge_target_select"
@change="updateStructure('edgeTarget', $event)" @change="updateStructure('edgeTarget', $event)"
/> />
<Field>
A field keeping a node identifier where the edge ends.
</Field>
</Field> </Field>
</Fold> </Fold>
</Panel> </Panel>
@@ -55,6 +75,7 @@
<Dropdown <Dropdown
:options="keysOptions" :options="keysOptions"
:value="settings.style.nodes.label.source" :value="settings.style.nodes.label.source"
className="test_label_select"
@change="updateNodes('label.source', $event)" @change="updateNodes('label.source', $event)"
/> />
</Field> </Field>
@@ -81,7 +102,10 @@
<Panel group="Style" name="Edges"> <Panel group="Style" name="Edges">
<Fold name="Edges"> <Fold name="Edges">
<Field label="Direction"> <Field
label="Direction"
fieldContainerClassName="test_edge_direction"
>
<RadioBlocks <RadioBlocks
:options="visibilityOptions" :options="visibilityOptions"
:activeOption="settings.style.edges.showDirection" :activeOption="settings.style.edges.showDirection"
@@ -93,6 +117,7 @@
<Dropdown <Dropdown
:options="keysOptions" :options="keysOptions"
:value="settings.style.edges.label.source" :value="settings.style.edges.label.source"
className="test_edge_label_select"
@change="updateEdges('label.source', $event)" @change="updateEdges('label.source', $event)"
/> />
</Field> </Field>
@@ -123,6 +148,8 @@
<Dropdown <Dropdown
:options="layoutOptions" :options="layoutOptions"
:value="settings.layout.type" :value="settings.layout.type"
:clearable="false"
className="test_layout_algorithm_select"
@change="updateLayout($event)" @change="updateLayout($event)"
/> />
</Field> </Field>
@@ -143,10 +170,18 @@
/> />
</Fold> </Fold>
<div class="force-atlas-buttons"> <div class="force-atlas-buttons">
<Button variant="secondary" @click="resetFA2LayoutSettings"> <Button
variant="secondary"
class="test_fa2_reset"
@click="resetFA2LayoutSettings"
>
Reset Reset
</Button> </Button>
<Button variant="primary" @click="toggleFA2Layout"> <Button
variant="primary"
class="test_fa2_toggle"
@click="toggleFA2Layout"
>
<template #node:icon> <template #node:icon>
<div <div
:style="{ :style="{
@@ -164,8 +199,10 @@
</Panel> </Panel>
</PanelMenuWrapper> </PanelMenuWrapper>
</GraphEditorControls> </GraphEditorControls>
<div <div
ref="graph" ref="graph"
class="test_graph_output"
:style="{ :style="{
height: '100%', height: '100%',
width: '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 Field from 'react-chart-editor/lib/components/fields/Field'
import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue' import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue'
import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue' import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue'
// eslint-disable-next-line max-len
import AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue' import AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue'
import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue' import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue'
import FA2Layout from 'graphology-layout-forceatlas2/worker' 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 RunIcon from '@/components/svg/run.vue'
import StopIcon from '@/components/svg/stop.vue' import StopIcon from '@/components/svg/stop.vue'
import { downloadAsPNG, drawOnCanvas } from '@sigma/export-image' 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 NodeSizeSettings from '@/components/Graph/NodeSizeSettings.vue'
import EdgeSizeSettings from '@/components/Graph/EdgeSizeSettings.vue' import EdgeSizeSettings from '@/components/Graph/EdgeSizeSettings.vue'
import EdgeColorSettings from '@/components/Graph/EdgeColorSettings.vue' import EdgeColorSettings from '@/components/Graph/EdgeColorSettings.vue'
import events from '@/lib/utils/events'
export default { export default {
components: { components: {
@@ -235,7 +274,8 @@ export default {
inject: ['tabLayout'], inject: ['tabLayout'],
props: { props: {
dataSources: Object, dataSources: Object,
initOptions: Object initOptions: Object,
showViewSettings: Boolean
}, },
emits: ['update'], emits: ['update'],
data() { data() {
@@ -275,7 +315,7 @@ export default {
nodes: { nodes: {
size: { size: {
type: 'constant', type: 'constant',
value: 4 value: 10
}, },
color: { color: {
type: 'constant', type: 'constant',
@@ -319,11 +359,10 @@ export default {
if (!this.dataSources) { if (!this.dataSources) {
return [] return []
} }
const firstColumnName = Object.keys(this.dataSources)[0]
try { try {
return ( return (
this.dataSources[Object.keys(this.dataSources)[0] || 'doc'].map( this.dataSources[firstColumnName].map(json => JSON.parse(json)) || []
json => JSON.parse(json)
) || []
) )
} catch { } catch {
return [] return []
@@ -359,6 +398,14 @@ export default {
this.buildGraph() this.buildGraph()
} }
}, },
'settings.layout.type': {
immediate: true,
handler() {
events.send('viz_graph.render', null, {
layout: this.settings.layout.type
})
}
},
tabLayout: { tabLayout: {
deep: true, deep: true,
handler() { handler() {
@@ -464,7 +511,7 @@ export default {
if (layoutType === 'random') { if (layoutType === 'random') {
random.assign(this.graph, { random.assign(this.graph, {
rng: seedrandom(this.settings.layout.options.seedValue || 1) rng: seedrandom(this.settings.layout.options.seedValue)
}) })
return return
} }
@@ -497,7 +544,7 @@ export default {
this.settings.layout.options.hierarchyAttributes?.map( this.settings.layout.options.hierarchyAttributes?.map(
(_, index) => 'hierarchyAttribute' + index (_, index) => 'hierarchyAttribute' + index
) || [], ) || [],
rng: seedrandom(this.settings.layout.options.seedValue || 1) rng: seedrandom(this.settings.layout.options.seedValue)
}) })
return return
} }
@@ -547,10 +594,6 @@ export default {
} }
}, },
autoRunFA2Layout() { autoRunFA2Layout() {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
}
let iteration = 1 let iteration = 1
this.checkIteration = () => { this.checkIteration = () => {
if ( if (
@@ -565,7 +608,7 @@ export default {
this.fa2Layout.start() this.fa2Layout.start()
}, },
setRecommendedFA2Settings() { setRecommendedFA2Settings() {
const sensibleSettings = forceAtlas2.inferSettings(this.graph) const sensibleSettings = forceAtlas2.default.inferSettings(this.graph)
this.settings.layout.options = { this.settings.layout.options = {
initialIterationsAmount: 50, initialIterationsAmount: 50,
adjustSizes: false, adjustSizes: false,
@@ -611,7 +654,7 @@ export default {
</script> </script>
<style scoped> <style scoped>
.plotly_editor > div { .plotly_editor.with_controls > div {
display: flex !important; display: flex !important;
} }

View File

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

View File

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

View File

@@ -7,27 +7,33 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <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" fill="#A2B1C6"
/> />
<path <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" fill="#A2B1C6"
/> />
<path <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" fill="#A2B1C6"
/> />
<path <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" fill="#A2B1C6"
/> />
<path <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" fill="#A2B1C6"
/> />
<path <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" fill="#A2B1C6"
/> />
</svg> </svg>

View File

@@ -0,0 +1,129 @@
<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 16.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" xmlns="http://www.w3.org/2000/svg"
> >
<path <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" fill="#A2B1C6"
/> />
</svg> </svg>

View File

@@ -5,8 +5,39 @@ const TYPE_NODE = 0
const TYPE_EDGE = 1 const TYPE_EDGE = 1
const DEFAULT_SCALE = COLOR_PICKER_CONSTANTS.DEFAULT_SCALE 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) { export function buildNodes(graph, dataSources, options) {
const docColumn = Object.keys(dataSources)[0] || 'doc' const docColumn = Object.keys(dataSources)[0]
const { objectType, nodeId } = options.structure const { objectType, nodeId } = options.structure
if (objectType && nodeId) { if (objectType && nodeId) {
@@ -14,16 +45,17 @@ export function buildNodes(graph, dataSources, options) {
.map(json => JSON.parse(json)) .map(json => JSON.parse(json))
.filter(item => item[objectType] === TYPE_NODE) .filter(item => item[objectType] === TYPE_NODE)
nodes.forEach(node => { nodes.forEach(node => {
graph.addNode(node[nodeId], { if (node[nodeId]) {
data: node, graph.addNode(node[nodeId], {
labelColor: options.style.nodes.label.color data: node
}) })
}
}) })
} }
} }
export function buildEdges(graph, dataSources, options) { 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 const { objectType, edgeSource, edgeTarget } = options.structure
if (objectType && edgeSource && edgeTarget) { if (objectType && edgeSource && edgeTarget) {
@@ -36,8 +68,7 @@ export function buildEdges(graph, dataSources, options) {
const target = edge[edgeTarget] const target = edge[edgeTarget]
if (graph.hasNode(source) && graph.hasNode(target)) { if (graph.hasNode(source) && graph.hasNode(target)) {
graph.addEdge(source, target, { graph.addEdge(source, target, {
data: edge, data: edge
labelColor: options.style.edges.label.color
}) })
} }
}) })
@@ -110,10 +141,23 @@ function getUpdateSizeMethod(graph, sizeSettings) {
if (type === 'constant') { if (type === 'constant') {
return attributes => (attributes.size = value) return attributes => (attributes.size = value)
} else if (type === 'variable') { } else if (type === 'variable') {
return getVariabledSizeMethod(mode, source, scale, min) return attributes => {
attributes.size = getVariabledSize(
mode,
attributes.data[source],
scale,
min
)
}
} else { } else {
return (attributes, nodeId) => return (attributes, nodeId) => {
(attributes.size = Math.max(graph[method](nodeId) * scale, min)) 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') { if (mode === 'diameter') {
return attributes => return Math.max((value / 2) * scale, min / 2)
(attributes.size = Math.max(
(attributes.data[source] / 2) * scale,
min / 2
))
} else if (mode === 'area') { } else if (mode === 'area') {
return attributes => return Math.max(Math.sqrt((value / 2) * scale), min / 2)
(attributes.size = Math.max(
Math.sqrt((attributes.data[source] / 2) * scale),
min / 2
))
} else { } else {
return attributes => return Math.max(value * scale, min)
(attributes.size = Math.max(attributes.data[source] * scale, min))
} }
} }
@@ -224,7 +259,6 @@ function getColorMethod(
colorscale[index % colorscale.length] colorscale[index % colorscale.length]
]) ])
) )
return (attributes, nodeId) => { return (attributes, nodeId) => {
const category = sourceGetter(nodeId, attributes) const category = sourceGetter(nodeId, attributes)
attributes.color = colorMap[category] attributes.color = colorMap[category]
@@ -240,6 +274,7 @@ function getColorMethod(
const value = sourceGetter(nodeId, attributes) const value = sourceGetter(nodeId, attributes)
const normalizedValue = (value - min) / (max - min) const normalizedValue = (value - min) / (max - min)
if (isNaN(normalizedValue)) { if (isNaN(normalizedValue)) {
attributes.color = '#000000'
return return
} }
const exactMatch = normalizedColorscale.find( const exactMatch = normalizedColorscale.find(
@@ -251,9 +286,9 @@ function getColorMethod(
} }
const rightColorIndex = normalizedColorscale.findIndex( const rightColorIndex = normalizedColorscale.findIndex(
([value]) => value >= normalizedValue ([value]) => value > normalizedValue
) )
const leftColorIndex = (rightColorIndex || 1) - 1 const leftColorIndex = rightColorIndex - 1
const right = normalizedColorscale[rightColorIndex] const right = normalizedColorscale[rightColorIndex]
const left = normalizedColorscale[leftColorIndex] const left = normalizedColorscale[leftColorIndex]
const interpolationFactor = const interpolationFactor =
@@ -290,18 +325,3 @@ function getEdgeValueScale(graph, sourceGetter) {
}, new Set()) }, new Set())
return Array.from(scaleSet).sort((a, b) => a - b) 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 { createVfm, VueFinalModal, useVfm } from 'vue-final-modal'
import '@/assets/styles/variables.css' import '@/assets/styles/variables.css'
import '@/assets/styles/typography.css'
import '@/assets/styles/buttons.css' import '@/assets/styles/buttons.css'
import '@/assets/styles/tables.css' import '@/assets/styles/tables.css'
import '@/assets/styles/dialogs.css' import '@/assets/styles/dialogs.css'

View File

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

View File

@@ -1,19 +1,30 @@
<template> <template>
<div ref="graphContainer" class="chart-container"> <div ref="graphContainer" class="graph-container">
<div v-show="!dataSources" class="warning chart-warning"> <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 There is no data to build a graph. Run your SQL query and make sure the
result is not empty. result is not empty.
</div> </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 <div
class="graph" class="graph"
:style="{ :style="{
height: !dataSources ? 'calc(100% - 40px)' : '100%' height:
!dataSources || !dataSourceIsValid ? 'calc(100% - 40px)' : '100%'
}" }"
> >
<GraphEditor <GraphEditor
ref="graphEditor" ref="graphEditor"
:dataSources="dataSources" :dataSources="dataSources"
:initOptions="initOptions" :initOptions="initOptions"
:showViewSettings="showViewSettings"
@update="$emit('update')" @update="$emit('update')"
/> />
</div> </div>
@@ -22,8 +33,8 @@
<script> <script>
import 'react-chart-editor/lib/react-chart-editor.css' import 'react-chart-editor/lib/react-chart-editor.css'
import events from '@/lib/utils/events' import GraphEditor from '@/components/Graph/GraphEditor.vue'
import GraphEditor from './GraphEditor.vue' import { dataSourceIsValid } from '@/lib/graphHelper'
export default { export default {
name: 'Graph', name: 'Graph',
@@ -33,11 +44,14 @@ export default {
initOptions: Object, initOptions: Object,
exportToPngEnabled: Boolean, exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean, exportToSvgEnabled: Boolean,
exportToHtmlEnabled: Boolean exportToHtmlEnabled: Boolean,
showViewSettings: Boolean
}, },
emits: [ emits: [
'update:exportToSvgEnabled', 'update:exportToSvgEnabled',
'update:exportToHtmlEnabled', 'update:exportToHtmlEnabled',
'update:exportToPngEnabled',
'update:exportToClipboardEnabled',
'update', 'update',
'loadingImageCompleted' 'loadingImageCompleted'
], ],
@@ -46,10 +60,26 @@ export default {
resizeObserver: null 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() { created() {
this.$emit('update:exportToSvgEnabled', false) this.$emit('update:exportToSvgEnabled', false)
this.$emit('update:exportToHtmlEnabled', false) this.$emit('update:exportToHtmlEnabled', false)
this.$emit('update:exportToPngEnabled', !!this.dataSources)
this.$emit('update:exportToClipboardEnabled', !!this.dataSources)
}, },
mounted() { mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize) this.resizeObserver = new ResizeObserver(this.handleResize)
@@ -66,14 +96,14 @@ export default {
await this.$refs.graphEditor.saveAsPng() await this.$refs.graphEditor.saveAsPng()
this.$emit('loadingImageCompleted') this.$emit('loadingImageCompleted')
}, },
async prepareCopy() { prepareCopy() {
return await this.$refs.graphEditor.prepareCopy() return this.$refs.graphEditor.prepareCopy()
}, },
async handleResize() { async handleResize() {
const renderer = this.$refs.graphEditor.renderer const renderer = this.$refs.graphEditor.renderer
if (renderer) { if (renderer) {
renderer.refresh() renderer.refresh()
renderer.getCamera().setState({ x: 0.5, y: 0.5 }) renderer.getCamera().animatedReset({ duration: 600 })
} }
} }
} }
@@ -81,18 +111,11 @@ export default {
</script> </script>
<style scoped> <style scoped>
.chart-container { .graph-container {
height: 100%; height: 100%;
} }
.chart-warning { .graph {
height: 40px;
line-height: 40px;
border-bottom: 1px solid var(--color-border);
box-sizing: border-box;
}
.chart {
min-height: 242px; min-height: 242px;
} }

View File

@@ -1,137 +1,132 @@
<template> <template>
<div class="pivot-ui"> <div class="pivot-ui">
<div :class="{ collapsed }"> <div class="row">
<div class="row"> <label>Columns</label>
<label>Columns</label> <multiselect
<multiselect v-model="cols"
v-model="cols" class="sqliteviz-select cols"
class="sqliteviz-select cols" :options="colsToSelect"
:options="colsToSelect" :disabled="colsToSelect.length === 0"
:disabled="colsToSelect.length === 0" :multiple="true"
:multiple="true" :hideSelected="true"
:hideSelected="true" :closeOnSelect="true"
:closeOnSelect="true" :showLabels="false"
:showLabels="false" :max="colsToSelect.length"
:max="colsToSelect.length" openDirection="bottom"
openDirection="bottom" placeholder=""
placeholder="" >
> <template #maxElements>
<template #maxElements> <span class="no-results">No Results</span>
<span class="no-results">No Results</span> </template>
</template>
<template #placeholder>Choose columns</template> <template #placeholder>Choose columns</template>
<template #noResult> <template #noResult>
<span class="no-results">No Results</span> <span class="no-results">No Results</span>
</template> </template>
</multiselect> </multiselect>
<pivot-sort-btn v-model="colOrder" class="sort-btn" direction="col" /> <pivot-sort-btn v-model="colOrder" class="sort-btn" direction="col" />
</div> </div>
<div class="row"> <div class="row">
<label>Rows</label> <label>Rows</label>
<multiselect <multiselect
v-model="rows" v-model="rows"
class="sqliteviz-select rows" class="sqliteviz-select rows"
:options="rowsToSelect" :options="rowsToSelect"
:disabled="rowsToSelect.length === 0" :disabled="rowsToSelect.length === 0"
:multiple="true" :multiple="true"
:hideSelected="true" :hideSelected="true"
:closeOnSelect="true" :closeOnSelect="true"
:showLabels="false" :showLabels="false"
:max="rowsToSelect.length" :max="rowsToSelect.length"
:optionHeight="29" :optionHeight="29"
openDirection="bottom" openDirection="bottom"
placeholder="" placeholder=""
> >
<template #maxElements> <template #maxElements>
<span class="no-results">No Results</span> <span class="no-results">No Results</span>
</template> </template>
<template #placeholder>Choose rows</template> <template #placeholder>Choose rows</template>
<template #noResult> <template #noResult>
<span class="no-results">No Results</span> <span class="no-results">No Results</span>
</template> </template>
</multiselect> </multiselect>
<pivot-sort-btn v-model="rowOrder" class="sort-btn" direction="row" /> <pivot-sort-btn v-model="rowOrder" class="sort-btn" direction="row" />
</div> </div>
<div class="row aggregator"> <div class="row aggregator">
<label>Aggregator</label> <label>Aggregator</label>
<multiselect <multiselect
v-model="aggregator" v-model="aggregator"
class="sqliteviz-select short aggregator" class="sqliteviz-select short aggregator"
:options="aggregators" :options="aggregators"
label="name" label="name"
trackBy="name" trackBy="name"
:closeOnSelect="true" :closeOnSelect="true"
:showLabels="false" :showLabels="false"
:hideSelected="true" :hideSelected="true"
:optionHeight="29" :optionHeight="29"
openDirection="bottom" openDirection="bottom"
placeholder="Choose a function" placeholder="Choose a function"
> >
<template #noResult> <template #noResult>
<span class="no-results">No Results</span> <span class="no-results">No Results</span>
</template> </template>
</multiselect> </multiselect>
<multiselect <multiselect
v-show="valCount > 0" v-show="valCount > 0"
v-model="val1" v-model="val1"
class="sqliteviz-select aggr-arg" class="sqliteviz-select aggr-arg"
:options="keyNames" :options="keyNames"
:disabled="keyNames.length === 0" :disabled="keyNames.length === 0"
:closeOnSelect="true" :closeOnSelect="true"
:showLabels="false" :showLabels="false"
:hideSelected="true" :hideSelected="true"
:optionHeight="29" :optionHeight="29"
openDirection="bottom" openDirection="bottom"
placeholder="Choose an argument" placeholder="Choose an argument"
/> />
<multiselect <multiselect
v-show="valCount > 1" v-show="valCount > 1"
v-model="val2" v-model="val2"
class="sqliteviz-select aggr-arg" class="sqliteviz-select aggr-arg"
:options="keyNames" :options="keyNames"
:disabled="keyNames.length === 0" :disabled="keyNames.length === 0"
:closeOnSelect="true" :closeOnSelect="true"
:showLabels="false" :showLabels="false"
:hideSelected="true" :hideSelected="true"
:optionHeight="29" :optionHeight="29"
openDirection="bottom" openDirection="bottom"
placeholder="Choose a second argument" placeholder="Choose a second argument"
/> />
</div> </div>
<div class="row"> <div class="row">
<label>View</label> <label>View</label>
<multiselect <multiselect
v-model="renderer" v-model="renderer"
class="sqliteviz-select short renderer" class="sqliteviz-select short renderer"
:options="renderers" :options="renderers"
label="name" label="name"
trackBy="name" trackBy="name"
:closeOnSelect="true" :closeOnSelect="true"
:allowEmpty="false" :allowEmpty="false"
:showLabels="false" :showLabels="false"
:hideSelected="true" :hideSelected="true"
:optionHeight="29" :optionHeight="29"
openDirection="bottom" openDirection="bottom"
placeholder="Choose a view" placeholder="Choose a view"
> >
<template #noResult> <template #noResult>
<span class="no-results">No Results</span> <span class="no-results">No Results</span>
</template> </template>
</multiselect> </multiselect>
</div>
</div> </div>
<span class="switcher" @click="collapsed = !collapsed">
{{ collapsed ? 'Show pivot settings' : 'Hide pivot settings' }}
</span>
</div> </div>
</template> </template>
@@ -163,7 +158,6 @@ export default {
const rendererName = const rendererName =
(this.modelValue && this.modelValue.rendererName) || 'Table' (this.modelValue && this.modelValue.rendererName) || 'Table'
return { return {
collapsed: false,
renderer: { renderer: {
name: rendererName, name: rendererName,
fun: $.pivotUtilities.renderers[rendererName] fun: $.pivotUtilities.renderers[rendererName]
@@ -291,9 +285,6 @@ export default {
margin-left: 12px; margin-left: 12px;
flex-shrink: 0; flex-shrink: 0;
} }
.collapsed {
display: none;
}
.switcher { .switcher {
display: block; display: block;

View File

@@ -5,6 +5,7 @@
result is not empty. result is not empty.
</div> </div>
<pivot-ui <pivot-ui
v-show="showViewSettings"
v-model="pivotOptions" v-model="pivotOptions"
:keyNames="columns" :keyNames="columns"
@update="$emit('update')" @update="$emit('update')"
@@ -30,7 +31,7 @@ import fIo from '@/lib/utils/fileIo'
import $ from 'jquery' import $ from 'jquery'
import 'pivottable' import 'pivottable'
import 'pivottable/dist/pivot.css' import 'pivottable/dist/pivot.css'
import PivotUi from './PivotUi' import PivotUi from './PivotUi/index.vue'
import pivotHelper from './pivotHelper' import pivotHelper from './pivotHelper'
import Chart from '@/views/MainView/Workspace/Tabs/Tab/DataView/Chart' import Chart from '@/views/MainView/Workspace/Tabs/Tab/DataView/Chart'
import chartHelper from '@/lib/chartHelper' import chartHelper from '@/lib/chartHelper'
@@ -47,7 +48,8 @@ export default {
dataSources: Object, dataSources: Object,
initOptions: Object, initOptions: Object,
exportToPngEnabled: Boolean, exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean exportToSvgEnabled: Boolean,
showViewSettings: Boolean
}, },
emits: [ emits: [
'loadingImageCompleted', 'loadingImageCompleted',
@@ -125,6 +127,9 @@ export default {
}, },
pivotOptions() { pivotOptions() {
this.show() this.show()
},
showViewSettings() {
this.handleResize()
} }
}, },
created() { created() {
@@ -222,12 +227,12 @@ export default {
async prepareCopy() { async prepareCopy() {
if (this.viewCustomChart) { if (this.viewCustomChart) {
return await this.$refs.customChart.prepareCopy() return this.$refs.customChart.prepareCopy()
} }
if (this.viewStandartChart) { 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() { async saveAsSvg() {
@@ -321,4 +326,8 @@ export default {
.pivot-output:empty { .pivot-output:empty {
flex-grow: 0; flex-grow: 0;
} }
:deep(.js-plotly-plot) {
height: 100%;
}
</style> </style>

View File

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

View File

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

View File

@@ -42,8 +42,8 @@ export default {
) { ) {
const stmt = [ const stmt = [
'/*', '/*',
' * Your database is empty. In order to start building charts', ' * Your database is empty. In order to start building data visualisations',
' * you should create a table and insert data into it.', ' * you should create tables and insert data into them.',
' */', ' */',
'CREATE TABLE house', 'CREATE TABLE house',
'(', '(',
@@ -54,7 +54,20 @@ export default {
"('Gryffindor', 100),", "('Gryffindor', 100),",
"('Hufflepuff', 90),", "('Hufflepuff', 90),",
"('Ravenclaw', 95),", "('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') ].join('\n')
const tabId = await this.$store.dispatch('addTab', { query: stmt }) 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', () => { it('getOptionsForSave called with proper arguments', () => {
// mount the component
const wrapper = mount(Chart, { const wrapper = mount(Chart, {
global: { global: {
mocks: { $store } mocks: { $store }
@@ -30,7 +29,6 @@ describe('Chart.vue', () => {
}) })
it('emits update when plotly updates', async () => { it('emits update when plotly updates', async () => {
// mount the component
const wrapper = mount(Chart, { const wrapper = mount(Chart, {
global: { global: {
mocks: { $store } mocks: { $store }
@@ -48,7 +46,6 @@ describe('Chart.vue', () => {
points: [80] points: [80]
} }
// mount the component
const wrapper = mount(Chart, { const wrapper = mount(Chart, {
props: { props: {
dataSources, dataSources,
@@ -187,7 +184,8 @@ describe('Chart.vue', () => {
const wrapper = mount(Chart, { const wrapper = mount(Chart, {
attachTo: document.body, attachTo: document.body,
props: { props: {
dataSources dataSources,
showViewSettings: true
}, },
global: { global: {
mocks: { $store } mocks: { $store }
@@ -207,4 +205,41 @@ describe('Chart.vue', () => {
expect(wrapper.find('.Select__menu').text()).to.contain('name' + 'points') expect(wrapper.find('.Select__menu').text()).to.contain('name' + 'points')
wrapper.unmount() 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 { expect } from 'chai'
import { mount } from '@vue/test-utils' 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 sinon from 'sinon'
import { nextTick } from 'vue' import { nextTick } from 'vue'
import cIo from '@/lib/utils/clipboardIo' import cIo from '@/lib/utils/clipboardIo'
@@ -15,7 +15,7 @@ describe('DataView.vue', () => {
it('emits update on mode changing', async () => { it('emits update on mode changing', async () => {
const wrapper = mount(DataView, { const wrapper = mount(DataView, {
global: { 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 () => { it('method getOptionsForSave calls the same method of the current view component', async () => {
const wrapper = mount(DataView, { const wrapper = mount(DataView, {
global: { global: {
mocks: { $store } mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
} }
}) })
@@ -53,13 +56,28 @@ describe('DataView.vue', () => {
expect(wrapper.vm.getOptionsForSave()).to.eql({ expect(wrapper.vm.getOptionsForSave()).to.eql({
here_are: 'pivot_settings' 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() wrapper.unmount()
}) })
it('method saveAsSvg calls the same method of the current view component', async () => { it('method saveAsSvg calls the same method of the current view component', async () => {
const wrapper = mount(DataView, { const wrapper = mount(DataView, {
global: { global: {
mocks: { $store } mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
} }
}) })
@@ -87,13 +105,22 @@ describe('DataView.vue', () => {
// Export to svg // Export to svg
await svgBtn.trigger('click') await svgBtn.trigger('click')
expect(pivot.saveAsSvg.calledOnce).to.equal(true) 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() wrapper.unmount()
}) })
it('method saveAsHtml calls the same method of the current view component', async () => { it('method saveAsHtml calls the same method of the current view component', async () => {
const wrapper = mount(DataView, { const wrapper = mount(DataView, {
global: { global: {
mocks: { $store } mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
} }
}) })
@@ -114,9 +141,76 @@ describe('DataView.vue', () => {
const pivot = wrapper.findComponent({ name: 'pivot' }).vm const pivot = wrapper.findComponent({ name: 'pivot' }).vm
sinon.spy(pivot, 'saveAsHtml') sinon.spy(pivot, 'saveAsHtml')
// Export to svg // Export to html
await htmlBtn.trigger('click') await htmlBtn.trigger('click')
expect(pivot.saveAsHtml.calledOnce).to.equal(true) 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() wrapper.unmount()
}) })
@@ -276,4 +370,52 @@ describe('DataView.vue', () => {
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false) expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false)
wrapper.unmount() 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 { expect } from 'chai'
import { mount, flushPromises } from '@vue/test-utils' 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 chartHelper from '@/lib/chartHelper'
import fIo from '@/lib/utils/fileIo' import fIo from '@/lib/utils/fileIo'
import $ from 'jquery' import $ from 'jquery'
@@ -586,4 +586,48 @@ describe('Pivot.vue', () => {
wrapper.unmount() 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()
})
}) })