1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2026-08-05 23:39:17 +08:00

change repo structure

This commit is contained in:
lana-k
2026-01-15 21:53:12 +01:00
parent 85b5a200e2
commit 7edc196a02
64 changed files with 74 additions and 74 deletions

View File

@@ -0,0 +1,75 @@
<template>
<div :class="['pivot-sort-btn', direction]" @click="changeSorting">
{{ modelValue.includes('key') ? 'key' : 'value' }}
<sort-icon
class="sort-icon"
:horizontal="direction === 'col'"
:asc="modelValue.includes('a_to_z')"
/>
</div>
</template>
<script>
import SortIcon from '@/components/svg/sort'
export default {
name: 'PivotSortBtn',
components: {
SortIcon
},
props: {
direction: String,
modelValue: String
},
emits: ['update:modelValue'],
methods: {
changeSorting() {
if (this.modelValue === 'key_a_to_z') {
this.$emit('update:modelValue', 'value_a_to_z')
} else if (this.modelValue === 'value_a_to_z') {
this.$emit('update:modelValue', 'value_z_to_a')
} else {
this.$emit('update:modelValue', 'key_a_to_z')
}
}
}
}
</script>
<style scoped>
.pivot-sort-btn {
display: flex;
justify-content: center;
align-items: center;
width: 43px;
height: 27px;
background-color: var(--color-bg-light-4);
border-radius: var(--border-radius-medium-2);
border: 1px solid var(--color-border);
cursor: pointer;
font-size: 11px;
color: var(--color-text-base);
line-height: 8px;
box-sizing: border-box;
}
.pivot-sort-btn:hover {
color: var(--color-text-active);
border-color: var(--color-border-dark);
}
.pivot-sort-btn:hover :deep(.sort-icon path) {
fill: var(--color-text-active);
}
.pivot-sort-btn.col {
flex-direction: column;
padding-top: 5px;
}
.pivot-sort-btn.row {
flex-direction: row;
}
.pivot-sort-btn.row .sort-icon {
margin-left: 2px;
}
</style>

View File

@@ -0,0 +1,300 @@
<template>
<div class="pivot-ui">
<div class="row">
<label>Columns</label>
<multiselect
v-model="cols"
class="sqliteviz-select cols"
:options="colsToSelect"
:disabled="colsToSelect.length === 0"
:multiple="true"
:hideSelected="true"
:closeOnSelect="true"
:showLabels="false"
:max="colsToSelect.length"
openDirection="bottom"
placeholder=""
>
<template #maxElements>
<span class="no-results">No Results</span>
</template>
<template #placeholder>Choose columns</template>
<template #noResult>
<span class="no-results">No Results</span>
</template>
</multiselect>
<pivot-sort-btn v-model="colOrder" class="sort-btn" direction="col" />
</div>
<div class="row">
<label>Rows</label>
<multiselect
v-model="rows"
class="sqliteviz-select rows"
:options="rowsToSelect"
:disabled="rowsToSelect.length === 0"
:multiple="true"
:hideSelected="true"
:closeOnSelect="true"
:showLabels="false"
:max="rowsToSelect.length"
:optionHeight="29"
openDirection="bottom"
placeholder=""
>
<template #maxElements>
<span class="no-results">No Results</span>
</template>
<template #placeholder>Choose rows</template>
<template #noResult>
<span class="no-results">No Results</span>
</template>
</multiselect>
<pivot-sort-btn v-model="rowOrder" class="sort-btn" direction="row" />
</div>
<div class="row aggregator">
<label>Aggregator</label>
<multiselect
v-model="aggregator"
class="sqliteviz-select short aggregator"
:options="aggregators"
label="name"
trackBy="name"
:closeOnSelect="true"
:showLabels="false"
:hideSelected="true"
:optionHeight="29"
openDirection="bottom"
placeholder="Choose a function"
>
<template #noResult>
<span class="no-results">No Results</span>
</template>
</multiselect>
<multiselect
v-show="valCount > 0"
v-model="val1"
class="sqliteviz-select aggr-arg"
:options="keyNames"
:disabled="keyNames.length === 0"
:closeOnSelect="true"
:showLabels="false"
:hideSelected="true"
:optionHeight="29"
openDirection="bottom"
placeholder="Choose an argument"
/>
<multiselect
v-show="valCount > 1"
v-model="val2"
class="sqliteviz-select aggr-arg"
:options="keyNames"
:disabled="keyNames.length === 0"
:closeOnSelect="true"
:showLabels="false"
:hideSelected="true"
:optionHeight="29"
openDirection="bottom"
placeholder="Choose a second argument"
/>
</div>
<div class="row">
<label>View</label>
<multiselect
v-model="renderer"
class="sqliteviz-select short renderer"
:options="renderers"
label="name"
trackBy="name"
:closeOnSelect="true"
:allowEmpty="false"
:showLabels="false"
:hideSelected="true"
:optionHeight="29"
openDirection="bottom"
placeholder="Choose a view"
>
<template #noResult>
<span class="no-results">No Results</span>
</template>
</multiselect>
</div>
</div>
</template>
<script>
import $ from 'jquery'
import Multiselect from 'vue-multiselect'
import PivotSortBtn from './PivotSortBtn'
import {
renderers,
aggregators,
zeroValAggregators,
twoValAggregators
} from '../pivotHelper'
export default {
name: 'PivotUi',
components: {
Multiselect,
PivotSortBtn
},
props: {
keyNames: Array,
modelValue: Object
},
emits: ['update:modelValue', 'update'],
data() {
const aggregatorName =
(this.modelValue && this.modelValue.aggregatorName) || 'Count'
const rendererName =
(this.modelValue && this.modelValue.rendererName) || 'Table'
return {
renderer: {
name: rendererName,
fun: $.pivotUtilities.renderers[rendererName]
},
aggregator: {
name: aggregatorName,
fun: $.pivotUtilities.aggregators[aggregatorName]
},
rows: (this.modelValue && this.modelValue.rows) || [],
cols: (this.modelValue && this.modelValue.cols) || [],
val1:
(this.modelValue && this.modelValue.vals && this.modelValue.vals[0]) ||
'',
val2:
(this.modelValue && this.modelValue.vals && this.modelValue.vals[1]) ||
'',
colOrder: (this.modelValue && this.modelValue.colOrder) || 'key_a_to_z',
rowOrder: (this.modelValue && this.modelValue.rowOrder) || 'key_a_to_z'
}
},
computed: {
valCount() {
if (zeroValAggregators.includes(this.aggregator.name)) {
return 0
}
if (twoValAggregators.includes(this.aggregator.name)) {
return 2
}
return 1
},
renderers() {
return renderers
},
aggregators() {
return aggregators
},
rowsToSelect() {
return this.keyNames.filter(key => !this.cols.includes(key))
},
colsToSelect() {
return this.keyNames.filter(key => !this.rows.includes(key))
}
},
watch: {
renderer() {
this.returnValue()
},
aggregator() {
this.returnValue()
},
rows() {
this.returnValue()
},
cols() {
this.returnValue()
},
val1() {
this.returnValue()
},
val2() {
this.returnValue()
},
colOrder() {
this.returnValue()
},
rowOrder() {
this.returnValue()
}
},
methods: {
returnValue() {
const vals = []
for (let i = 1; i <= this.valCount; i++) {
vals.push(this[`val${i}`])
}
this.$emit('update')
this.$emit('update:modelValue', {
rows: this.rows,
cols: this.cols,
colOrder: this.colOrder,
rowOrder: this.rowOrder,
aggregator: this.aggregator.fun(vals),
aggregatorName: this.aggregator.name,
renderer: this.renderer.fun,
rendererName: this.renderer.name,
vals
})
}
}
}
</script>
<style scoped>
.pivot-ui {
padding: 12px 24px;
color: var(--color-text-base);
font-size: 12px;
border-bottom: 1px solid var(--color-border-light);
background-color: var(--color-bg-light);
}
.pivot-ui .row {
display: flex;
align-items: center;
margin: 12px 0;
}
.pivot-ui .row label {
width: 76px;
flex-shrink: 0;
}
.pivot-ui .row .sqliteviz-select.short {
width: 220px;
flex-shrink: 0;
}
.pivot-ui .row .aggr-arg {
margin-left: 12px;
max-width: 220px;
}
.pivot-ui .row .sort-btn {
margin-left: 12px;
flex-shrink: 0;
}
.switcher {
display: block;
width: min-content;
white-space: nowrap;
margin: auto;
cursor: pointer;
}
.switcher:hover {
color: var(--color-accent);
}
</style>

View File

@@ -0,0 +1,333 @@
<template>
<div class="pivot-container">
<div v-show="!dataSources" class="warning pivot-warning">
There is no data to build a pivot. Run your SQL query and make sure the
result is not empty.
</div>
<pivot-ui
v-show="showViewSettings"
v-model="pivotOptions"
:keyNames="columns"
@update="$emit('update')"
/>
<div ref="pivotOutput" class="pivot-output" />
<div
v-show="viewCustomChart"
ref="customChartOutput"
class="custom-chart-output"
>
<chart
ref="customChart"
v-bind="customChartComponentProps"
@update="$emit('update')"
@loading-image-completed="$emit('loadingImageCompleted')"
/>
</div>
</div>
</template>
<script>
import fIo from '@/lib/utils/fileIo'
import $ from 'jquery'
import 'pivottable'
import 'pivottable/dist/pivot.css'
import PivotUi from './PivotUi/index.vue'
import pivotHelper from './pivotHelper'
import Chart from '@/components/Chart'
import chartHelper from '@/lib/chartHelper'
import events from '@/lib/utils/events'
import plotly from 'plotly.js'
export default {
name: 'Pivot',
components: {
PivotUi,
Chart
},
props: {
dataSources: Object,
initOptions: Object,
exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean,
showViewSettings: Boolean
},
emits: [
'loadingImageCompleted',
'update',
'update:exportToSvgEnabled',
'update:exportToPngEnabled',
'update:exportToHtmlEnabled'
],
data() {
return {
resizeObserver: null,
pivotOptions: !this.initOptions
? {
rows: [],
cols: [],
colOrder: 'key_a_to_z',
rowOrder: 'key_a_to_z',
aggregatorName: 'Count',
aggregator: $.pivotUtilities.aggregators.Count(),
vals: [],
rendererName: 'Table',
renderer: $.pivotUtilities.renderers.Table
}
: {
rows: this.initOptions.rows,
cols: this.initOptions.cols,
colOrder: this.initOptions.colOrder,
rowOrder: this.initOptions.rowOrder,
aggregatorName: this.initOptions.aggregatorName,
aggregator: $.pivotUtilities.aggregators[
this.initOptions.aggregatorName
](this.initOptions.vals),
vals: this.initOptions.vals,
rendererName: this.initOptions.rendererName,
renderer: $.pivotUtilities.renderers[this.initOptions.rendererName]
},
customChartComponentProps: {
initOptions: this.initOptions?.rendererOptions?.customChartOptions,
forPivot: true
}
}
},
computed: {
columns() {
return Object.keys(this.dataSources || {})
},
viewStandartChart() {
return this.pivotOptions.rendererName in $.pivotUtilities.plotly_renderers
},
viewCustomChart() {
return this.pivotOptions.rendererName === 'Custom chart'
}
},
watch: {
dataSources() {
this.show()
},
'pivotOptions.rendererName': {
immediate: true,
handler() {
this.$emit(
'update:exportToPngEnabled',
this.pivotOptions.rendererName !== 'TSV Export'
)
this.$emit(
'update:exportToSvgEnabled',
this.viewStandartChart || this.viewCustomChart
)
events.send('viz_pivot.render', null, {
type: this.pivotOptions.rendererName
})
}
},
pivotOptions() {
this.show()
},
showViewSettings() {
this.handleResize()
}
},
created() {
this.$emit('update:exportToHtmlEnabled', true)
},
mounted() {
this.show()
// We need to detect resizing because plotly doesn't resize when resize its container
// but it resize on window.resize (we will trigger it manualy in order to make plotly resize)
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.pivotOutput)
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.pivotOutput)
},
methods: {
handleResize() {
// hack: plotly changes size only on window.resize event,
// so, we resize it manually when container resizes (e.g. when move splitter)
if (this.viewStandartChart) {
plotly.Plots.resize(
this.$refs.pivotOutput.querySelector('.js-plotly-plot')
)
}
},
show() {
const options = { ...this.pivotOptions }
if (this.viewStandartChart) {
options.rendererOptions = {
plotly: {
autosize: true,
width: null,
height: null
},
plotlyConfig: {
displaylogo: false,
responsive: true,
modeBarButtonsToRemove: ['toImage']
}
}
}
if (this.viewCustomChart) {
options.rendererOptions = {
getCustomComponentsProps: () => this.customChartComponentProps
}
}
$(this.$refs.pivotOutput).pivot(
function (callback) {
const rowCount = !this.dataSources
? 0
: this.dataSources[this.columns[0]].length
for (let i = 1; i <= rowCount; i++) {
const row = {}
this.columns.forEach(col => {
row[col] = this.dataSources[col][i - 1]
})
callback(row)
}
}.bind(this),
options
)
// fix for Firefox: fit plotly renderers just after choosing it in pivotUi
this.handleResize()
},
getOptionsForSave() {
const options = { ...this.pivotOptions }
if (this.viewCustomChart) {
const chartComponent = this.$refs.customChart
options.rendererOptions = {
customChartOptions: chartComponent.getOptionsForSave()
}
}
return options
},
async saveAsPng() {
if (this.viewCustomChart) {
this.$refs.customChart.saveAsPng()
} else {
const source = this.viewStandartChart
? await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
: (
await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
).toDataURL('image/png')
this.$emit('loadingImageCompleted')
fIo.downloadFromUrl(source, 'pivot')
}
},
async prepareCopy() {
if (this.viewCustomChart) {
return this.$refs.customChart.prepareCopy()
}
if (this.viewStandartChart) {
return chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
}
return pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
},
async saveAsSvg() {
if (this.viewCustomChart) {
this.$refs.customChart.saveAsSvg()
} else if (this.viewStandartChart) {
const url = await chartHelper.getImageDataUrl(
this.$refs.pivotOutput,
'svg'
)
fIo.downloadFromUrl(url, 'pivot')
}
},
saveAsHtml() {
if (this.viewCustomChart) {
this.$refs.customChart.saveAsHtml()
return
}
if (this.viewStandartChart) {
const chartState = chartHelper.getChartData(this.$refs.pivotOutput)
fIo.exportToFile(
chartHelper.getHtml(chartState),
'chart.html',
'text/html'
)
return
}
fIo.exportToFile(
pivotHelper.getPivotHtml(this.$refs.pivotOutput),
'pivot.html',
'text/html'
)
}
}
}
</script>
<style scoped>
.pivot-container {
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--color-white);
}
.pivot-output,
.custom-chart-output {
flex-grow: 1;
width: 100%;
overflow: auto;
}
.pivot-warning {
height: 40px;
line-height: 40px;
box-sizing: border-box;
}
:deep(.pvtTable) {
min-width: 100%;
}
:deep(table.pvtTable tbody tr td),
:deep(table.pvtTable thead tr th),
:deep(table.pvtTable tbody tr th) {
border-color: var(--color-border-light);
}
:deep(table.pvtTable thead tr th),
:deep(table.pvtTable tbody tr th) {
background-color: var(--color-bg-dark);
color: var(--color-text-light);
}
:deep(table.pvtTable tbody tr td) {
color: var(--color-text-base);
}
.pivot-output :deep(textarea) {
color: var(--color-text-base);
min-width: 100%;
height: 100% !important;
display: block;
box-sizing: border-box;
border-width: 0;
}
.pivot-output :deep(textarea:focus-visible) {
outline: none;
}
.pivot-output:empty {
flex-grow: 0;
}
:deep(.js-plotly-plot) {
height: 100%;
}
</style>

View File

@@ -0,0 +1,121 @@
import $ from 'jquery'
import 'pivottable'
import 'pivottable/dist/export_renderers.js'
import 'pivottable/dist/plotly_renderers.js'
import html2canvas from 'html2canvas'
export const zeroValAggregators = [
'Count',
'Count as Fraction of Total',
'Count as Fraction of Rows',
'Count as Fraction of Columns'
]
export const twoValAggregators = [
'Sum over Sum',
'80% Upper Bound',
'80% Lower Bound'
]
export function _getDataSources(pivotData) {
const rowKeys = pivotData.getRowKeys()
const colKeys = pivotData.getColKeys()
const dataSources = {
'Column keys': colKeys.map(colKey => colKey.join('-')),
'Row keys': rowKeys.map(rowKey => rowKey.join('-'))
}
const dataSourcesByRows = {}
const dataSourcesByCols = {}
const rowAttrs = pivotData.rowAttrs.join('-')
const colAttrs = pivotData.colAttrs.join('-')
colKeys.forEach(colKey => {
const sourceColKey = colAttrs + ':' + colKey.join('-')
dataSourcesByCols[sourceColKey] = []
rowKeys.forEach(rowKey => {
const value = pivotData.getAggregator(rowKey, colKey).value()
dataSourcesByCols[sourceColKey].push(value)
const sourceRowKey = rowAttrs + ':' + rowKey.join('-')
if (!dataSourcesByRows[sourceRowKey]) {
dataSourcesByRows[sourceRowKey] = []
}
dataSourcesByRows[sourceRowKey].push(value)
})
})
return Object.assign(dataSources, dataSourcesByCols, dataSourcesByRows)
}
function customChartRenderer(data, options) {
const propsRef = options.getCustomComponentsProps()
propsRef.dataSources = _getDataSources(data)
return null
}
$.extend(
$.pivotUtilities.renderers,
$.pivotUtilities.export_renderers,
$.pivotUtilities.plotly_renderers,
{ 'Custom chart': customChartRenderer }
)
export const renderers = Object.keys($.pivotUtilities.renderers).map(key => {
return {
name: key,
fun: $.pivotUtilities.renderers[key]
}
})
export const aggregators = Object.keys($.pivotUtilities.aggregators).map(
key => {
return {
name: key,
fun: $.pivotUtilities.aggregators[key]
}
}
)
export async function getPivotCanvas(pivotOutput) {
const tableElement = pivotOutput.querySelector('.pvtTable')
return await html2canvas(tableElement, { logging: false })
}
export function getPivotHtml(pivotOutput) {
return `
<style>
table.pvtTable {
font-family: Arial, sans-serif;
font-size: 12px;
text-align: left;
border-collapse: collapse;
min-width: 100%;
}
table.pvtTable .pvtColLabel {
text-align: center;
}
table.pvtTable .pvtTotalLabel {
text-align: right;
}
table.pvtTable tbody tr td {
color: #506784;
border: 1px solid #DFE8F3;
text-align: right;
}
table.pvtTable thead tr th,
table.pvtTable tbody tr th {
background-color: #506784;
color: #fff;
border: 1px solid #DFE8F3;
}
</style>
${pivotOutput.outerHTML}
`
}
export default {
getPivotCanvas,
getPivotHtml
}