Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visualise changes over time #175

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
acdca8f
Fix npm package vulnerabilities via audit
codemacabre Nov 14, 2024
02d5608
Add placeholder slider controls
codemacabre Nov 15, 2024
ec3716c
Get statementDate values from data
codemacabre Nov 18, 2024
34ae963
Update selected date when apply button pressed
codemacabre Nov 19, 2024
70f2cc9
Filter data by selected statementDate
codemacabre Nov 19, 2024
6330a4c
Filter data by closed records of various types
codemacabre Nov 19, 2024
d093709
Fix data not rendering if no duplicates
codemacabre Nov 26, 2024
6cd4b0c
Ensure unique nodes are included in rendered data
codemacabre Nov 26, 2024
946d82d
Re-render graph on slider input change & remove apply button
codemacabre Nov 26, 2024
75fd541
Fix entity nodes not displaying properties
codemacabre Nov 26, 2024
85b3d40
Render <0.4 data, accommodating changes over time
codemacabre Nov 26, 2024
0ca1646
Remove unused 'latest' function
codemacabre Nov 26, 2024
f1872cc
Process duplicate and unique statements through all filters
codemacabre Nov 26, 2024
c20c234
Set default version to 0.4
codemacabre Nov 26, 2024
ffe342f
Fix incorrect icons for unknownPerson and unknownEntity
codemacabre Nov 28, 2024
f236db7
Ensure dates are being parsed correctly
codemacabre Nov 28, 2024
17d55ed
Clear properties when new data is loaded
codemacabre Nov 28, 2024
145e3b2
Update tests to accommodate new util function
codemacabre Nov 28, 2024
eac22e8
Remove unused sets and update test to match
codemacabre Nov 28, 2024
fe59d69
Fix incorrect icons for listed companies
codemacabre Nov 28, 2024
cb05489
Ensure data with just statement.recordType is minimum valid statement
codemacabre Dec 9, 2024
a5491df
Ensure slider drag event persists & page doesn't scroll on input
codemacabre Dec 9, 2024
37d0c49
Add grab / grabbing cursor to input thumb
codemacabre Dec 9, 2024
e8ebf40
Hide tooltips on draw()
codemacabre Dec 9, 2024
d472ed5
Improve error handling of invalid JSON
codemacabre Dec 10, 2024
a3db52a
Modify the curevMonotoneX algorithm to smooth the ends of the curves
codemacabre Dec 10, 2024
cdde010
Tweak curve algorithm to be less angular
codemacabre Dec 10, 2024
8d029ac
Ensure data with just statementType is valid for <0.4 data
codemacabre Dec 11, 2024
b212b52
Add grab / grabbing cursor on base SVG
codemacabre Dec 11, 2024
778b12a
Hide tooltip on click
codemacabre Dec 11, 2024
3006086
Add more detailed comment on how <0.4 change over time data is handled
codemacabre Dec 13, 2024
9ed5ca1
Set cursor to 'not-allowed' on disabled slider input
codemacabre Dec 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions demo/demo.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ button {
}

#svg-holder {
cursor: grab;
position: relative;
text-align: center;
border: 1px solid #000;
min-height: 400px;
}

#svg-holder:active {
cursor: grabbing;
}

#bods-svg {
width: 100%;
min-height: 400px;
Expand Down Expand Up @@ -57,6 +62,61 @@ button {
overflow: scroll;
}

#slider-container {
display: none;
border: 1px solid #000;
margin: 10px 0 10px 0;
width: 100%;
}

#slider-input {
width: calc(100% - 20px);
margin: 10px auto 0 auto;
}

#slider-input:disabled {
cursor: not-allowed;
}

#slider-input:active {
cursor: grabbing;
}

#slider-input:disabled::-webkit-slider-thumb {
cursor: not-allowed;
}

#slider-input:disabled::-moz-range-thumb {
cursor: not-allowed;
}

#slider-input:disabled::-ms-thumb {
cursor: not-allowed;
}

#slider-input::-webkit-slider-thumb {
cursor: grab;
}

#slider-input::-moz-range-thumb {
cursor: grab;
}

#slider-input::-ms-thumb {
cursor: grab;
}
#slider-input::-webkit-slider-thumb:active {
cursor: grabbing;
}

#slider-input::-moz-range-thumb:active {
cursor: grabbing;
}

#slider-input::-ms-thumb:active {
cursor: grabbing;
}

.button-container {
text-align: right;
}
Expand Down
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ <h1>BODS Data Visualisation Demo</h1>
<button id="zoom_in">+</button>
<button id="zoom_out">-</button>
</div>
<div id="slider-container"></div>
<div id="disclosure-widget"></div>
<button id="download-svg">Download SVG</button>
<button id="download-png">Download PNG</button>
Expand Down
10 changes: 6 additions & 4 deletions demo/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { draw } from '../src/index.js';
import { selectData } from '../src/index.js';
import { clearSVG } from '../src/utils/svgTools.js';
import { parse } from '../src/parse/parse.js';
import './demo.css';
Expand Down Expand Up @@ -37,9 +37,11 @@ const getJSON = async () => {

const visualiseData = (data) => {
// Render data as text
document.getElementById('result').value = data.formatted;
// Render data as graph
draw({
if (data.formatted) {
document.getElementById('result').value = data.formatted;
}
// Select data and render as graph
selectData({
data: data.parsed,
container: document.getElementById('svg-holder'),
imagesPath: 'images',
Expand Down
1 change: 1 addition & 0 deletions demo/script-tag.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ <h1>BODS Data Visualisation Demo</h1>
<button id="zoom_in">+</button>
<button id="zoom_out">-</button>
</div>
<div id="slider-container"></div>
<div id="disclosure-widget"></div>
<div class="info-container">
<p><a href="https://github.com/openownership/visualisation-tool/blob/master/docs/spec.md#data-requirements">Data requirements</a></p>
Expand Down
2 changes: 1 addition & 1 deletion demo/script-tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const visualiseData = (data) => {
// Render data as text
document.getElementById('result').value = data.formatted;
// Render data as graph
BODSDagre.draw({
BODSDagre.selectData({
data: data.parsed,
container: document.getElementById('svg-holder'),
imagesPath: 'images',
Expand Down
21 changes: 11 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 50 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,74 @@ import {
createUnspecifiedNode,
} from './render/renderD3';
import { setupGraph, setEdges, setNodes } from './render/renderGraph';
import { setupUI, renderMessage, renderProperties } from './render/renderUI';
import { setupUI, renderMessage, renderProperties, renderDateSlider } from './render/renderUI';
import { getDates, filteredData } from './utils/bods.js';

import './style.css';

// This sets up the basic format of the graph, such as direction, node and rank separation, and default label limits
const draw = ({
export const selectData = ({
data,
selectedData,
container,
imagesPath,
labelLimit = 8,
rankDir = 'LR',
viewProperties = true,
useTippy = false,
currentlySelectedDate = null,
}) => {
const config = {
data,
container,
imagesPath,
labelLimit,
rankDir,
viewProperties,
useTippy,
};

if (data) {
const version = data[0]?.publicationDetails?.bodsVersion || '0.4';

// Detect dates in data; default to most recent
const dates = getDates(data);
let selectedDate = currentlySelectedDate ? currentlySelectedDate : dates[dates.length - 1];

// Update selected date according to slider position
renderDateSlider(dates, version, currentlySelectedDate);
const slider = document.querySelector('#slider-input');
if (slider) {
slider.addEventListener('input', (e) => {
const scrollPosition = window.scrollY;
selectedDate = dates[document.querySelector('#slider-input').value];
config.selectedData = filteredData(data, selectedDate, version);
draw(config);
window.scrollTo(0, scrollPosition);
slider.focus();
});
}
config.selectedData = filteredData(data, selectedDate, version);
draw(config);
}
};

// This sets up the basic format of the graph, such as direction, node and rank separation, and default label limits
export const draw = (config) => {
const { data, selectedData, container, imagesPath, labelLimit, rankDir, viewProperties, useTippy } = config;
// Initialise D3 and graph
const { svg, inner } = setupD3(container);
const { g, render } = setupGraph(rankDir);

defineArrowHeads(svg);

// Extract the BODS data that is required for drawing the graph
const { edges } = getEdges(data);
const { nodes } = getNodes(data, edges);
const { edges } = getEdges(selectedData);
const { nodes } = getNodes(selectedData, edges);

if (edges.length === 0 && nodes.length === 0) {
const message = 'Your data does not have any information that can be drawn.';
renderMessage(message);
}

// This section maps the incoming BODS data to the parameters expected by Dagre
setEdges(edges, g);
Expand Down Expand Up @@ -146,5 +191,3 @@ const draw = ({
renderProperties(inner, g, useTippy);
}
};

export { draw };
9 changes: 4 additions & 5 deletions src/model/edges/edges.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { compareVersions } from 'compare-versions';
import { curveMonotoneX } from 'd3';
import { closedRecords, latest } from '../../utils/bods.js';
import { monotoneX } from '../../utils/curve.js';
import interestTypesCodelist from '../../codelists/interestTypes.js';

// This sets the style and shape of the edges using D3 parameters
const edgeConfig = {
style: 'fill: none; stroke: #000; stroke-width: 5px;',
curve: curveMonotoneX,
curve: monotoneX,
};

const defaultStroke = 5;
Expand Down Expand Up @@ -65,7 +64,7 @@ export const checkInterests = (interestRelationship) => {
};

export const getOwnershipEdges = (bodsData) => {
const version = bodsData[0]?.publicationDetails?.bodsVersion || '0';
const version = bodsData[0]?.publicationDetails?.bodsVersion || '0.4';

const filteredData = bodsData.filter((statement) => {
if (compareVersions(version, '0.4') >= 0) {
Expand Down Expand Up @@ -205,7 +204,7 @@ export const getOwnershipEdges = (bodsData) => {
};
});

return latest(mappedData, closedRecords, version);
return mappedData;
};

export const getEdges = (data) => {
Expand Down
26 changes: 13 additions & 13 deletions src/model/nodes/nodes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { compareVersions } from 'compare-versions';
import generateNodeLabel from './nodeSVGLabel.js';
import { closedRecords, latest } from '../../utils/bods.js';
import sanitise from '../../utils/sanitiser.js';

// This will generate a node when there are unspecified fields
Expand Down Expand Up @@ -95,7 +94,7 @@ const iconType = (nodeType) => {

// This builds up the required person object from the BODS data, using the functions above
export const getPersonNodes = (bodsData) => {
const version = bodsData[0]?.publicationDetails?.bodsVersion || '0';
const version = bodsData[0]?.publicationDetails?.bodsVersion || '0.4';

const filteredData = bodsData.filter((statement) => {
if (compareVersions(version, '0.4') >= 0) {
Expand All @@ -111,16 +110,16 @@ export const getPersonNodes = (bodsData) => {
statementId = null,
statementDate = null,
recordId = null,
recordDetails = null,
recordDetails = {},
names = null,
personType = null,
personType = '',
nationalities = null,
} = statement;
const countryCode = nationalities && nationalities[0].code ? sanitise(nationalities[0].code) : null;
const replaces = statement.replacesStatements ? statement.replacesStatements : [];

const personNameData = recordDetails?.names || names;
const personTypeData = recordDetails?.personType || personType;
const personTypeData = recordDetails?.personType || personType || 'unknownPerson';

const personLabel =
personNameData && personNameData.length > 0 && personTypeData
Expand All @@ -146,12 +145,12 @@ export const getPersonNodes = (bodsData) => {
};
});

return latest(mappedData, closedRecords, version);
return mappedData;
};

// This builds up the required entity object from the BODS data, using the functions above
export const getEntityNodes = (bodsData) => {
const version = bodsData[0]?.publicationDetails?.bodsVersion || '0';
const version = bodsData[0]?.publicationDetails?.bodsVersion || '0.4';

const filteredData = bodsData.filter((statement) => {
if (compareVersions(version, '0.4') >= 0) {
Expand All @@ -167,9 +166,9 @@ export const getEntityNodes = (bodsData) => {
statementId = null,
statementDate = null,
recordId = null,
recordDetails = null,
recordDetails = {},
name = null,
entityType = 'unknown',
entityType = '',
publicListing = null,
incorporatedInJurisdiction = null,
jurisdiction = null,
Expand All @@ -191,9 +190,10 @@ export const getEntityNodes = (bodsData) => {

const replaces = statement.replacesStatements ? statement.replacesStatements : [];
const nodeType =
publicListing?.hasPublicListing !== true || recordDetails?.publicListing?.hasPublicListing !== true
? recordDetails?.entityType.type || entityType
: 'registeredEntityListed';
publicListing?.hasPublicListing === true || recordDetails?.publicListing?.hasPublicListing === true
? 'registeredEntityListed'
: recordDetails?.entityType?.type || entityType || 'unknownEntity';

return {
id: statementId || statementID,
statementDate,
Expand All @@ -214,7 +214,7 @@ export const getEntityNodes = (bodsData) => {
};
});

return latest(mappedData, closedRecords, version);
return mappedData;
};

export const setUnknownNode = (source) => unknownNode(source);
Expand Down
2 changes: 1 addition & 1 deletion src/parse/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const parse = (data) => {
const message = 'There was an error drawing your data. The data must be a valid JSON array of objects.';
renderMessage(message);
console.error(error);
return {};
return [];
}

// Format JSON consistently
Expand Down
Loading