diff --git a/src/charts/bar.vue b/src/charts/bar.vue
index 95fee69f..89c9038c 100644
--- a/src/charts/bar.vue
+++ b/src/charts/bar.vue
@@ -1,5 +1,14 @@
-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"
@@ -7,8 +16,9 @@ vue-plotly(:data="data" :layout="layout" :options="options" :config="{responsive
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,
@@ -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)
@@ -36,6 +49,7 @@ export default class VueComponent extends Vue {
await this.loadData()
this.resizePlot()
window.addEventListener('resize', this.myEventHandler)
+
this.$emit('isLoaded')
}
@@ -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)
}
}
@@ -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
@@ -148,6 +229,7 @@ export default class VueComponent extends Vue {
textinfo: 'label+percent',
textposition: 'inside',
automargin: true,
+ opacity: 1.0,
})
}
}
@@ -164,7 +246,7 @@ export default class VueComponent extends Vue {
color: '#444444',
family: UI_FONT,
},
- barmode: '',
+ barmode: 'overlay',
bargap: 0.08,
xaxis: {
autorange: true,
@@ -190,6 +272,7 @@ export default class VueComponent extends Vue {
textinfo: 'label+percent',
textposition: 'inside',
automargin: true,
+ opacity: 1.0,
},
]
diff --git a/src/js/DashboardDataManager.ts b/src/js/DashboardDataManager.ts
new file mode 100644
index 00000000..40eeb280
--- /dev/null
+++ b/src/js/DashboardDataManager.ts
@@ -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[] } = {}
+
+ private filteredDataCache: { [id: string]: any[] } = {}
+ private subfolder = ''
+ private root = ''
+ private fileApi: FileSystemConfig
+}
diff --git a/src/views/DashBoard.vue b/src/views/DashBoard.vue
index 500dd1f8..fd33e555 100644
--- a/src/views/DashBoard.vue
+++ b/src/views/DashBoard.vue
@@ -28,6 +28,7 @@
:files="fileList"
:yaml="card.props.configFile"
:config="card.props"
+ :datamanager="datamanager"
:style="{opacity: opacity[card.id]}"
@isLoaded="handleCardIsLoaded(card)"
)
@@ -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
@@ -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
diff --git a/src/views/TabbedDashboardView.vue b/src/views/TabbedDashboardView.vue
index 6b4046b1..9792e5d6 100644
--- a/src/views/TabbedDashboardView.vue
+++ b/src/views/TabbedDashboardView.vue
@@ -20,6 +20,7 @@
:root="root"
:xsubfolder="xsubfolder"
:config="dashboards[activeTab]"
+ :datamanager="dashboardDataManager"
@zoom="handleZoom"
)
@@ -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 {
@@ -51,6 +53,7 @@ export default class VueComponent extends Vue {
private fileApi!: HTTPFileSystem
private dashboards: any = []
+ private dashboardDataManager?: DashboardDataManager
private isZoomed = false
@@ -58,12 +61,19 @@ export default class VueComponent extends Vue {
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()