Skip to content

Commit

Permalink
feat: Selectable filters in bar charts, interactive across entire das…
Browse files Browse the repository at this point in the history
…hboard
  • Loading branch information
billyc committed Dec 1, 2021
1 parent 24e273d commit f4f82a6
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 31 deletions.
62 changes: 46 additions & 16 deletions src/charts/bar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ vue-plotly#vue-bar-chart(

<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'
Expand All @@ -39,22 +37,23 @@ export default class VueComponent extends Vue {
private globalState = this.$store.state
// private thread!: any
private dataRows: any = {}
private filteredRows: any = {}
private plotID = this.getRandomInt(100000)
private async mounted() {
this.updateTheme()
await this.loadData()
this.resizePlot()
window.addEventListener('resize', this.myEventHandler)
window.addEventListener('resize', this.handleResizeEvent)
this.$emit('isLoaded')
}
private async beforeDestroy() {
window.removeEventListener('resize', this.myEventHandler)
private beforeDestroy() {
window.removeEventListener('resize', this.handleResizeEvent)
this.datamanager.removeFilterListener(this.config, this.handleFilterChanged)
}
@Watch('globalState.isDarkMode') updateTheme() {
Expand All @@ -63,29 +62,60 @@ export default class VueComponent extends Vue {
this.layout.font.color = this.globalState.isDarkMode ? '#cccccc' : '#444444'
}
private handlePlotlyClick(click: any) {
private async 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
const filter = this.config.groupBy
const value = x
this.datamanager.setFilter(this.config.dataset, filter, value)
} catch (e) {
console.error(e)
}
}
private async handleFilterChanged() {
console.log('CHANGED FILTER')
try {
const { filteredRows } = await this.datamanager.getFilteredDataset(this.config)
// is filter UN-selected?
if (!filteredRows) {
this.data = [this.data[0]]
this.data[0].opacity = 1.0
return
}
const fullDataCopy = Object.assign({}, this.data[0])
fullDataCopy.x = filteredRows.x
fullDataCopy.y = filteredRows.y
fullDataCopy.opacity = 1.0
fullDataCopy.name = 'Filtered'
//@ts-ignore - let plotly manage bar colors EXCEPT the filter
fullDataCopy.marker = { color: '#ffaf00' } // 3c6' }
this.data = [this.data[0], fullDataCopy]
this.data[0].opacity = 0.3
this.data[0].name = 'All'
} catch (e) {
const message = '' + e
console.log(message)
this.dataRows = {}
}
}
private async loadData() {
if (!this.files.length) return
try {
const { fullData, filteredData } = await this.datamanager.getDataset(this.config)
const { allRows } = await this.datamanager.getDataset(this.config)
this.datamanager.addFilterListener(this.config, this.handleFilterChanged)
// console.log({ fullData })
this.dataRows = fullData
this.dataRows = allRows
this.updateChart()
} catch (e) {
const message = '' + e
Expand All @@ -98,8 +128,8 @@ export default class VueComponent extends Vue {
return Math.floor(Math.random() * max).toString()
}
// The myEventHandler was added because Plottly has a bug with resizing.
private myEventHandler() {
// The handleResizeEvent was added because Plotly has a bug with resizing (in stacked mode)
private handleResizeEvent() {
this.resizePlot()
}
Expand Down
116 changes: 101 additions & 15 deletions src/js/DashboardDataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,47 @@ export default class DashboardDataManager {
this.fileApi = this.getFileSystem(this.root)
}

public async getDataset(config: { dataset: string; groupBy?: string; value?: string }) {
console.log('getDataset', config)
public async getFilteredDataset(config: { dataset: string; groupBy?: string; value?: string }) {
const rows = this.datasets[config.dataset].filteredRows
if (!rows) return { filteredRows: null }

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

if (config.value && config.groupBy) {
const columnValues = config.value
const columnGroups = config.groupBy
bars = rollup(
rows,
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())

// filter the rows, too

return { filteredRows: { x, y } }
}

public async getDataset(config: { dataset: string; groupBy?: string; value?: string }) {
// first, get the dataset
if (!this.dataCache[config.dataset]) {
console.log('fetch:', config.dataset)
this.dataCache[config.dataset] = this.fetchDataset(config)
if (!this.datasets[config.dataset]) {
console.log('load:', config.dataset)

// allRows immediately returns a Promise<any[], which we wait on so that
// multiple charts don't all try to fetch the dataset individually
this.datasets[config.dataset] = {
rows: this.fetchDataset(config),
filteredRows: null,
activeFilters: {},
filterListeners: new Set(),
}
}
dataframe = await this.dataCache[config.dataset]
const allRows = await this.datasets[config.dataset].rows

// group the rows as needed
let bars: any = {}
Expand All @@ -45,7 +75,7 @@ export default class DashboardDataManager {
const columnValues = config.value
const columnGroups = config.groupBy
bars = rollup(
dataframe,
allRows,
v => v.reduce((a, b) => a + b[columnValues], 0),
(d: any) => d[columnGroups] // group-by
)
Expand All @@ -55,20 +85,70 @@ export default class DashboardDataManager {
const x = Array.from(bars.keys())
const y = Array.from(bars.values())

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

public setFilter(filter: string) {}
public setFilter(dataset: string, column: string, value: any) {
console.log('FILTERING:', dataset)

const allFilters = this.datasets[dataset].activeFilters
if (allFilters[column] !== undefined && allFilters[column] === value) {
delete allFilters[column]
} else {
allFilters[column] = value
}
this.datasets[dataset].activeFilters = allFilters

console.log('about to update filters')
console.log(this.datasets)
this.updateFilters(dataset) // this is async
}

public addFilterListener(config: { dataset: string }, listener: any) {
this.datasets[config.dataset].filterListeners.add(listener)
}

public removeFilterListener(config: { dataset: string }, listener: any) {
this.datasets[config.dataset].filterListeners.delete(listener)
}

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

// ---- PRIVATE STUFFS -----------------------

private async updateFilters(datasetId: string) {
const dataset = this.datasets[datasetId]
console.log({ dataset })

if (!Object.keys(dataset.activeFilters).length) {
dataset.filteredRows = null
} else {
const allRows = await dataset.rows

let filteredRows = allRows
for (const [column, value] of Object.entries(dataset.activeFilters)) {
console.log('filtering:', column, value)
filteredRows = filteredRows.filter(row => row[column] === value)
}
dataset.filteredRows = filteredRows
}
console.log(dataset.filteredRows)
this.notifyListeners(datasetId)
}

private notifyListeners(datasetId: string) {
const dataset = this.datasets[datasetId]
for (const notifyListener of dataset.filterListeners) {
notifyListener()
}
}

private thread!: any
private files: any[] = []

private async fetchDataset(config: { dataset: string; groupBy?: string; value?: string }) {
private async fetchDataset(config: { dataset: string }) {
if (!this.files.length) {
const { files } = await new HTTPFileSystem(this.fileApi).getDirectory(this.subfolder)
this.files = files
Expand Down Expand Up @@ -106,10 +186,16 @@ export default class DashboardDataManager {
return svnProject[0]
}

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

private filteredDataCache: { [id: string]: any[] } = {}
private subfolder = ''
private root = ''
private fileApi: FileSystemConfig

private datasets: {
[id: string]: {
rows: Promise<any[]>
filteredRows: any[] | null
activeFilters: { [column: string]: any }
filterListeners: Set<any>
}
} = {}
}

0 comments on commit f4f82a6

Please sign in to comment.