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:
authored and GitHub committed 2024-04-09 23:57:34 -04:00
1 parent dc0b183dd4
commit d57e8bc157
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';