Skip to content

Commit

Permalink
Merge pull request #242 from x-atlas-consortia/tjmadonna/232-sankey-s…
Browse files Browse the repository at this point in the history
…upport

Tjmadonna/232 sankey support
  • Loading branch information
maxsibilla authored Dec 6, 2024
2 parents 7c63483 + 2d46c10 commit 1ea758b
Show file tree
Hide file tree
Showing 16 changed files with 343 additions and 36 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.4.1
1.4.2
13 changes: 6 additions & 7 deletions src/components/DataTable/DatasetTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -430,13 +430,13 @@ const DatasetTable = ({
<ChartProvider>
<Visualizations data={countFilteredRecords(rawData, filters)} filters={filters} applyFilters={handleTableChange} />
</ChartProvider>
<div className="row">
<div className="col-12 col-md-3 count mt-md-3">
{TABLE.rowSelectionDropdown({menuProps, selectedEntities, countFilteredRecords, modifiedData, filters})}
{TABLE.csvDownloadButton({selectedEntities, countFilteredRecords, checkedModifiedData, filters, modifiedData, filename: 'datasets-data.csv'})}
</div>
<div className="count c-table--header">
{TABLE.rowSelectionDropdown({menuProps, selectedEntities, countFilteredRecords, modifiedData, filters})}
{TABLE.csvDownloadButton({selectedEntities, countFilteredRecords, checkedModifiedData, filters, modifiedData, filename: 'datasets-data.csv'})}
{TABLE.viewSankeyButton({filters})}
</div>
<Table className={`m-4 c-table--main ${countFilteredRecords(data, filters).length > 0 ? '' : 'no-data'}`}

<Table className={`c-table--main ${countFilteredRecords(data, filters).length > 0 ? '' : 'no-data'}`}
columns={filteredDatasetColumns}
dataSource={countFilteredRecords(rawData, filters)}
showHeader={!loading}
Expand All @@ -450,7 +450,6 @@ const DatasetTable = ({
type: 'checkbox',
...rowSelection,
}}

/>

<Modal
Expand Down
12 changes: 5 additions & 7 deletions src/components/DataTable/UploadTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,13 +205,11 @@ const UploadTable = ({ data, loading, filterUploads, uploadData, datasetData, ha
<Spinner />
) : (
<>
<div className="row">
<div className="col-12 col-md-3 count mt-md-3">
{TABLE.rowSelectionDropdown({menuProps, selectedEntities, countFilteredRecords, modifiedData, filters, entity: 'Upload'})}
{TABLE.csvDownloadButton({selectedEntities, countFilteredRecords, checkedModifiedData, filters, modifiedData, filename: 'uploads-data.csv'})}
</div>
<div className="count c-table--header">
{TABLE.rowSelectionDropdown({menuProps, selectedEntities, countFilteredRecords, modifiedData, filters, entity: 'Upload'})}
{TABLE.csvDownloadButton({selectedEntities, countFilteredRecords, checkedModifiedData, filters, modifiedData, filename: 'uploads-data.csv'})}
</div>
<Table className={`m-4 c-table--main ${countFilteredRecords(data, filters).length > 0 ? '' : 'no-data'}`}
<Table className={`c-table--main ${countFilteredRecords(data, filters).length > 0 ? '' : 'no-data'}`}
columns={uploadColumns}
showHeader={!loading}
dataSource={countFilteredRecords(rawData, filters)}
Expand Down Expand Up @@ -243,4 +241,4 @@ const UploadTable = ({ data, loading, filterUploads, uploadData, datasetData, ha
);
};

export default UploadTable
export default UploadTable
212 changes: 212 additions & 0 deletions src/components/Visualizations/Charts/Sankey.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import Spinner from '@/components/Spinner'
import { getRequestOptions } from '@/lib/helpers/general'
import { getHierarchy } from '@/lib/helpers/hierarchy'
import URLS from '@/lib/helpers/urls'
import axios from 'axios'
import * as d3 from 'd3'
import { sankey as d3sankey, sankeyLinkHorizontal } from 'd3-sankey'
import { useEffect, useRef, useState } from 'react'

function Sankey({ filters }) {
const [loading, setLoading] = useState(true)
const [graph, setGraph] = useState(null)
const containerRef = useRef(null)
const [containerDimensions, setContainerDimensions] = useState({ width: 0, height: 0 })

const validFilterMap = {
group_name: 'dataset_group_name',
dataset_type: 'dataset_dataset_type',
organ: 'organ_type',
status: 'dataset_status'
}

const getValidFilters = () => {
// converts the filter from the URL to the field names returned from the sankey endpoint
// also splits comma separated filter values into an array
return Object.keys(filters).reduce((acc, key) => {
if (validFilterMap[key.toLowerCase()] !== undefined) {
acc[validFilterMap[key].toLowerCase()] = filters[key].split(',')
}
return acc
}, {})
}

const fetchData = async () => {
// call the sankey endpoint
const res = await axios.get(URLS.entity.sankey(), getRequestOptions())
const data = res.data.map((row) => {
return { ...row, organ_type: getHierarchy(row.organ_type) }
})

// filter the data if there are valid filters
const validFilters = getValidFilters()
let filteredData = data
if (Object.keys(validFilters).length > 0) {
// Filter the data based on the valid filters
filteredData = data.filter((row) => {
// this acts as an AND filter
for (const [field, validValues] of Object.entries(validFilters)) {
if (!validValues.includes(row[field].toLowerCase())) {
return false
}
}
return true
})
}

// group the data into nodes and links
const columnNames = Object.values(validFilterMap)
const newGraph = { nodes: [], links: [] }
filteredData.forEach((row) => {
columnNames.forEach((columnName, columnIndex) => {
if (columnIndex !== columnNames.length - 1) {
let found = newGraph.nodes.find((found) => found.column === columnIndex && found.name === row[columnNames[columnIndex]])
if (found === undefined) {
found = { node: newGraph.nodes.length, name: row[columnName], column: columnIndex }
newGraph.nodes.push(found)
}

let found2 = newGraph.nodes.find((found2) => found2.column === columnIndex + 1 && found2.name === row[columnNames[columnIndex + 1]])
if (found2 === undefined) {
found2 = { node: newGraph.nodes.length, name: row[columnNames[columnIndex + 1]], column: columnIndex + 1 }
newGraph.nodes.push(found2)
}

let found3 = newGraph.links.find((found3) => found3.source === found.node && found3.target === found2.node)
if (found3 === undefined) {
found3 = { source: found.node, target: found2.node, value: 0 }
newGraph.links.push(found3)
}
found3.value = found3.value + 1
}
})
})

setLoading(false)
setGraph(newGraph)
}

const handleWindowResize = () => {
if (!containerRef.current) return
setContainerDimensions({
width: containerRef.current.clientWidth,
height: Math.max(containerRef.current.clientHeight, 1080)
})
}

useEffect(() => {
fetchData()
handleWindowResize()
window.addEventListener('resize', handleWindowResize)

return () => {
window.removeEventListener('resize', handleWindowResize)
}
}, [])

useEffect(() => {
if (!graph || !containerDimensions.width || !containerDimensions.height) return

// svg dimensions
const margin = { top: 20, right: 20, bottom: 20, left: 20 }
const width = containerDimensions.width - margin.left - margin.right
const height = containerDimensions.height - margin.top - margin.bottom

const color = d3.scaleOrdinal(d3.schemeCategory10)

// Layout the svg element
const container = d3.select(containerRef.current)
const svg = container.append('svg').attr('width', width).attr('height', height).attr('transform', `translate(${margin.left},${margin.top})`)

// Set up the Sankey generator
const sankey = d3sankey()
.nodeWidth(30)
.nodePadding(15)
.extent([
[0, margin.top],
[width, height - margin.bottom]
])

// Create the Sankey layout
const { nodes, links } = sankey({
nodes: graph.nodes.map((d) => Object.assign({}, d)),
links: graph.links.map((d) => Object.assign({}, d))
})

// Define the drag behavior
const drag = d3
.drag()
.on('start', function (event, d) {
d3.select(this).raise()
d.dragging = {
offsetX: event.x - d.x0,
offsetY: event.y - d.y0
}
})
.on('drag', function (event, d) {
d.x0 = Math.max(0, Math.min(width - d.x1 + d.x0, event.x - d.dragging.offsetX))
d.y0 = Math.max(0, Math.min(height - d.y1 + d.y0, event.y - d.dragging.offsetY))
d.x1 = d.x0 + sankey.nodeWidth()
d.y1 = d.y0 + (d.y1 - d.y0)
d3.select(this).attr('transform', `translate(${d.x0},${d.y0})`)
svg.selectAll('.c-sankey__link').attr('d', sankeyLinkHorizontal())
sankey.update({ nodes, links })
link.attr('d', sankeyLinkHorizontal())
})
.on('end', function (event, d) {
delete d.dragging
})

// Links
const link = svg
.append('g')
.selectAll('.link')
.data(links)
.join('path')
.attr('class', 'c-sankey__link')
.attr('d', sankeyLinkHorizontal())
.attr('stroke-width', (d) => Math.max(2, d.width))
.append('title')
.text((d) => `${d.source.name}${d.target.name}\n${d.value} Datasets`) // Tooltip

// Nodes
const node = svg
.append('g')
.selectAll('.node')
.data(nodes)
.join('g')
.attr('class', 'c-sankey__node')
.attr('transform', (d) => `translate(${d.x0},${d.y0})`)
.call(drag)

node.append('rect')
.attr('height', (d) => Math.max(5, d.y1 - d.y0))
.attr('width', sankey.nodeWidth())
.attr('fill', (d) => color(d.name))
.attr('stroke-width', 0)
.append('title')
.text((d) => `${d.name}\n${d.value} Datasets`) // Tooltip

node.append('text')
.attr('x', -6)
.attr('y', (d) => (d.y1 - d.y0) / 2)
.attr('dy', '0.35em')
.attr('text-anchor', 'end')
.text((d) => d.name)
.filter((d) => d.x0 < width / 2)
.attr('x', 6 + sankey.nodeWidth())
.attr('text-anchor', 'start')

return () => {
svg.remove()
}
}, [graph, containerDimensions])

return (
<div ref={containerRef} className='c-sankey__container'>
{loading && <Spinner />}
</div>
)
}

export default Sankey
4 changes: 2 additions & 2 deletions src/components/Visualizations/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ function Visualizations({ data, filters, applyFilters, defaultColumn = 'group_na
okButtonProps={{ disabled: selectedFilterValues.length === 0 }}
>
<Row>
<Col span={24}>
<div className='c-visualizations__dropdownContainer'>
<Dropdown
className='c-visualizations__columnDropdown'
menu={columnMenuProps}
Expand All @@ -270,7 +270,7 @@ function Visualizations({ data, filters, applyFilters, defaultColumn = 'group_na
<AreaChartOutlined />
</Button>
</Dropdown>
</Col>
</div>
</Row>

<Row>
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useContent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function useContent() {
)
let organsDict = {}
for (let o of organTypes.data) {
organsDict[o.term.trim()] = o.category?.term?.trim() || o.term?.trim()
organsDict[o.term.trim().toLowerCase()] = o.category?.term?.trim() || o.term?.trim()
}
window.UBKG = {organTypes: organTypes.data, organTypesGroups: organsDict}
return window.UBKG
Expand All @@ -53,4 +53,4 @@ function useContent() {



export default useContent
export default useContent
2 changes: 1 addition & 1 deletion src/lib/helpers/hierarchy.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const getHierarchy = (str) => {
if (!window.UBKG) return str
let res = window.UBKG.organTypesGroups[str.trim()]
let res = window.UBKG.organTypesGroups[str.trim().toLowerCase()]
if (!res) {
const r = new RegExp(/.+?(?=\()/)
res = str.match(r)
Expand Down
31 changes: 24 additions & 7 deletions src/lib/helpers/table.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {CaretDownOutlined, CloseOutlined, DownloadOutlined, EditOutlined} from "@ant-design/icons";
import {Dropdown, Space, Tooltip} from "antd";
import {Button, Dropdown, Space, Tooltip} from "antd";
import React from "react";
import {eq, toDateString} from "./general";
import ENVS from "./envs";
Expand Down Expand Up @@ -296,11 +296,28 @@ const TABLE = {
</span>
},
rowSelectionDropdown: ({menuProps, selectedEntities, countFilteredRecords, modifiedData, filters, entity = 'Dataset'}) => {
return <Space wrap>
<Dropdown.Button menu={menuProps}>
{selectedEntities.length ? 'Selected': 'Showing'} {selectedEntities.length ? selectedEntities.length : countFilteredRecords(modifiedData, filters).length} {entity}(s)
</Dropdown.Button>
</Space>
return <Space wrap>
<Dropdown.Button menu={menuProps}>
{selectedEntities.length ? 'Selected': 'Showing'} {selectedEntities.length ? selectedEntities.length : countFilteredRecords(modifiedData, filters).length} {entity}(s)
</Dropdown.Button>
</Space>
},
viewSankeyButton: ({filters}) => {
let queryString = ''
if (Object.keys(filters).length > 0) {
queryString = '?' + Object.entries(filters)
.map(([key, values]) => `${key}=${values}`)
.join('&');
}

return <Button
href={`/sankey${queryString}`}
target="_blank"
rel="noopener noreferrer"
type="primary"
className="text-decoration-none ms-2">
View Sankey Diagram
</Button>
},
getSelectedRows: (selectedEntities) => {
return selectedEntities.map((item) => item[TABLE.cols.f('id')])
Expand Down Expand Up @@ -361,4 +378,4 @@ const TABLE = {
}
}

export default TABLE
export default TABLE
3 changes: 2 additions & 1 deletion src/lib/helpers/urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const URLS = {
revisions: (uuid) => {
let path = process.env.NEXT_PUBLIC_REVISIONS_PATH.format(uuid)
return ENVS.urlFormat.entity(path)
}
},
sankey: () => ENVS.urlFormat.entity('/datasets/sankey_data')
},
ingest: {
data: {
Expand Down
3 changes: 2 additions & 1 deletion src/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const nextConfig = {
'rc-pagination',
'rc-picker',
'rc-notification',
'rc-tooltip'
'rc-tooltip',
'rc-input'
]
}

Expand Down
3 changes: 2 additions & 1 deletion src/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "data-ingest-board",
"version": "1.4.0",
"version": "1.4.2",
"private": true,
"scripts": {
"all": "npm-run-all --parallel dev css addons",
Expand All @@ -26,6 +26,7 @@
"bootstrap": "^5.3.2",
"cookies-next": "^3.0.0",
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"eslint-config-next": "^14.2.7",
"next": "^14.2.7",
"npm-run-all": "^4.1.5",
Expand Down
Loading

0 comments on commit 1ea758b

Please sign in to comment.