Skip to content

Commit

Permalink
DashboardDataManager - first version working with centralized data mgmt
Browse files Browse the repository at this point in the history
  • Loading branch information
billyc committed Dec 1, 2021
1 parent 28f4b70 commit 24e273d
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 17 deletions.
117 changes: 100 additions & 17 deletions src/charts/bar.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
<template lang="pug">
vue-plotly(:data="data" :layout="layout" :options="options" :config="{responsive: true}" :class="className")
vue-plotly#vue-bar-chart(
:data="data"
:layout="layout"
:options="options"
:config="{responsive: true}"
:id="id"
ref="plotly-element"
@click="handlePlotlyClick"
)
//- :class="className"
</template>

<script lang="ts">
import { Vue, Component, Watch, Prop } from 'vue-property-decorator'
import { Worker, spawn, Thread } from 'threads'
import VuePlotly from '@statnett/vue-plotly'
import { rollup } from 'd3-array'
import { FileSystemConfig, UI_FONT } from '@/Globals'
import DashboardDataManager from '@/js/DashboardDataManager'
const mockData = {
car: 34,
Expand All @@ -23,10 +33,13 @@ export default class VueComponent extends Vue {
@Prop({ required: true }) subfolder!: string
@Prop({ required: true }) files!: string[]
@Prop({ required: true }) config!: any
@Prop({ required: true }) datamanager!: DashboardDataManager
private id = 'bar-' + Math.random()
private globalState = this.$store.state
private thread!: any
// private thread!: any
private dataRows: any = {}
private plotID = this.getRandomInt(100000)
Expand All @@ -36,6 +49,7 @@ export default class VueComponent extends Vue {
await this.loadData()
this.resizePlot()
window.addEventListener('resize', this.myEventHandler)
this.$emit('isLoaded')
}
Expand All @@ -49,28 +63,34 @@ export default class VueComponent extends Vue {
this.layout.font.color = this.globalState.isDarkMode ? '#cccccc' : '#444444'
}
private handlePlotlyClick(click: any) {
try {
const { x, y, data } = click.points[0]
const fullData = Object.assign({}, data)
fullData.x = [x]
fullData.y = [y]
this.data.push(fullData)
this.data[0].opacity = 0.25
} catch (e) {
console.error(e)
}
}
private async loadData() {
if (!this.files.length) return
if (this.thread) Thread.terminate(this.thread)
this.thread = await spawn(new Worker('../workers/DataFetcher.thread'))
try {
const data = await this.thread.fetchData({
fileSystemConfig: this.fileSystemConfig,
subfolder: this.subfolder,
files: this.files,
config: this.config,
})
this.dataRows = data
const { fullData, filteredData } = await this.datamanager.getDataset(this.config)
// console.log({ fullData })
this.dataRows = fullData
this.updateChart()
} catch (e) {
const message = '' + e
console.log(message)
this.dataRows = {}
} finally {
Thread.terminate(this.thread)
}
}
Expand Down Expand Up @@ -104,6 +124,67 @@ export default class VueComponent extends Vue {
}
private updateChart() {
if (this.config.groupBy) this.updateChartWithGroupBy()
else this.updateChartSimple()
}
private updateChartWithGroupBy() {
this.className = this.plotID // stacked bug-fix hack
const { x, y } = this.dataRows
this.data = [
{
x,
y,
name: this.config.groupBy,
type: 'bar',
textinfo: 'label+percent',
textposition: 'inside',
automargin: true,
opacity: 1.0,
},
]
// var useOwnNames = false
// for (var i = 0; i < this.dataRows.length; i++) {
// if (i == 0 && this.config.skipFirstRow) {
// } else {
// x.push(this.dataRows[i][this.config.x])
// }
// }
// for (let i = 0; i < this.config.columns.length; i++) {
// const name = this.config.columns[i]
// let legendName = ''
// if (this.config.columns[i] !== 'undefined') {
// if (useOwnNames) {
// legendName = this.config.legendTitles[i]
// } else {
// legendName = name
// }
// const value = []
// for (var j = 0; j < this.dataRows.length; j++) {
// if (j == 0 && this.config.skipFirstRow) {
// } else {
// value.push(this.dataRows[j][name])
// }
// }
// this.data.push({
// x: x,
// y: value,
// name: legendName,
// type: 'bar',
// textinfo: 'label+percent',
// textposition: 'inside',
// automargin: true,
// })
// }
// }
}
private updateChartSimple() {
const x = []
var useOwnNames = false
Expand Down Expand Up @@ -148,6 +229,7 @@ export default class VueComponent extends Vue {
textinfo: 'label+percent',
textposition: 'inside',
automargin: true,
opacity: 1.0,
})
}
}
Expand All @@ -164,7 +246,7 @@ export default class VueComponent extends Vue {
color: '#444444',
family: UI_FONT,
},
barmode: '',
barmode: 'overlay',
bargap: 0.08,
xaxis: {
autorange: true,
Expand All @@ -190,6 +272,7 @@ export default class VueComponent extends Vue {
textinfo: 'label+percent',
textposition: 'inside',
automargin: true,
opacity: 1.0,
},
]
Expand Down
115 changes: 115 additions & 0 deletions src/js/DashboardDataManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* DashboardDataManager: this class loads, caches, and filters CSV and XML datasets
* for use by dashboard charts and maps. Loosely based on the VizWit system
* (see http://vizwit.io/) but we don't have a Carto database so all of the data
* is stored internally in this class.
*
* Each tabbed dashboard should instantiate this class once, and destroy it when the dashboard
* is closed. Datasets can be big, we don't want them to stick around forever!
*
* Data queries will return -both- the full dataset AND a filtered dataset. That way
* the filtered data can be visually layered on top of the full data.
*/

import { rollup } from 'd3-array'
import { Worker, spawn, Thread } from 'threads'

import { FileSystemConfig } from '@/Globals'
import globalStore from '@/store'
import HTTPFileSystem from './HTTPFileSystem'

export default class DashboardDataManager {
constructor(...args: string[]) {
// hello
this.root = args.length ? args[0] : ''
this.subfolder = args.length ? args[1] : ''
this.fileApi = this.getFileSystem(this.root)
}

public async getDataset(config: { dataset: string; groupBy?: string; value?: string }) {
console.log('getDataset', config)

let dataframe: any[] = []

// first, get the dataset
if (!this.dataCache[config.dataset]) {
console.log('fetch:', config.dataset)
this.dataCache[config.dataset] = this.fetchDataset(config)
}
dataframe = await this.dataCache[config.dataset]

// group the rows as needed
let bars: any = {}

if (config.value && config.groupBy) {
const columnValues = config.value
const columnGroups = config.groupBy
bars = rollup(
dataframe,
v => v.reduce((a, b) => a + b[columnValues], 0),
(d: any) => d[columnGroups] // group-by
)
} else {
// TODO need to handle non-value, non-group here
}
const x = Array.from(bars.keys())
const y = Array.from(bars.values())

return { fullData: { x, y }, filteredData: {} }
}

public setFilter(filter: string) {}

public clearCache() {
this.dataCache = {}
}

// ---- PRIVATE STUFFS -----------------------
private thread!: any
private files: any[] = []

private async fetchDataset(config: { dataset: string; groupBy?: string; value?: string }) {
if (!this.files.length) {
const { files } = await new HTTPFileSystem(this.fileApi).getDirectory(this.subfolder)
this.files = files
}

if (this.thread) Thread.terminate(this.thread)
this.thread = await spawn(new Worker('../workers/DataFetcher.thread'))

try {
const data = await this.thread.fetchData({
fileSystemConfig: this.fileApi,
subfolder: this.subfolder,
files: this.files,
config: config,
})

return data
} catch (e) {
const message = '' + e
console.log(message)
} finally {
Thread.terminate(this.thread)
}
return []
}

private getFileSystem(name: string) {
const svnProject: FileSystemConfig[] = globalStore.state.svnProjects.filter(
(a: FileSystemConfig) => a.slug === name
)
if (svnProject.length === 0) {
console.error('DDM: no such project')
throw Error
}
return svnProject[0]
}

private dataCache: { [dataset: string]: Promise<any> | any[] } = {}

private filteredDataCache: { [id: string]: any[] } = {}
private subfolder = ''
private root = ''
private fileApi: FileSystemConfig
}
3 changes: 3 additions & 0 deletions src/views/DashBoard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
:files="fileList"
:yaml="card.props.configFile"
:config="card.props"
:datamanager="datamanager"
:style="{opacity: opacity[card.id]}"
@isLoaded="handleCardIsLoaded(card)"
)
Expand All @@ -42,6 +43,7 @@ import HTTPFileSystem from '@/js/HTTPFileSystem'
import { FileSystemConfig } from '@/Globals'
import TopSheet from '@/components/TopSheet/TopSheet.vue'
import charts from '@/charts/allCharts'
import DashboardDataManager from '@/js/DashboardDataManager'
// append a prefix so the html template is legal
const namedCharts = {} as any
Expand All @@ -54,6 +56,7 @@ Object.keys(charts).forEach((key: any) => {
export default class VueComponent extends Vue {
@Prop({ required: true }) private root!: string
@Prop({ required: true }) private xsubfolder!: string
@Prop({ required: true }) private datamanager!: DashboardDataManager
@Prop({ required: false }) private gist!: any
@Prop({ required: false }) private config!: any
Expand Down
10 changes: 10 additions & 0 deletions src/views/TabbedDashboardView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
:root="root"
:xsubfolder="xsubfolder"
:config="dashboards[activeTab]"
:datamanager="dashboardDataManager"
@zoom="handleZoom"
)

Expand All @@ -40,6 +41,7 @@ import { FileSystemConfig } from '@/Globals'
import DashBoard from '@/views/DashBoard.vue'
import FolderBrowser from '@/views/FolderBrowser.vue'
import HTTPFileSystem from '@/js/HTTPFileSystem'
import DashboardDataManager from '@/js/DashboardDataManager'
@Component({ components: { DashBoard, FolderBrowser }, props: {} })
export default class VueComponent extends Vue {
Expand All @@ -51,19 +53,27 @@ export default class VueComponent extends Vue {
private fileApi!: HTTPFileSystem
private dashboards: any = []
private dashboardDataManager?: DashboardDataManager
private isZoomed = false
private mounted() {
this.updateRoute()
}
private beforeDestroy() {
if (this.dashboardDataManager) this.dashboardDataManager.clearCache()
}
@Watch('root')
@Watch('xsubfolder')
private updateRoute() {
this.fileSystemConfig = this.getFileSystem(this.root)
if (!this.fileSystemConfig) return
if (this.dashboardDataManager) this.dashboardDataManager.clearCache()
this.dashboardDataManager = new DashboardDataManager(this.root, this.xsubfolder)
this.fileApi = new HTTPFileSystem(this.fileSystemConfig)
// this.generateBreadcrumbs()
Expand Down

0 comments on commit 24e273d

Please sign in to comment.