feat(4D): Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode (#3664)
Co-authored-by: Alireza <ar.sedghi@gmail.com> Co-authored-by: Neil <neil.a.macphee@gmail.com>
This commit is contained in:
200 files changed
+9589
-2178
No files matched your search
@@ -0,0 +1,40 @@
|
||||
.LineChart svg {
|
||||
color: #ddd;
|
||||
border: solid 1px #383f42;
|
||||
}
|
||||
|
||||
.LineChart svg .background {
|
||||
stroke: none;
|
||||
fill: #151c20;
|
||||
}
|
||||
|
||||
.LineChart svg .axis.label {
|
||||
fill: #ddd;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.LineChart svg .axis .tick line {
|
||||
stroke: #ddd;
|
||||
}
|
||||
|
||||
.LineChart svg .axis .tick text {
|
||||
fill: #ddd;
|
||||
}
|
||||
|
||||
.LineChart svg .axis.grid .domain {
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.LineChart svg .axis.grid .tick line {
|
||||
stroke: #383f42;
|
||||
}
|
||||
|
||||
.LineChart svg .line {
|
||||
stroke-width: 1px;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.LineChart svg .legend .legend-background {
|
||||
fill: none;
|
||||
stroke: #383f42;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import React, { useEffect, useLayoutEffect, useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as d3Selection from 'd3-selection';
|
||||
import { lineChart } from './d3LineChart';
|
||||
import './LineChart.css';
|
||||
|
||||
const LineChart = ({
|
||||
width: widthProp,
|
||||
height: heightProp,
|
||||
axis,
|
||||
series,
|
||||
showAxisLabels,
|
||||
showAxisGrid,
|
||||
showLegend,
|
||||
legendWidth,
|
||||
transparentChartBackground,
|
||||
containerClassName,
|
||||
chartContainerClassName,
|
||||
}: {
|
||||
title: string;
|
||||
width: number;
|
||||
height: number;
|
||||
showAxisGrid: boolean;
|
||||
showAxisLabels: boolean;
|
||||
showLegend: boolean;
|
||||
legendWidth: number;
|
||||
transparentChartBackground: boolean;
|
||||
containerClassName: string;
|
||||
chartContainerClassName: string;
|
||||
}): JSX.Element => {
|
||||
const chartContainerRef = useRef(null);
|
||||
const [d3SVGContainer, setD3SVGRef] = useState(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
const [height, setHeight] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const chartContainer = chartContainerRef.current;
|
||||
const containerWidth = chartContainer.offsetWidth;
|
||||
const containerHeight = chartContainer.offsetHeight;
|
||||
const d3Container = d3Selection
|
||||
.create('svg')
|
||||
.attr('viewBox', [0, 0, containerWidth, containerHeight])
|
||||
.style('max-width', `${containerWidth}px`)
|
||||
.style('overflow', 'visible');
|
||||
|
||||
chartContainer.append(d3Container.node());
|
||||
|
||||
setD3SVGRef(d3Container);
|
||||
setWidth(containerWidth);
|
||||
setHeight(containerHeight);
|
||||
}, [chartContainerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!d3SVGContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
lineChart.addLineChartNode({
|
||||
d3SVGRef: d3SVGContainer,
|
||||
axis,
|
||||
series,
|
||||
width,
|
||||
height,
|
||||
showAxisLabels,
|
||||
showAxisGrid,
|
||||
showLegend,
|
||||
legendWidth,
|
||||
transparentChartBackground,
|
||||
});
|
||||
}, [
|
||||
d3SVGContainer,
|
||||
axis,
|
||||
series,
|
||||
width,
|
||||
height,
|
||||
showAxisLabels,
|
||||
showAxisGrid,
|
||||
transparentChartBackground,
|
||||
showLegend,
|
||||
legendWidth,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'LineChart text-white',
|
||||
{
|
||||
[`w-[${widthProp}px]`]: !!widthProp,
|
||||
[`h-[${heightProp}px]`]: !!heightProp,
|
||||
},
|
||||
{
|
||||
'w-full': !widthProp,
|
||||
'h-full': !heightProp,
|
||||
},
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
id="chartContainer"
|
||||
ref={chartContainerRef}
|
||||
className={classnames('h-full w-full', chartContainerClassName)}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LineChart.defaultProps = {
|
||||
showAxisLabels: true,
|
||||
showAxisGrid: true,
|
||||
showLegend: false,
|
||||
legendWidth: 120,
|
||||
transparentChartBackground: false,
|
||||
};
|
||||
|
||||
LineChart.propTypes = {
|
||||
title: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
showAxisLabels: PropTypes.bool,
|
||||
showAxisGrid: PropTypes.bool,
|
||||
showLegend: PropTypes.bool,
|
||||
legendWidth: PropTypes.number,
|
||||
transparentChartBackground: PropTypes.bool,
|
||||
containerClassName: PropTypes.string,
|
||||
chartContainerClassName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default LineChart;
|
||||
@@ -0,0 +1,181 @@
|
||||
import LineChart from '../LineChart';
|
||||
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
|
||||
|
||||
export const argTypes = {
|
||||
component: LineChart,
|
||||
title: 'Components/LineChart',
|
||||
};
|
||||
|
||||
<Meta
|
||||
title="Components/LineChart"
|
||||
component={LineChart}
|
||||
/>
|
||||
|
||||
export const LineChartTemplate = args => (
|
||||
<div className="w-96">
|
||||
<LineChart {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
<Heading
|
||||
title="LineChart"
|
||||
componentRelativePath="LineChart/LineChart.tsx"
|
||||
/>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
LineChart is a component that...
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Overview"
|
||||
args={{
|
||||
width: 800,
|
||||
height: 400,
|
||||
showLegend: true,
|
||||
legendWidth: 150,
|
||||
axis: {
|
||||
x: {
|
||||
label: 'Time (s)',
|
||||
indexRef: 0,
|
||||
type: 'x',
|
||||
},
|
||||
y: {
|
||||
label: 'Signal Intensity',
|
||||
indexRef: 1,
|
||||
type: 'y',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
label: 'First series',
|
||||
points: [
|
||||
[0, 607.54],
|
||||
[1, 648.28],
|
||||
[2, 696.7],
|
||||
[3, 693.87],
|
||||
[4, 740.02],
|
||||
[5, 707.04],
|
||||
[6, 748.73],
|
||||
[7, 792.34],
|
||||
[8, 842.03],
|
||||
[9, 808.63],
|
||||
[10, 798.81],
|
||||
[11, 836.71],
|
||||
[12, 800.22],
|
||||
[13, 761.66],
|
||||
[14, 745.5],
|
||||
[15, 774.53],
|
||||
[16, 824.04],
|
||||
[17, 837.02],
|
||||
[18, 857.5],
|
||||
[19, 818.29],
|
||||
[20, 786.71],
|
||||
[21, 797.34],
|
||||
[22, 795.29],
|
||||
[23, 771.26],
|
||||
[24, 751.51],
|
||||
[25, 800.11],
|
||||
[26, 824.1],
|
||||
[27, 851.25],
|
||||
[28, 826.65],
|
||||
[29, 845.48],
|
||||
[30, 891.25],
|
||||
[31, 899.52],
|
||||
[32, 944.36],
|
||||
[33, 947.73],
|
||||
[34, 923.11],
|
||||
[35, 889.22],
|
||||
[36, 871.86],
|
||||
[37, 871.42],
|
||||
[38, 826.14],
|
||||
[39, 866.41],
|
||||
[40, 897.81],
|
||||
[41, 858.01],
|
||||
[42, 848.76],
|
||||
[43, 818.07],
|
||||
[44, 778.81],
|
||||
[45, 737.75],
|
||||
[46, 719.47],
|
||||
[47, 730.18],
|
||||
[48, 705.91],
|
||||
[49, 689.15],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Second series',
|
||||
points: [
|
||||
[0, 307.54],
|
||||
[1, 348.28],
|
||||
[2, 396.7],
|
||||
[3, 393.87],
|
||||
[4, 440.02],
|
||||
[5, 407.04],
|
||||
[6, 448.73],
|
||||
[7, 492.34],
|
||||
[8, 542.03],
|
||||
[9, 508.63],
|
||||
[10, 498.81],
|
||||
[11, 536.71],
|
||||
[12, 500.22],
|
||||
[13, 461.66],
|
||||
[14, 445.5],
|
||||
[15, 474.53],
|
||||
[16, 524.04],
|
||||
[17, 537.02],
|
||||
[18, 557.5],
|
||||
[19, 518.29],
|
||||
[20, 486.71],
|
||||
[21, 497.34],
|
||||
[22, 495.29],
|
||||
[23, 471.26],
|
||||
[24, 451.51],
|
||||
[25, 500.11],
|
||||
[26, 524.1],
|
||||
[27, 551.25],
|
||||
[28, 526.65],
|
||||
[29, 545.48],
|
||||
[30, 591.25],
|
||||
[31, 599.52],
|
||||
[32, 644.36],
|
||||
[33, 647.73],
|
||||
[34, 623.11],
|
||||
[35, 589.22],
|
||||
[36, 571.86],
|
||||
[37, 571.42],
|
||||
[38, 526.14],
|
||||
[39, 566.41],
|
||||
[40, 597.81],
|
||||
[41, 558.01],
|
||||
[42, 548.76],
|
||||
[43, 518.07],
|
||||
[44, 478.81],
|
||||
[45, 437.75],
|
||||
[46, 419.47],
|
||||
[47, 430.18],
|
||||
[48, 405.91],
|
||||
[49, 389.15],
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{LineChartTemplate.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={LineChart} />
|
||||
|
||||
## Usage
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="LineChart/__stories__/lineChart.stories.mdx" />
|
||||
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* It adds attributes to root element from passed attributes list
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {object} attributes list of attributes to be added on root element
|
||||
* @modifies {root}
|
||||
*
|
||||
*/
|
||||
const _addAttributes = (root, attributes = []) => {
|
||||
for (const attr of attributes) {
|
||||
const { key, value } = attr;
|
||||
|
||||
if (key && value) {
|
||||
if (key === 'class') {
|
||||
root.classed(value, true);
|
||||
} else {
|
||||
root.attr(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* It formats axis labels. It adds units if existing
|
||||
*
|
||||
* @param {Object} def
|
||||
* @param {string} def.label string representing axis label
|
||||
* @param {string} [def.unit] string representing axis unit
|
||||
* @return {string} formatted label
|
||||
*/
|
||||
const _formatAxisLabel = ({ label, unit }) => {
|
||||
let formattedLabel = label;
|
||||
|
||||
if (unit) {
|
||||
formattedLabel += `(${unit})`;
|
||||
}
|
||||
|
||||
return formattedLabel;
|
||||
};
|
||||
|
||||
/**
|
||||
* It appends chart svg element container to root element
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {number} width width of container
|
||||
* @param {number} height height of container
|
||||
* @param {number} translateX value to translate on axis x
|
||||
* @param {number} translateY value to translate on axis y
|
||||
* @modifies {root}
|
||||
*
|
||||
* @return {object} appended chart container (svg element)
|
||||
*/
|
||||
const _addChartContainer = (root, width, height, translateX, translateY) => {
|
||||
root.append('rect').attr('class', 'background').attr('width', width).attr('height', height);
|
||||
|
||||
return root
|
||||
.append('g')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.append('g')
|
||||
.attr('transform', 'translate(' + translateX + ',' + translateY + ')');
|
||||
};
|
||||
|
||||
const _addLegend = (root, width, height, posX, posY) => {
|
||||
const legendContainer = root
|
||||
.append('g')
|
||||
.attr('class', 'legend')
|
||||
.attr('transform', 'translate(' + posX + ',' + posY + ')');
|
||||
|
||||
legendContainer
|
||||
.append('rect')
|
||||
.attr('class', 'legend-background')
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
return legendContainer;
|
||||
};
|
||||
|
||||
const _setLegendLabels = (root, labels, colors, itemHeight, itemWidth, textEllipses) => {
|
||||
const yOffset = itemHeight / 2;
|
||||
const textLeft = 20;
|
||||
|
||||
// Add one dot in the legend for each
|
||||
root
|
||||
.selectAll('labelDots')
|
||||
.data(labels)
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('cx', 10)
|
||||
.attr('cy', (_d, i) => yOffset + i * itemHeight)
|
||||
.attr('r', 5)
|
||||
.style('fill', (_d, i) => colors[i]);
|
||||
|
||||
root
|
||||
.selectAll('labelText')
|
||||
.data(labels)
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('x', textLeft)
|
||||
.attr('y', (_d, i) => yOffset + i * itemHeight)
|
||||
.style('fill', (_d, i) => colors[i])
|
||||
.append('tspan')
|
||||
.style('alignment-baseline', 'middle')
|
||||
.text(d => d)
|
||||
.each(textEllipses(itemWidth - textLeft, 1));
|
||||
};
|
||||
|
||||
/**
|
||||
* It appends axis svg element to root element
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {TimecoursePointDef} axisDef definition of given axis
|
||||
* @param {number} posX position X to place content
|
||||
* @param {number} posY position Y to place content
|
||||
* @param {number} axisExtraAttrs list of extra attributes to be added on content
|
||||
* @param {function} axisBuilder callback function to create axis graphics
|
||||
* @param {boolean} showAxisLabels flag to display labels or not
|
||||
* @param {number} labelPosX position X to place label content
|
||||
* @param {number} labelPosY position Y to place label content
|
||||
* @param {object} labelExtraAttrs list of extra attributes to be added on label content
|
||||
* @modifies {root}
|
||||
*
|
||||
* @return {object} axis svg element
|
||||
*/
|
||||
const _addAxis = (
|
||||
root,
|
||||
axisDef,
|
||||
posX = 0,
|
||||
posY = 0,
|
||||
axisExtraAttrs = [],
|
||||
axisBuilder,
|
||||
showAxisLabels,
|
||||
labelPosX,
|
||||
labelPosY,
|
||||
labelExtraAttrs = [],
|
||||
showAxisGrid
|
||||
) => {
|
||||
const { type } = axisDef;
|
||||
const showAxisGridClass = showAxisGrid ? 'grid' : '';
|
||||
const translateValue = `${posX}, ${posY}`;
|
||||
const axisContent = root
|
||||
.append('g')
|
||||
.attr('class', `${type} ${showAxisGridClass} axis`)
|
||||
.attr('transform', `translate(${translateValue})`);
|
||||
|
||||
_addAttributes(axisContent, axisExtraAttrs);
|
||||
axisContent.call(axisBuilder());
|
||||
|
||||
if (showAxisLabels) {
|
||||
const label = _formatAxisLabel(axisDef);
|
||||
// text label for the x axis
|
||||
if (label) {
|
||||
const labelContent = root
|
||||
.append('text')
|
||||
.attr('class', 'label axis')
|
||||
.attr('x', labelPosX)
|
||||
.attr('y', labelPosY)
|
||||
.style('text-anchor', 'middle')
|
||||
.text(label);
|
||||
|
||||
_addAttributes(labelContent, labelExtraAttrs);
|
||||
}
|
||||
}
|
||||
|
||||
return axisContent;
|
||||
};
|
||||
|
||||
/**
|
||||
* It scales axis graphics
|
||||
*
|
||||
* @param {object} sourceGraphicsXAxis original d3 graphics to be proccessed (x Axis)
|
||||
* @param {object} sourceGraphicsYAxis original d3 graphics to be proccessed (y Axis)
|
||||
* @param {object} xAxisGenerator d3 X axis generator
|
||||
* @param {object} yAxisGenerator d3 Y axis generator
|
||||
* @param {object} newXAxisScale d3 continuos scale for X Axis
|
||||
* @param {object} newYAxisScale d3 continuos scale for Y Axis
|
||||
* @modifies {sourceGraphicsXAxis|sourceGraphicsYAxis}
|
||||
*/
|
||||
const _scaleAxisGraphics = (
|
||||
sourceGraphicsXAxis,
|
||||
sourceGraphicsYAxis,
|
||||
xAxisGenerator,
|
||||
yAxisGenerator,
|
||||
newXAxisScale,
|
||||
newYAxisScale
|
||||
) => {
|
||||
const newXAxis = xAxisGenerator.scale(newXAxisScale);
|
||||
const newYAxis = yAxisGenerator.scale(newYAxisScale);
|
||||
|
||||
sourceGraphicsXAxis.call(newXAxis);
|
||||
sourceGraphicsYAxis.call(newYAxis);
|
||||
};
|
||||
|
||||
/**
|
||||
* It appends background svg element to root element and also a clip path for chart boundaries
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {number} width content`s width
|
||||
* @param {number} height content`s height
|
||||
* @param {boolean} transparent background to be transparent or not
|
||||
* @param {number} offset content`s clip offset
|
||||
* @modifies {root}
|
||||
*
|
||||
*/
|
||||
const _addChartClipPath = (root, width, height, transparent = false, offset = 6) => {
|
||||
const translateOffset = -offset / 2;
|
||||
root
|
||||
.append('clipPath')
|
||||
.attr('id', 'clip')
|
||||
.append('rect')
|
||||
.attr('transform', `translate(${translateOffset}, ${translateOffset})`)
|
||||
.attr('width', width + offset)
|
||||
.attr('height', height + offset);
|
||||
|
||||
root
|
||||
.append('rect')
|
||||
.attr('class', 'background')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.classed('transparent', transparent);
|
||||
};
|
||||
|
||||
const _addSeries = (root, seriesIndex, style) => {
|
||||
return root
|
||||
.append('g')
|
||||
.attr('id', `series_${seriesIndex}`)
|
||||
.attr('class', 'series')
|
||||
.attr('stroke', style.color || '#ffffff');
|
||||
};
|
||||
|
||||
/**
|
||||
* It appends line chart svg element to root element
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {object} dataset dataset to tie to content
|
||||
* @param {string} dAttrValue path d attribute
|
||||
* @modifies {root}
|
||||
*
|
||||
* @return {object} svg line element line
|
||||
*/
|
||||
const _addLine = (root, dataset, dAttrValue) => {
|
||||
return root
|
||||
.append('path')
|
||||
.attr('clip-path', 'url(#clip)')
|
||||
.datum(dataset)
|
||||
.attr('class', 'line')
|
||||
.attr('d', dAttrValue);
|
||||
};
|
||||
|
||||
/**
|
||||
* It updates line chart svg element with new dataset and dAttrValue value
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {object} dataset dataset to tie to content
|
||||
* @param {string} dAttrValue path d attribute
|
||||
* @modifies {root}
|
||||
*
|
||||
*/
|
||||
const _updateLine = (root, dataset = [], dAttrValue) => {
|
||||
const line = root.select('.line');
|
||||
|
||||
if (dataset && dataset.length > 0) {
|
||||
line.datum(dataset);
|
||||
}
|
||||
|
||||
line.attr('d', dAttrValue);
|
||||
};
|
||||
|
||||
/**
|
||||
* It appends points svg element to root element
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {object} dataset dataset to tie to content
|
||||
* @param {number} cxAttrValue cx attribute
|
||||
* @param {number} cyAttrValue cy attribute
|
||||
* @modifies {root}
|
||||
*
|
||||
* @return {object} points svg element
|
||||
*
|
||||
*/
|
||||
const _addPoints = (root, dataset, cxAttrValue, cyAttrValue) => {
|
||||
return root
|
||||
.selectAll('.dot')
|
||||
.data(dataset)
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('id', (point, index) => {
|
||||
return `point-${index}`;
|
||||
})
|
||||
.attr('class', 'dot')
|
||||
.attr('clip-path', 'url(#clip)')
|
||||
.attr('cx', cxAttrValue)
|
||||
.attr('cy', cyAttrValue)
|
||||
.attr('r', 2);
|
||||
};
|
||||
|
||||
/**
|
||||
* It return points from root parent
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @return {object} points svg element
|
||||
*
|
||||
*/
|
||||
const _getPoints = root => {
|
||||
return root.selectAll('.dot');
|
||||
};
|
||||
|
||||
/**
|
||||
* It updates points svg element with new dataset, cxAttrValue and cyAttrValue
|
||||
*
|
||||
* @param {object} root svg element to be changed
|
||||
* @param {object} dataset dataset to tie to content
|
||||
* @param {number} cxAttrValue cx attribute
|
||||
* @param {number} cyAttrValue cy attribute
|
||||
* @modifies {root}
|
||||
*
|
||||
*/
|
||||
const _updatePoints = (root, dataset = [], cxAttrValue, cyAttrValue) => {
|
||||
const dot = root.selectAll('.dot');
|
||||
|
||||
dot.data(dataset).attr('cx', cxAttrValue).attr('cy', cyAttrValue);
|
||||
};
|
||||
|
||||
/**
|
||||
* It removes children nodes from root param.
|
||||
*
|
||||
* @param {object} root svg element to remove children content
|
||||
* @modifies {root}
|
||||
*
|
||||
*/
|
||||
const _removeChartContents = root => {
|
||||
root.selectAll('*').remove();
|
||||
};
|
||||
|
||||
const chart = {
|
||||
axis: {
|
||||
addNode: _addAxis,
|
||||
scaleGraphics: _scaleAxisGraphics,
|
||||
},
|
||||
series: {
|
||||
addNode: _addSeries,
|
||||
},
|
||||
lines: {
|
||||
addNode: _addLine,
|
||||
updateNode: _updateLine,
|
||||
},
|
||||
points: {
|
||||
addNode: _addPoints,
|
||||
updateNode: _updatePoints,
|
||||
getPoints: _getPoints,
|
||||
},
|
||||
background: {
|
||||
addNode: _addChartClipPath,
|
||||
},
|
||||
container: {
|
||||
addNode: _addChartContainer,
|
||||
},
|
||||
legend: {
|
||||
addNode: _addLegend,
|
||||
setLabels: _setLegendLabels,
|
||||
},
|
||||
removeContents: _removeChartContents,
|
||||
};
|
||||
|
||||
export default chart;
|
||||
@@ -0,0 +1,149 @@
|
||||
import * as d3Shape from 'd3-shape';
|
||||
import * as d3Zoom from 'd3-zoom';
|
||||
|
||||
import chart from './chart';
|
||||
|
||||
const d3ZoomNativeEvents = [
|
||||
'wheel.zoom',
|
||||
'mousedown.zoom',
|
||||
'dblclick.zoom',
|
||||
'touchstart.zoom',
|
||||
'touchmove.zoom',
|
||||
'touchend.zoom touchcancel.zoom',
|
||||
];
|
||||
/**
|
||||
* Object to override native zoom events.
|
||||
* It defines a specific callback for a event name.
|
||||
*
|
||||
* @type {Object<string, function>} zoomEvents <zoom d3 event name, callback method>
|
||||
*/
|
||||
const zoomEvents = {
|
||||
'dblclick.zoom': (root, zoom) => {
|
||||
_resetZoom(root, zoom);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* It resets given root
|
||||
*
|
||||
* @param {object} root svg element to be reset
|
||||
* @param {object} zoom d3 zoom object
|
||||
*
|
||||
* @modifies {root}
|
||||
*/
|
||||
const _resetZoom = (root, zoom) => {
|
||||
if (root) {
|
||||
root.transition().duration(750).call(zoom.transform, d3Zoom.zoomIdentity);
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_MAX_ZOOM_SCALE = 20;
|
||||
|
||||
const _zoom = d3Zoom.zoom().scaleExtent([1, DEFAULT_MAX_ZOOM_SCALE]);
|
||||
|
||||
const _bindZoom = (
|
||||
root,
|
||||
gX,
|
||||
gY,
|
||||
xAxisScale,
|
||||
yAxisScale,
|
||||
xAxisGenerator,
|
||||
yAxisGenerator,
|
||||
parseXPoint,
|
||||
parseYPoint,
|
||||
datasets
|
||||
) => {
|
||||
_zoom.on('zoom', _zoomListener); // .filter(zoomEventsFilter);
|
||||
|
||||
_removeListeners();
|
||||
_setListeners();
|
||||
|
||||
function _removeListeners() {
|
||||
for (const eventName of d3ZoomNativeEvents) {
|
||||
root.on(eventName, null);
|
||||
}
|
||||
}
|
||||
|
||||
function _setListeners() {
|
||||
const keys = Object.keys(zoomEvents);
|
||||
|
||||
for (const key of keys) {
|
||||
root.call(_zoom).on(key, zoomEvents[key].bind(undefined, root, _zoom));
|
||||
}
|
||||
}
|
||||
|
||||
function _zoomListener(event) {
|
||||
const transformedXAxisScale = event.transform.rescaleX(xAxisScale);
|
||||
const transformedYAxisScale = event.transform.rescaleY(yAxisScale);
|
||||
|
||||
chart.axis.scaleGraphics(
|
||||
gX,
|
||||
gY,
|
||||
xAxisGenerator,
|
||||
yAxisGenerator,
|
||||
transformedXAxisScale,
|
||||
transformedYAxisScale
|
||||
);
|
||||
|
||||
datasets.forEach((dataset, seriesIndex) => {
|
||||
const seriesContainer = root.select(`#series_${seriesIndex}`);
|
||||
// create line
|
||||
const line = d3Shape
|
||||
.line()
|
||||
.x(parseXPoint(transformedXAxisScale))
|
||||
.y(parseYPoint(transformedYAxisScale));
|
||||
|
||||
chart.lines.updateNode(seriesContainer, dataset, line);
|
||||
|
||||
chart.points.updateNode(
|
||||
seriesContainer,
|
||||
dataset,
|
||||
parseXPoint(transformedXAxisScale),
|
||||
parseYPoint(transformedYAxisScale)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return _zoom;
|
||||
};
|
||||
|
||||
const bindMouseEvents = (
|
||||
root,
|
||||
gX,
|
||||
gY,
|
||||
xAxisScale,
|
||||
yAxisScale,
|
||||
xAxisGenerator,
|
||||
yAxisGenerator,
|
||||
parseXPoint,
|
||||
parseYPoint,
|
||||
datasets
|
||||
) => {
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
_bindZoom(
|
||||
root,
|
||||
gX,
|
||||
gY,
|
||||
xAxisScale,
|
||||
yAxisScale,
|
||||
xAxisGenerator,
|
||||
yAxisGenerator,
|
||||
parseXPoint,
|
||||
parseYPoint,
|
||||
datasets
|
||||
);
|
||||
};
|
||||
|
||||
const events = {
|
||||
bindMouseEvents,
|
||||
external: {
|
||||
resetZoom: root => {
|
||||
_resetZoom(root, _zoom);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default events;
|
||||
@@ -0,0 +1,3 @@
|
||||
import * as lineChart from './lineChart';
|
||||
|
||||
export { lineChart };
|
||||
@@ -0,0 +1,272 @@
|
||||
import * as d3Array from 'd3-array';
|
||||
import * as d3Axis from 'd3-axis';
|
||||
import * as d3Scale from 'd3-scale';
|
||||
import * as d3ScaleChromatic from 'd3-scale-chromatic';
|
||||
import * as d3Selection from 'd3-selection';
|
||||
import * as d3Shape from 'd3-shape';
|
||||
|
||||
import chart from './chart';
|
||||
import events from './events';
|
||||
|
||||
const {
|
||||
external: { resetZoom },
|
||||
} = events;
|
||||
|
||||
/**
|
||||
* @typedef TimecoursePoint It defines a tuple of timecourse value (time X intensity)
|
||||
* @type {array}
|
||||
* @property {number} 0 indicates x|y value on the pair (x,y)
|
||||
* @property {number} 1 indicates x|y value on the pair (x,y)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef TimecoursePointDef It defines the shape of a given TimecoursePoint Axis
|
||||
* @type {object}
|
||||
* @property {string} label label for given TimecoursePoint Axis
|
||||
* @property {string} [unit] unit for given TimecoursePoint Axis
|
||||
* @property {string} type defines the type of given TimecoursePoint Axis
|
||||
* @property {number} indexRef refers the index into given TimecoursePoint for the current Axis Definition
|
||||
*/
|
||||
|
||||
/**
|
||||
* It gets max value of an array, considering value of param index
|
||||
*
|
||||
* @param {TimecoursePoint[]} array array of items to be evaluated
|
||||
* @param {number} index index of each array`s item to be evaluated
|
||||
* @return {any} max value
|
||||
*/
|
||||
function _getMaxValue(array, index) {
|
||||
return d3Array.max(array, arrayItem => {
|
||||
return arrayItem[index];
|
||||
});
|
||||
}
|
||||
|
||||
function _getMinValue(array, index) {
|
||||
return d3Array.min(array, arrayItem => {
|
||||
return arrayItem[index];
|
||||
});
|
||||
}
|
||||
|
||||
const LEGEND = { width: 100, margin: 10 };
|
||||
const MARGIN = { top: 20, right: 20, bottom: 50, left: 50 };
|
||||
|
||||
function _createAxisScale(domainBottom, domainUpper, rangeBottom, rangeUpper) {
|
||||
return d3Scale
|
||||
.scaleLinear()
|
||||
.domain([domainBottom, domainUpper * 1.05])
|
||||
.range([rangeBottom, rangeUpper]);
|
||||
}
|
||||
|
||||
const _getSeriesColor = series => {
|
||||
const seriesLabels = series.reduce((labels, series) => [...labels, series.label], []);
|
||||
|
||||
return d3Scale.scaleOrdinal().domain(seriesLabels).range(d3ScaleChromatic.schemeSet2);
|
||||
};
|
||||
|
||||
const _updateSeriesColors = series => {
|
||||
const seriesColor = _getSeriesColor(series);
|
||||
|
||||
return series.map(series => ({
|
||||
...series,
|
||||
color: series.color ?? seriesColor(series.label),
|
||||
}));
|
||||
};
|
||||
|
||||
const _textEllipses = (width, padding = 0) => {
|
||||
return function (...args) {
|
||||
const self = d3Selection.select(this);
|
||||
let textLength = self.node().getComputedTextLength();
|
||||
let text = self.text();
|
||||
|
||||
while (textLength > width - 2 * padding && text.length > 0) {
|
||||
text = text.slice(0, -1);
|
||||
self.text(text + '...');
|
||||
textLength = self.node().getComputedTextLength();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const _addLegend = (root, series, chartWidth, chartHeight, legendWidth) => {
|
||||
const legendItemHeight = 25;
|
||||
const legendHeight = legendItemHeight * series.length;
|
||||
|
||||
const legendContainer = chart.legend.addNode(
|
||||
root,
|
||||
legendWidth,
|
||||
legendHeight,
|
||||
chartWidth + LEGEND.margin,
|
||||
chartHeight / 2 - legendHeight / 2
|
||||
);
|
||||
|
||||
const seriesLabels = series.reduce((labels, series) => [...labels, series.label], []);
|
||||
|
||||
const seriesColors = series.reduce((colors, series) => [...colors, series.color], []);
|
||||
|
||||
chart.legend.setLabels(
|
||||
legendContainer,
|
||||
seriesLabels,
|
||||
seriesColors,
|
||||
legendItemHeight,
|
||||
legendWidth,
|
||||
_textEllipses
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* It creates a svg chart containing lines, dots, axis, labels
|
||||
*
|
||||
* @param {object} d3SVGRef svg content reference to append chart
|
||||
* @param {Object<string, TimecoursePointDef>} axis definition of axis
|
||||
* @param {object} points list of points to be created
|
||||
* @param {number} width width for whole content including lines, dots, axis, labels
|
||||
* @param {number} height height for whole content including lines, dots, axis, labels
|
||||
* @param {boolean} showAxisLabels flag to display labels or not
|
||||
*
|
||||
* @modifies {d3SVGRef}
|
||||
*/
|
||||
const addLineChartNode = ({
|
||||
d3SVGRef,
|
||||
axis,
|
||||
series,
|
||||
width,
|
||||
height,
|
||||
legendWidth = LEGEND.width,
|
||||
showAxisLabels = true,
|
||||
showAxisGrid = false,
|
||||
showLegend = false,
|
||||
transparentChartBackground = false,
|
||||
}) => {
|
||||
const marginRight = showLegend ? legendWidth + 2 * LEGEND.margin : MARGIN.right;
|
||||
const _width = width - MARGIN.left - marginRight;
|
||||
const _height = height - MARGIN.top - MARGIN.bottom;
|
||||
const { x: XAxis, y: YAxis } = axis;
|
||||
|
||||
series = _updateSeriesColors(series);
|
||||
|
||||
let maxX = -Infinity;
|
||||
let minX = Infinity;
|
||||
let maxY = -Infinity;
|
||||
let minY = Infinity;
|
||||
|
||||
series.forEach(currentSeries => {
|
||||
minX = Math.min(minX, _getMinValue(currentSeries.points, XAxis.indexRef));
|
||||
maxX = Math.max(maxX, _getMaxValue(currentSeries.points, XAxis.indexRef));
|
||||
minY = Math.min(minY, _getMinValue(currentSeries.points, YAxis.indexRef));
|
||||
maxY = Math.max(maxY, _getMaxValue(currentSeries.points, YAxis.indexRef));
|
||||
});
|
||||
|
||||
minX = axis?.x?.range?.min ?? minX;
|
||||
maxX = axis?.x?.range?.max ?? maxX;
|
||||
minY = axis?.y?.range?.min ?? minY;
|
||||
maxY = axis?.y?.range?.max ?? maxY;
|
||||
|
||||
const xAxisScale = _createAxisScale(minX, maxX, 0, _width);
|
||||
const yAxisScale = _createAxisScale(minY, maxY, _height, 0);
|
||||
|
||||
const parseXPoint = axisScale => point => {
|
||||
return (axisScale || xAxisScale)(point.x);
|
||||
};
|
||||
|
||||
const parseYPoint = axisScale => point => {
|
||||
return (axisScale || yAxisScale)(point.y);
|
||||
};
|
||||
|
||||
// Remove old D3 elements
|
||||
chart.removeContents(d3SVGRef);
|
||||
|
||||
const chartWrapper = chart.container.addNode(d3SVGRef, width, height, MARGIN.left, MARGIN.top);
|
||||
|
||||
// add background
|
||||
chart.background.addNode(chartWrapper, _width, _height, transparentChartBackground);
|
||||
|
||||
// call the x axis in a group tag
|
||||
const xAxisGenerator = d3Axis.axisBottom(xAxisScale);
|
||||
|
||||
if (showAxisGrid) {
|
||||
xAxisGenerator.tickSize(-_height).tickPadding(10);
|
||||
}
|
||||
const gXAxis = chart.axis.addNode(
|
||||
chartWrapper,
|
||||
XAxis,
|
||||
undefined,
|
||||
_height,
|
||||
undefined,
|
||||
() => xAxisGenerator,
|
||||
showAxisLabels,
|
||||
_width / 2,
|
||||
_height + MARGIN.bottom / 2 + 10,
|
||||
undefined,
|
||||
showAxisGrid
|
||||
);
|
||||
const yAxisGenerator = d3Axis.axisLeft(yAxisScale);
|
||||
|
||||
if (showAxisGrid) {
|
||||
yAxisGenerator.tickSize(-_width).tickPadding(10);
|
||||
}
|
||||
// add y axis
|
||||
const gYAxis = chart.axis.addNode(
|
||||
chartWrapper,
|
||||
YAxis,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
() => yAxisGenerator,
|
||||
showAxisLabels,
|
||||
0 - _height / 2,
|
||||
0 - MARGIN.left,
|
||||
[
|
||||
{ key: 'transform', value: 'rotate(-90)' },
|
||||
{ key: 'dy', value: '1em' },
|
||||
],
|
||||
showAxisGrid
|
||||
);
|
||||
|
||||
const datasets = [];
|
||||
|
||||
series.forEach((currentSeries, seriesIndex) => {
|
||||
const { points } = currentSeries;
|
||||
const line = d3Shape.line().x(parseXPoint(xAxisScale)).y(parseYPoint(yAxisScale));
|
||||
|
||||
const dataset = points.map((point, pointIndex) => {
|
||||
return { x: point[0], y: point[1], seriesIndex, pointIndex };
|
||||
});
|
||||
|
||||
const seriesContainer = chart.series.addNode(chartWrapper, seriesIndex, {
|
||||
color: currentSeries.color || '#00ff00',
|
||||
});
|
||||
|
||||
chart.lines.addNode(seriesContainer, dataset, line);
|
||||
|
||||
// add chart points
|
||||
chart.points.addNode(
|
||||
seriesContainer,
|
||||
dataset,
|
||||
parseXPoint(xAxisScale),
|
||||
parseYPoint(yAxisScale)
|
||||
);
|
||||
|
||||
datasets.push(dataset);
|
||||
});
|
||||
|
||||
if (showLegend) {
|
||||
_addLegend(chartWrapper, series, _width, _height, legendWidth);
|
||||
}
|
||||
|
||||
// bind events
|
||||
events.bindMouseEvents(
|
||||
chartWrapper,
|
||||
gXAxis,
|
||||
gYAxis,
|
||||
xAxisScale,
|
||||
yAxisScale,
|
||||
xAxisGenerator,
|
||||
yAxisGenerator,
|
||||
parseXPoint,
|
||||
parseYPoint,
|
||||
datasets
|
||||
);
|
||||
|
||||
return chartWrapper;
|
||||
};
|
||||
|
||||
export { addLineChartNode, resetZoom };
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './LineChart';
|
||||
Reference in new issue
Block a user