constructor(props) { super(props); this.x = scaleTime(); this.y = scaleLinear(); this.x2 = scaleTime(); this.y2 = scaleLinear(); this.margin = { top: 0, right: 40, bottom: 100, left: 0 }; this.margin2 = { right: 10, bottom: 20, left: 0 }; this.state = { brush: false, selection: null, cursorData: null, cursorX: 0, cursorVisible: false, }; this.getDefaultFocusDomain = this.getDefaultFocusDomain.bind(this); this.getCursorState = this.getCursorState.bind(this); this.handleWheel = this.handleWheel.bind(this); this.handleMouseMove = this.handleMouseMove.bind(this); this.handleMouseOut = this.handleMouseOut.bind(this); this.handleBrushMount = this.handleBrushMount.bind(this); this.handleBrushStart = this.handleBrushStart.bind(this); this.handleBrush = this.handleBrush.bind(this); this.handleBrushEnd = this.handleBrushEnd.bind(this); this.updateContextDomains = this.updateContextDomains.bind(this); this.updateFocusDomain = this.updateFocusDomain.bind(this); this.updateCursor = this.updateCursor.bind(this); this.updateDimension = this.updateDimension.bind(this); this.updateD3(props); }
describe('without discontinuities', () => { var range = [0, 100]; var start = new Date(2015, 0, 18); // sunday var end = new Date(2015, 0, 28); // wednesday var referenceScale = scaleTime() .domain([start, end]) .range(range); var dateTime = discontinuous(scaleTime()) .domain([start, end]) .range(range); it('should match the scale functionality of a d3 time scale', () => { var date = new Date(2015, 0, 19); expect(dateTime(date)).toEqual(referenceScale(date)); date = new Date(2015, 0, 25); expect(dateTime(date)).toEqual(referenceScale(date)); expect(dateTime(start)).toEqual(referenceScale(start)); expect(dateTime(end)).toEqual(referenceScale(end)); }); it('should match the invert functionality of a d3 time scale', () => { expect(dateTime.invert(0)).toEqual(referenceScale.invert(0)); expect(dateTime.invert(50)).toEqual(referenceScale.invert(50)); expect(dateTime.invert(100)).toEqual(referenceScale.invert(100)); }); });
function setUpCommonTimeAxis() { sharedTimeScale = scaleTime().domain(timelineSpan).range([0, timelineSize.width]); sharedTimeScale0 = scaleTime().domain(timelineSpan).range([0, timelineSize.width]); const customTimeFormat = makeTimeTickFormat(".%L", ":%S", "%_I:%M", "%_I %p", "%b %_d", "%b %_d", "%b", "%Y"), customTimeFormatDayNames = makeTimeTickFormat(" ", " ", " ", " ", "%a", "%a", "%a", " "); timelineXAxisMain = makeTimelineAxis(sharedTimeScale, customTimeFormat, -timelineSize.height, 6); timelineXAxisDayNames = makeTimelineAxis(sharedTimeScale, customTimeFormatDayNames, -timelineSize.height, 18); timelineXAxisDays = makeTimelineAxis(sharedTimeScale, "", -timelineSize.height, 0).ticks(timeDay.every(1)); timelineXAxisWeeks = makeTimelineAxis(sharedTimeScale, "", -timelineSize.height, 0).ticks(timeWeek.every(1)); timelineXAxisHidden = makeTimelineAxis(sharedTimeScale, "", 0, 0).ticks(timeYear.every(1)); }
function addFeed(feed) { dataFeeds.push(feed); const feedIndex = Object.keys(feedIndices).length, newTimelineSpan = extent(feed.data, d => d.timestamp), yAxis = select("#timelineRootSVG").append("g"); select('#timelineInner').append('g').attr('id', 'data' + feed.feedInfo.feedId); select('#timelineInner').append('g').attr('id', 'trend' + feed.feedInfo.feedId); select('#timelineInner').append('g').attr('id', 'baseLine' + feed.feedInfo.feedId); select('#timelineOuter').append('g') .attr('id', 'label' + feed.feedInfo.feedId) .attr('pointer-events', 'auto'); feedIndices[feed.feedInfo.feedId] = feedIndex; timelineSpan = dataFeeds.length === 0 ? newTimelineSpan : [Math.min(timelineSpan[0], newTimelineSpan[0]), Math.max(timelineSpan[1], newTimelineSpan[1])]; sharedTimeScale0 = scaleTime().domain(timelineSpan).range([0, timelineSize.width]); resetTimelineSpan(timelineSpan); feedHeight = (timelineSize.height - feedPadding) / Object.keys(feedIndices).length - feedPadding; yAxis .attr("id", "yAxis" + feed.feedInfo.feedId) .attr("pointer-events", "none"); yAxis.append("g") .attr("class", "axis-y"); updateFeed(feed); }
it('should clamp the values supplied', () => { var dateTime = discontinuous(scaleTime()) .discontinuityProvider(skipWeekends()) .domain([start, end]); expect(dateTime.domain()[0]).toEqual(startOfWeek); expect(dateTime.domain()[1]).toEqual(endOfWeek); });
constructor(props) { super(props); this.x = scaleTime(); this.y = scaleLinear(); this.margin = { top: 0, right: 0, bottom: 60, left: 0 }; this.getDefaultDomain = this.getDefaultDomain.bind(this); this.updateDomains = this.updateDomains.bind(this); this.updateDimension = this.updateDimension.bind(this); this.updateD3(props); }
xScale: computed('data.[]', 'xProp', 'timeseries', 'yAxisOffset', function() { const xProp = this.xProp; const scale = this.timeseries ? d3Scale.scaleTime() : d3Scale.scaleLinear(); const data = this.data; const domain = data.length ? d3Array.extent(this.data, d => d[xProp]) : [0, 1]; scale.rangeRound([10, this.yAxisOffset]).domain(domain); return scale; }),
buildChartScales() { const tasks = this.props.tasks; const dateElements = this.getDateElements(tasks); const maxDate = max(dateElements); const minDate = min(dateElements); this.xScale = scaleTime().domain([minDate,maxDate]).range([0,this.width]); this.yScale = scaleLinear().domain([0,tasks.length]).range([0,this.height]); }
it('should support arguments being passed to ticks', () => { var start = new Date(2015, 0, 9); // friday var end = new Date(2015, 0, 12); // monday var dateTime = discontinuous(scaleTime()) .discontinuityProvider(skipWeekends()) .domain([start, end]); var ticks = dateTime.ticks(100); expect(ticks.length).toEqual(25); });
it('should ensure ticks are not within discontinuities', () => { var start = new Date(2015, 0, 9); // friday var end = new Date(2015, 0, 12); // monday var dateTime = discontinuous(scaleTime()) .discontinuityProvider(skipWeekends()) .domain([start, end]); var ticks = dateTime.ticks(); expect(ticks.length).toEqual(5); });
/** * Creates the x, y and color scales of the chart * @private */ function buildScales() { xScale = d3Scale.scaleTime() .domain(d3Array.extent(dataByDate, ({date}) => date)) .rangeRound([0, chartWidth]); yScale = d3Scale.scaleLinear() .domain([0, getMaxValueByDate()]) .rangeRound([chartHeight, 0]) .nice(); categoryColorMap = order.reduce((memo, topic, index) => ( assign({}, memo, {[topic]: colorSchema[index]}) ), {}); }
xScale: computed('data.[]', 'xProp', 'timeseries', 'yAxisOffset', function() { const xProp = this.xProp; const scale = this.timeseries ? d3Scale.scaleTime() : d3Scale.scaleLinear(); const data = this.data; const [low, high] = d3Array.extent(data, d => d[xProp]); const minLow = moment(high) .subtract(5, 'minutes') .toDate(); const extent = data.length ? [Math.min(low, minLow), high] : [minLow, new Date()]; scale.rangeRound([10, this.yAxisOffset]).domain(extent); return scale; }),
it('should support copy', () => { var start = new Date(2015, 0, 8); // thursday var end = new Date(2015, 0, 15); // thursday var dateTime = discontinuous(scaleTime()) .discontinuityProvider(skipWeekends()) .range([0, 100]) .domain([start, end]); var clone = dateTime.copy(); expect(clone.discontinuityProvider()).toEqual(dateTime.discontinuityProvider()); expect(clone.range()[0]).toEqual(0); expect(clone.range()[1]).toEqual(100); expect(clone.domain()[0]).toEqual(start); expect(clone.domain()[1]).toEqual(end); });
AreaChartStackedComponent.prototype.getXScale = function (domain, width) { var scale; if (this.scaleType === 'time') { scale = scaleTime(); } else if (this.scaleType === 'linear') { scale = scaleLinear(); } else if (this.scaleType === 'ordinal') { scale = scalePoint() .padding(0.1); } scale .range([0, width]) .domain(domain); return this.roundDomains ? scale.nice() : scale; };
PolarChartComponent.prototype.getXScale = function (domain, width) { switch (this.scaleType) { case 'time': return scaleTime() .range([0, width]) .domain(domain); case 'linear': var scale = scaleLinear() .range([0, width]) .domain(domain); return this.roundDomains ? scale.nice() : scale; default: return scalePoint() .range([0, width - twoPI / domain.length]) .padding(0) .domain(domain); } };
render() { let { data, type, width, ratio } = this.props; return ( <div> <FormattedMessage {...messages.header} /> <ChartCanvas ratio={ratio} width={width} height={400} margin={{ left: 50, right: 50, top: 10, bottom: 30 }} seriesName="MSFT" data={data} type={type} xAccessor={d => d.date} xScale={scaleTime()} xExtents={[new Date(2011, 0, 1), new Date(2013, 0, 2)]}> <Chart id={0} yExtents={d => d.close}> <XAxis axisAt="bottom" orient="bottom" ticks={6}/> <YAxis axisAt="left" orient="left"/> <AreaSeries yAccessor={(d) => d.close}/> </Chart> </ChartCanvas> </div> ); }
const PatientViewBannerRiskChart = ({ riskAssessments, onClickHandler=function(){}}) => { // This is > 1 because if there is only one assessment a chart doesn't make a whole lot of sense. if (riskAssessments && riskAssessments.length > 1) { riskAssessments = riskAssessments.sort((a,b) => new Date(a.datetime) - new Date(b.datetime)); let timeScale = scaleTime() .domain(extent(riskAssessments, (r) => new Date(r.datetime))) .range([1,99]); let riskScale = scaleLinear().domain([4,0]).range([1,9]); let areaGenerator = area().y0(100); // Define a function to project the riskAssessments into the svg's coordinate system // This will make building diagram easier // Note ,if you need more data from the riskAssessments the indexes line up let projection = (d) => [timeScale(new Date(d.datetime)), riskScale(d.value)]; let hitTargets = voronoi().extent([[-1, -1], [101, 21]]); let projectedData = riskAssessments.map(projection); let hitTargetPolys = hitTargets.polygons(projectedData); return ( <div className='patient-view-banner-risk-chart'> <svg viewBox='0 0 100 10' width='100%' height='100%'> <path d={areaGenerator(projectedData)} className='path'/> {projectedData.map((ra, i) => <g key={i}> <circle cx={ra[0]} cy={ra[1]} r={0.6} className='data-point'/> <path d={line()(hitTargetPolys[i])} opacity={0} onClick={() => onClickHandler(i)} className='click-target'/> </g> )} </svg> </div> ); } return ( <div> </div> ); };
export function getScale(domain, range, scaleType, roundDomains) { var scale; if (scaleType === 'time') { scale = scaleTime() .range(range) .domain(domain); } else if (scaleType === 'linear') { scale = scaleLinear() .range(range) .domain(domain); if (roundDomains) { scale = scale.nice(); } } else if (scaleType === 'ordinal') { scale = scalePoint() .range([range[0], range[1]]) .domain(domain); } return scale; }
render() { const { series, height = 500, width = 800 } = this.props; const x = scaleTime().range([0, width]); const y = scaleLinear().range([0, height]); const xAxis = axisBottom(x); const yAxis = axisLeft(y); const path = line() .x(d => x(d.time)) .y(d => y(d.price)); x.domain(extent(series, d => d.time)); y.domain(extent(series, d => d.price)); return ( <svg width={width} height={height}> <g className="chart"> <Line line={path} data={series}/> <Axis axis={yAxis}/> <Axis axis={xAxis}/> </g> </svg> ); }
constructor(props) { super(props); const { ticks, lowestPrice, highestPrice, tradingHours } = data; this.timeScale = d3Scale .scaleTime() .domain(tradingHours.map( t => t * 1000)) .range([0, defaultWidth]); this.priceScale = d3Scale .scaleLinear() .domain([lowestPrice, highestPrice]) // reverse bacause the origin point of d3 svg is top left .range([0, defaultStockChartHeight].reverse()); this.volumes = ticks.map(t => t.volume); this.volumeScale = d3Scale .scaleLinear() .domain([Math.min(...this.volumes), Math.max(...this.volumes)]) .range([0, defaultVolumeChartHeight]); const chartPercent = (ticks[ticks.length - 1].time - tradingHours[0]) / (tradingHours[1] - tradingHours[0]); this.barWidth = chartPercent * defaultWidth / this.volumes.length; }
const viewData = (model, address) => { const {series, width, height, tooltipWidth, scrubber, xhairAt, recipeStart, recipeEnd, markers} = model; const lines = series.lines; // Read out series class into array. const extentX = [series.min, series.max]; const scrubberMaxX = width - 12; const scrubberX = clamp(scrubber.coords[0], 0, scrubberMaxX); const isDragging = scrubber.isDragging; // Calculate dimensions // We need to have one row per group of 2 lines const nTooltipRow = Math.floor(lines.length / 2); const tooltipHeight = (nTooltipRow * READOUT_HEIGHT) + (TOOLTIP_PADDING * 2); const plotWidth = calcPlotWidth(extentX); const plotHeight = calcPlotHeight(height, tooltipHeight); const svgHeight = calcSvgHeight(height); const tickTop = calcXhairTickTop(height, tooltipHeight); // Calculate scales const x = scaleTime() .domain(extentX) .range([0, plotWidth]); const xhairRatioToXhairX = scaleLinear() .domain(RATIO_DOMAIN) .range([0, width]) .clamp(true); const scrubberToPlotX = scaleLinear() .domain([0, scrubberMaxX]) // Translate up to the point that the right side of the plot is adjacent // to the right side of the viewport. .range([0, plotWidth - width]) .clamp(true); const plotX = scrubberToPlotX(scrubberX); const xhairX = xhairRatioToXhairX(xhairAt); const tooltipX = calcTooltipX(xhairX, width, tooltipWidth); // Calculate xhair absolute position by adding crosshair position in viewport // to plot offset. Then use absolute coord in chart viewport to get inverse // value (time) under xhair. const xhairTime = x.invert(plotX + xhairX); const children = lines.map(line => viewLine(line, address, x, plotHeight)); const axis = renderAxis(x, svgHeight); children.push(axis); const userMarkers = renderUserMarkers(markers, x, svgHeight); children.push(userMarkers); if (recipeStart) { const recipeStartMarker = renderAxisMarker( x(recipeStart), svgHeight, localize('Recipe Started') ); children.push(recipeStartMarker); } if (recipeEnd) { const recipeEndMarker = renderAxisMarker( x(recipeEnd), svgHeight, localize('Recipe Ended') ); children.push(recipeEndMarker); } const chartSvg = svg({ width: plotWidth, height: svgHeight, className: 'chart-svg', style: { // Translate SVG to move the visible portion of the plot in response // to scrubber. transform: translateXY(-1 * plotX, 0) } }, children); const readouts = series .asGroups() .map(group => renderReadout(group, xhairTime)); return html.div({ className: 'chart split-view-content', onMouseMove: event => { const [mouseX, mouseY] = calcRelativeMousePos( event.currentTarget, event.clientX, event.clientY ); const xhairAt = xhairRatioToXhairX.invert(mouseX); address(MoveXhair(xhairAt)); }, onTouchStart: event => { // Prevent from becoming a click event. event.preventDefault(); }, onTouchMove: event => { event.preventDefault(); const changedTouches = event.changedTouches; if (changedTouches.length) { // @TODO it might be better to find the common midpoint between multiple // touches if touches > 1. const touch = changedTouches.item(0); const coords = calcRelativeMousePos( event.currentTarget, touch.clientX, touch.clientY ); const xhairAt = xhairRatioToXhairX.invert(coords[0]); address(MoveXhair(xhairAt)); } }, onResize: onWindow(address, () => { return Resize( calcChartWidth(window.innerWidth), calcChartHeight(window.innerHeight) ); }), style: { width: px(width), height: px(height) } }, [ chartSvg, html.div({ className: 'chart-xhair', style: { transform: translateXY(xhairX, 0) } }), html.div({ className: 'chart-xhair--tick', style: { transform: translateXY(xhairX, tickTop) } }), html.div({ className: 'chart-tooltip', style: { width: px(tooltipWidth), height: px(tooltipHeight), transform: translateXY(tooltipX, 0) } }, [ html.div({ className: 'chart-timestamp' }, [ html.div({ className: 'chart-timestamp--time' }, [ // Convert seconds to ms for formatTime formatTime(xhairTime) ]), html.div({ className: 'chart-timestamp--day' }, [ // Convert seconds to ms for formatTime formatDay(xhairTime) ]), ]), html.div({ className: 'chart-readouts' }, readouts) ]), html.div({ className: classed({ 'chart-scrubber': true, 'chart-scrubber--active': isDragging }), onMouseDown: onScrubberMouseDown(address), onMouseMove: onScrubberMouseMove(address), onMouseUp: onScrubberMouseUp(address), onTouchStart: onScrubberTouchStart(address), onTouchMove: onScrubberTouchMove(address), onTouchEnd: onScrubberTouchEnd(address) }, [ html.div({ className: 'chart-scrubber--backing' }), html.div({ className: 'chart-progress', style: { width: px(scrubberX) } }), html.div({ className: classed({ 'chart-handle': true, 'chart-handle--dragging': isDragging }), style: { transform: translateXY(scrubberX, 0) } }, [ html.div({ className: 'chart-handle--cap' }), html.div({ className: 'chart-handle--line' }) ]) ]) ]); }
render() { const { ticks, lowestPrice, highestPrice, tradingHours } = data; const tickCounts = 3; const timeScale = d3Scale .scaleTime() .domain(tradingHours.map( t => t * 1000)) .range([0, deviceWidth]); const priceScale = d3Scale .scaleLinear() .domain([lowestPrice, highestPrice]) .range([0, defaultStockChartHeight].reverse()); const lineFunction = d3Shape .line() .x(d => timeScale(d.time * 1000)) .y(d => priceScale(d.price)); const areaFunction = d3Shape .area() .x(d => timeScale(d.time * 1000)) .y(d => priceScale(d.price)) .y1(() => priceScale(lowestPrice)); const priceTicks = d3Array.ticks(lowestPrice, highestPrice, tickCounts); const adjustPriceTicks = this.getExclusiveTicks(lowestPrice, highestPrice, tickCounts); const adjustPriceScale = d3Scale .scaleLinear() .domain([adjustPriceTicks[0], adjustPriceTicks[adjustPriceTicks.length - 1]]) .range([0, defaultStockChartHeight].reverse()); const timeTicks = timeScale.ticks(tickCounts); return ( <ScrollView style={styles.container}> <T heading>Challenge</T> <T>1. ticks should be beatuiful number (multiply by 2, 5, 10)</T> <T>2. the interval of grid line should be equally distributed</T> <Image source={{ uri: 'https://raw.githubusercontent.com/chunghe/React-Native-Stock-Chart/1478e64a8494d56ff2baf4518261c7c84e67916f/app/assets/chart.png' }} style={{ width: null, height: 300 }} resizeMode="contain" /> <Image source={{ uri: 'https://raw.githubusercontent.com/chunghe/React-Native-Stock-Chart/1478e64a8494d56ff2baf4518261c7c84e67916f/app/assets/better.png' }} style={{ width: null, height: 300 }} resizeMode="contain" /> <T heading>d3Array.ticks(start, end, count)</T> <T>Returns an array of approximately count + 1 uniformly-spaced, nicely-rounded values between start and stop (inclusive). Each value is a power of ten multiplied by 1, 2 or 5. See also tickStep and linear.ticks.</T> <T>Ticks are inclusive in the sense that they may include the specified start and stop values if (and only if) they are exact, nicely-rounded values consistent with the inferred step. More formally, each returned tick t satisfies start ≤ t and t ≤ stop.</T> <T>highest price: {lowestPrice}, lowest price: {highestPrice}, generating {tickCounts} points: {`[${priceTicks.join(', ')}]`}</T> <Code> {` import * as d3Array from 'd3-array'; const priceTicks = d3Array.ticks( lowestPrice, highestPrice, tickCounts ); `} </Code> <Svg height={defaultStockChartHeight} width={deviceWidth}> <Path d={areaFunction(ticks)} fill="rgb(209, 237, 255, 0.85)" /> <Path d={lineFunction(ticks)} stroke="rgb(0, 102, 221, 0.75)" fill="none" /> { priceTicks.map(t => { return ( <G key={t}> <Text x={deviceWidth - 5} y={priceScale(t)} textAnchor="end" fill="#999" key={t} > {`${t}`} </Text> <Path d={`M0 ${priceScale(t)} ${deviceWidth} ${priceScale(t)}`} stroke="#999" strokeDasharray="2,2" /> </G> ); }) } </Svg> <Code> {` { priceTicks.map(t => { return ( <G key={t}> <Text x={deviceWidth - 5} y={priceScale(t)} textAnchor="end" fill="#999" > {\`\${t}\`} </Text> <Path d={\`M0 \${priceScale(t)} \${deviceWidth} \${priceScale(t)}\`} stroke="#999" strokeDasharray="2,2" /> </G> ); }) } `} </Code> <T>exclusivePriceTicks</T> <Svg height={defaultStockChartHeight} width={deviceWidth}> <Path d={areaFunction(ticks)} fill="rgb(209, 237, 255, 0.85)" /> <Path d={lineFunction(ticks)} stroke="rgb(0, 102, 221, 0.75)" fill="none" /> { adjustPriceTicks.map(t => { return ( <G key={t}> <Text x={deviceWidth - 5} y={adjustPriceScale(t) - 15} textAnchor="end" fill="#999" key={t} > {`${t}`} </Text> <Path d={`M0 ${adjustPriceScale(t)} ${deviceWidth} ${adjustPriceScale(t)}`} stroke="#999" strokeDasharray="2,2" /> </G> ); }) } </Svg> <T>Calculate timeScale and timeTicks</T> <Svg height={defaultStockChartHeight + bottomAxisHeight} width={deviceWidth}> <Path d={areaFunction(ticks)} fill="rgb(209, 237, 255, 0.85)" /> <Path d={lineFunction(ticks)} stroke="rgb(0, 102, 221, 0.75)" fill="none" /> { adjustPriceTicks.map(t => { return ( <G key={t}> <Text key={t} x={deviceWidth - 5} y={adjustPriceScale(t) - 15} textAnchor="end" fill="#999" > {`${t}`} </Text> <Path d={`M0 ${adjustPriceScale(t)} ${deviceWidth} ${adjustPriceScale(t)}`} stroke="#999" strokeDasharray="2,2" /> </G> ); }) } <G y={defaultStockChartHeight} fill="red"> { timeTicks.map(t => { return ( <G key={t}> <Text key={t} x={timeScale(t)} y={0} textAnchor="middle" fill="#999" > {`${this.formatTime(t)}`} </Text> <Rect x={timeScale(t)} y={0} width="1" height="5" fill="#999" /> </G> ); }) } </G> </Svg> <Code> {` const timeTicks = timeScale.ticks(tickCounts); { timeTicks.map(t => { return ( <G key={t}> <Text key={t} x={timeScale(t)} y={0} textAnchor="middle" fill="#999" > {\`\${this.formatTime(t)}\`} </Text> <Rect x={timeScale(t)} y={0} width="1" height="5" fill="#999" /> </G> ); }) } `} </Code> </ScrollView> ); }
function computeXScale() { const xScale = domain.x[0] instanceof Date ? d3ScaleTime() : d3ScaleLinear(); return xScale.range([xPadding, size.width - xPadding]).domain(domain.x); }
getDefaultProps: () => ({ data: [{x: new Date('2015-01-01T00:00:00Z'), y: 1}], xScale: d3Scale.scaleTime(), yScale: d3Scale.scaleLinear(), }),
* * Copyright 2016-present, Raphaël Benitte. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ import { scaleLinear, scalePoint, scaleTime } from 'd3-scale' export const linearXScale = scaleLinear() .range([0, 280]) .domain([0, 80]) linearXScale.type = 'linear' export const linearYScale = scaleLinear() .range([160, 0]) .domain([0, 35]) linearYScale.type = 'linear' export const pointXScale = scalePoint() .range([0, 280]) .domain(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']) pointXScale.type = 'point' const timeScaleStart = new Date(2019, 0, 1, 0, 0, 0, 0) const timeScaleEnd = new Date(2020, 0, 1, 0, 0, 0, 0) export const timeXScale = scaleTime() .range([0, 280]) .domain([timeScaleStart, timeScaleEnd]) timeXScale.type = 'time'
/** * * data: [] * series: [ {label, x, y} ] * */ export default function timeline() { var _id = 'timeline_line_' + Date.now(); /** * Style stuff */ // Margin between the main plot group and the svg border var _margin = { top: 10, right: 10, bottom: 20, left: 40 }; // Height and width of the SVG element var _height = 100, _width = 600; // Render the grid? var _displayOptions = { xGrid: false, yGrid: false, pointEvents: false // value, values, series, custom (falsey is off) }; /** * Configuration of accessors to data */ // Various configuration functions var _fn = { valueX: function(d) { return d[0]; }, markerValueX: function(d) { return d[0]; }, markerLabel: function(d) { return d[1]; }, pointRadius: function() { return 2; } }; /** * Extents, Axes, and Scales */ // Extent configuration for x and y dimensions of plot var now = Date.now(); var _extent = { x: extent({ defaultValue: [ now - 60000 * 5, now ], getValue: function(d, i) { return _fn.valueX(d, i); } }), y: extent({ filter: function(d, i) { var x = _fn.valueX(d, i); return x >= _scale.x.domain()[0] && x <= _scale.x.domain()[1]; } }) }; var _multiExtent = multiExtent(); // Default scales for x and y dimensions var _scale = { x: d3_scaleTime(), y: d3_scaleLinear() }; // Default Axis definitions var _axis = { x: d3_axisBottom().scale(_scale.x), y: d3_axisLeft().ticks(3).scale(_scale.y), xGrid: d3_axisBottom().tickFormat('').tickSizeOuter(0).scale(_scale.x), yGrid: d3_axisLeft().tickFormat('').tickSizeOuter(0).ticks(3).scale(_scale.y) }; /** * Generators */ var _line = d3_line() .x(function(d, i) { return _scale.x(_fn.valueX(d, i)); }); var _area = d3_area() .x(function(d, i) { return _scale.x(_fn.valueX(d, i)); }); // Voronoi that we'll use for hovers var _voronoi = d3_voronoi() .x(function(d, i) { return _scale.x(d.x, i); }) .y(function(d, i) { return _scale.y(d.y, i); }); /** * Brush and Events */ // Brush Management var _brush = timelineBrush({ brush: d3_brushX(), scale: _scale.x }); _brush.dispatch() .on('end', function() { updateBrush(); _dispatch.call('brushEnd', this, getBrush()); }) .on('start', function() { updateBrush(); _dispatch.call('brushStart', this, getBrush()); }) .on('brush', function() { updateBrush(); _dispatch.call('brush', this, getBrush()); }); // The dispatch object and all events var _dispatch = d3_dispatch( 'brush', 'brushStart', 'brushEnd', 'markerClick', 'markerMouseover', 'markerMouseout', 'pointMouseover', 'pointMouseout', 'pointClick'); /** * Keep track of commonly access DOM elements */ // Storage for commonly used DOM elements var _element = { svg: undefined, g: { container: undefined, plots: undefined, plotBrushes: undefined, points: undefined, voronoi: undefined, xAxis: undefined, yAxis: undefined, xAxisGrid: undefined, yAxisGrid: undefined, markers: undefined, brush: undefined }, plotClipPath: undefined, plotBrushClipPath: undefined, markerClipPath: undefined }; /** * Data and Series and Markers */ // The main data array var _data = []; // The definition of the series to draw var _series = []; // Markers data var _markers = []; /** * Explodes the data into an array with one point per unique point * in the data (according to the series). * * I.e., * * data: [{ x: 0, y1: 1, y2: 2}] * series: [ * { key: 's1', getValue: function(d) { return d.y1; } }, * { key: 's2', getValue: function(d) { return d.y2; } } * ] * * ==> * * [ * { x: 0, y: 1, series: { key: 's1', ... }, data: { x: 0, y1: 1, y2: 2 }, * { x: 0, y: 2, series: { key: 's2', ... }, data: { x: 0, y1: 1, y2: 2 }, * ] * * @param series * @param data */ function getVoronoiData(series, data, getXValue) { var toReturn = []; // Loop over each series series.forEach(function(s, i) { // Convert the data to x/y series toReturn = toReturn.concat(data.map(function(d, ii) { return { x: getXValue(d, ii), y: s.getValue(d, ii), series: s, data: d }; })); }); return toReturn; } function highlightValues(hovered) { if (null != hovered) { var join = _element.g.points.selectAll('circle') .data(_series.map(function(d) { return { x: _fn.valueX(hovered.data), y: d.getValue(hovered.data), category: d.category }; })); var enter = join.enter().append('circle'); var update = join.selectAll('circle'); enter.merge(update) .attr('class', function(d, i) { return d.category; }) .attr('cx', function(d, i) { return _scale.x(d.x); }) .attr('cy', function(d, i) { return _scale.y(d.y); }) .attr('r', 3); } else { _element.g.points.selectAll('circle').remove(); } } function highlightValue(hovered) {} function highlightSeries(hovered) {} function onPointMouseover(d, i) { var pointAction = _displayOptions.pointEvents; if('value' === pointAction) { highlightValue(d.data); } else if('values' === pointAction) { highlightValues(d.data); } else if('series' === pointAction) { highlightSeries(d.data); } _dispatch.call('pointMouseover', this, d.data, i); } function onPointMouseout(d, i) { var pointAction = _displayOptions.pointEvents; if('value' === pointAction) { highlightValue(); } else if('values' === pointAction) { highlightValues(); } else if('series' === pointAction) { highlightSeries(); } _dispatch.call('pointMouseout', this, d.data, i); } function onPointClick(d, i) { _dispatch.call('pointClick', this, d.data, i); } /** * Get the current brush state in terms of the x data domain, in ms epoch time */ function getBrush() { // Try to get the node from the brush group selection var node = (null != _element.g.brush)? _element.g.brush.node() : null; // Get the current brush selection return _brush.getSelection(node); } function getBrushSelection() { // Try to get the node from the brush group selection var node = (null != _element.g.brush)? _element.g.brush.node() : null; // Get the current brush selection return _brush.getBrushSelection(node); } function getBrushHandlePath(d) { var w = 8, h = 12, ch = 4; var y = (_scale.y.range()[0] / 2) + (h / 2); return 'M' + (w / 2) + ' ' + y + ' c 0 ' + ch + ', ' + (-w) + ' ' + ch + ', ' + (-w) + ' 0 v0 ' + (-h) + ' c 0 ' + (-ch) + ', ' + w + ' ' + (-ch) + ', ' + w + ' 0 Z M0' + ' ' + y + ' v' + (-h); } /** * Set the current brush state in terms of the x data domain * @param v The new value of the brush * */ function setBrush(v) { _brush.setSelection(_element.g.brush, v); } /** * Update the state of the brush (as part of redrawing everything) * * The purpose of this function is to update the state of the brush to reflect changes * to the rest of the chart as part of a normal update/redraw cycle. When the x extent * changes, the brush needs to move to stay correctly aligned with the x axis. Normally, * we are only updating the drawn position of the brush, so the brushSelection doesn't * actually change. However, if the change results in the brush extending partially or * wholly outside of the x extent, we might have to clip or clear the brush, which will * result in brush change events being propagated. * * @param previousExtent The previous state of the brush extent. Must be provided to * accurately determine the extent of the brush in terms of the x data domain */ function updateBrush(previousExtent) { // If there was no previous extent, then there is no brush to update if (null != previousExtent) { // Derive the overall plot extent from the collection of series var plotExtent = _extent.x.getExtent(_data); if(null != plotExtent && Array.isArray(plotExtent) && plotExtent.length == 2) { // Clip extent by the full extent of the plot (this is in case we've slipped off the visible plot) var newExtent = [ Math.max(plotExtent[0], previousExtent[0]), Math.min(plotExtent[1], previousExtent[1]) ]; setBrush(newExtent); } else { // There is no plot/data so just clear the brush setBrush(undefined); } } _element.g.brush .style('display', (_brush.enabled())? 'unset' : 'none') .call(_brush.brush()); /* * Update the clip path for the brush plot */ var brushExtent = getBrushSelection(); if (null != brushExtent) { var height = _scale.y.range()[0]; // Update the brush clip path _element.plotBrushClipPath .attr('transform', 'translate(' + brushExtent[0] + ', -1)') .attr('width', Math.max(0, brushExtent[1] - brushExtent[0])) .attr('height', Math.max(0, height) + 2); // Create/Update the handles var handleJoin = _element.g.brush .selectAll('.resize-handle').data([ { type: 'w' }, { type: 'e' } ]); var handleEnter = handleJoin.enter().append('g') .attr('class', 'resize-handle') .attr('cursor', 'ew-resize'); handleEnter.append('path').attr('class', 'handle-line'); handleEnter.append('path').attr('class', 'handle-grip'); var merge = handleEnter.merge(handleJoin); merge.attr('transform', function(d, i) { return 'translate(' + brushExtent[i] + ', 0)'; }); merge.select('.handle-line') .attr('d', 'M0 ' + height + ' v' + (-height)); merge.select('.handle-grip') .attr('d', getBrushHandlePath); } else { // Empty the clip path _element.plotBrushClipPath .attr('transform', 'translate(-1, -1)') .attr('width', 0) .attr('height', 0); // Remove the handles _element.g.brush.selectAll('.resize-handle') .remove(); } } function updateAxes() { if (null != _axis.x) { _element.g.xAxis.call(_axis.x); } if (null != _axis.xGrid && _displayOptions.xGrid) { _element.g.xAxisGrid.call(_axis.xGrid); } if (null != _axis.y) { _element.g.yAxis.call(_axis.y); } if (null != _axis.yGrid && _displayOptions.yGrid) { _element.g.yAxisGrid.call(_axis.yGrid); } } function updatePlots() { // Join var plotJoin = _element.g.plots .selectAll('.plot') .data(_series, function(d) { return d.key; }); // Enter var plotEnter = plotJoin.enter().append('g').attr('class', 'plot'); var lineEnter = plotEnter.append('g').append('path') .attr('class', function(d) { return ((d.category)? d.category : '') + ' line'; }); var areaEnter = plotEnter.append('g').append('path') .attr('class', function(d) { return ((d.category)? d.category : '') + ' area'; }); var lineUpdate = plotJoin.select('.line'); var areaUpdate = plotJoin.select('.area'); // Enter + Update lineEnter.merge(lineUpdate) .attr('d', function(series) { return _line.y(function (d, i) { return _scale.y(series.getValue(d, i)); })(_data); }); areaEnter.merge(areaUpdate) .attr('d', function(series) { return _area .y0(_scale.y.range()[0]) .y1(function (d, i) { return _scale.y(series.getValue(d, i)); })(_data); }); // Remove the previous voronoi _element.g.voronoi.selectAll('path').remove(); if (_displayOptions.pointEvents) { // check range against width var extent = _scale.x.domain(); var voronoiData = getVoronoiData(_series, _data, _fn.valueX) .filter(function(d) { // Filter out points that are outside of the extent return (extent[0] <= d.x && d.x <= extent[1]); }); // Filter out paths that are null voronoiData = _voronoi.polygons(voronoiData) .filter(function (d) { return (null != d); }); // Draw the voronoi overlay polygons _element.g.voronoi.selectAll('path').data(voronoiData).enter().append('path') .attr('d', function (d) { return (null != d) ? 'M' + d.join('L') + 'Z' : null; }) .on('mouseover', onPointMouseover) .on('mouseout', onPointMouseout) .on('click', onPointClick); } // Exit var plotExit = plotJoin.exit(); plotExit.remove(); } function updatePlotBrushes() { // Join var plotJoin = _element.g.plotBrushes .selectAll('.plot-brush') .data((_brush.enabled())? _series : [], function(d) { return d.key; }); // Enter var plotEnter = plotJoin.enter().append('g').attr('class', 'plot plot-brush'); var lineEnter = plotEnter.append('g').append('path') .attr('class', function(d) { return ((d.category)? d.category : '') + ' line'; }); var areaEnter = plotEnter.append('g').append('path') .attr('class', function(d) { return ((d.category)? d.category : '') + ' area'; }); var lineUpdate = plotJoin.select('.line'); var areaUpdate = plotJoin.select('.area'); // Enter + Update lineEnter.merge(lineUpdate) .attr('d', function(series) { return _line.y(function (d, i) { return _scale.y(series.getValue(d, i)); })(_data); }); areaEnter.merge(areaUpdate) .attr('d', function(series) { return _area .y0(_scale.y.range()[0]) .y1(function (d, i) { return _scale.y(series.getValue(d, i)); })(_data); }); // Exit var plotExit = plotJoin.exit(); plotExit.remove(); } function updateMarkers() { // Join var markerJoin = _element.g.markers .selectAll('.marker') .data(_markers, _fn.markerValueX); // Enter var markerEnter = markerJoin.enter().append('g') .attr('class', 'marker') .on('mouseover', function(d, i) { _dispatch.call('markerMouseover', this, d, i); }) .on('mouseout', function(d, i) { _dispatch.call('markerMouseout', this, d, i); }) .on('click', function(d, i) { _dispatch.call('markerClick', this, d, i); }); var lineEnter = markerEnter.append('line'); var textEnter = markerEnter.append('text'); lineEnter .attr('y1', function(d) { return _scale.y.range()[1]; }) .attr('y2', function(d) { return _scale.y.range()[0]; }); textEnter .attr('dy', '0em') .attr('y', -3) .attr('text-anchor', 'middle') .text(_fn.markerLabel); // Enter + Update var lineUpdate = markerJoin.select('line'); var textUpdate = markerJoin.select('text'); lineEnter.merge(lineUpdate) .attr('x1', function(d, i) { return _scale.x(_fn.markerValueX(d, i)); }) .attr('x2', function(d, i) { return _scale.x(_fn.markerValueX(d)); }); textEnter.merge(textUpdate) .attr('x', function(d, i) { return _scale.x(_fn.markerValueX(d)); }); // Exit markerJoin.exit().remove(); } // Chart create/init method function _instance() {} /** * Initialize the chart (only called once). Performs all initial chart creation/setup * * @param container The container element to which to apply the chart * @returns {_instance} Instance of the chart */ _instance.init = function(container) { // Create a container div _element.div = container.append('div').attr('class', 'sentio timeline'); // Create the SVG element _element.svg = _element.div.append('svg'); // Add the defs and add the clip path definition var defs = _element.svg.append('defs'); _element.plotBrushClipPath = defs.append('clipPath').attr('id', 'plotBrush_' + _id).append('rect'); _element.plotClipPath = defs.append('clipPath').attr('id', 'plot_' + _id).append('rect'); _element.markerClipPath = defs.append('clipPath').attr('id', 'marker_' + _id).append('rect'); // Append a container for everything _element.g.container = _element.svg.append('g'); // Append the grid _element.g.grid = _element.g.container.append('g').attr('class', 'grid'); _element.g.xAxisGrid = _element.g.grid.append('g').attr('class', 'x'); _element.g.yAxisGrid = _element.g.grid.append('g').attr('class', 'y'); // Append the path group (which will have the clip path and the line path _element.g.plots = _element.g.container.append('g').attr('class', 'plots'); _element.g.plots.attr('clip-path', 'url(#plot_' + _id + ')'); // Append the path group (which will have the clip path and the line path _element.g.plotBrushes = _element.g.container.append('g').attr('class', 'plot-brushes'); _element.g.plotBrushes.attr('clip-path', 'url(#plotBrush_' + _id + ')'); _element.g.plotBrushHandles = _element.g.container.append('g').attr('class', 'plot-brush-handles'); // Append groups for the axes _element.g.axes = _element.g.container.append('g').attr('class', 'axis'); _element.g.xAxis = _element.g.axes.append('g').attr('class', 'x'); _element.g.yAxis = _element.g.axes.append('g').attr('class', 'y'); // Append a group for the voronoi and the points _element.g.points = _element.g.container.append('g').attr('class', 'points'); _element.g.points.attr('clip-path', 'url(#marker_' + _id + ')'); _element.g.voronoi = _element.g.container.append('g').attr('class', 'voronoi'); // Append a group for the markers _element.g.markers = _element.g.container.append('g').attr('class', 'markers'); _element.g.markers.attr('clip-path', 'url(#marker_' + _id + ')'); // Add the brush element _element.g.brush = _element.g.container.append('g').attr('class', 'x brush'); _instance.resize(); return _instance; }; /* * Set the data to drive the chart */ _instance.data = function(v) { if (!arguments.length) { return _data; } _data = (null != v)? v : []; return _instance; }; /* * Define the series to show on the chart */ _instance.series = function(v) { if (!arguments.length) { return _series; } _series = (null != v)? v : []; return _instance; }; /* * Set the markers data */ _instance.markers = function(v) { if (!arguments.length) { return _markers; } _markers = (null != v)? v : []; return _instance; }; /* * Updates all the elements that depend on the size of the various components */ _instance.resize = function() { // Need to grab the brush extent before we change anything var brushSelection = getBrush(); // Resize the SVG Pane _element.svg.attr('width', _width).attr('height', _height); // Update the margins on the main draw group _element.g.container.attr('transform', 'translate(' + _margin.left + ',' + _margin.top + ')'); // Resize Scales _scale.x.range([ 0, Math.max(0, _width - _margin.left - _margin.right) ]); _scale.y.range([ Math.max(0, _height - _margin.top - _margin.bottom), 0 ]); /** * Resize clip paths */ // Plot Brush clip path is only the plot pane _element.plotBrushClipPath .attr('transform', 'translate(-1, -1)') .attr('width', Math.max(0, _scale.x.range()[1]) + 2) .attr('height', Math.max(0, _scale.y.range()[0]) + 2); // Plot clip path is only the plot pane _element.plotClipPath .attr('transform', 'translate(-1, -1)') .attr('width', Math.max(0, _scale.x.range()[1]) + 2) .attr('height', Math.max(0, _scale.y.range()[0]) + 2); // Marker clip path includes top margin by default _element.markerClipPath .attr('transform', 'translate(0, -' + _margin.top + ')') .attr('width', Math.max(0, _width - _margin.left - _margin.right)) .attr('height', Math.max(0, _height - _margin.bottom)); // Resize the clip extent of the plot _voronoi.extent([ [ 0, 0 ], [ _width - _margin.left - _margin.right, _height - _margin.top - _margin.bottom ] ]); /** * Update axis and grids */ // Reset axis and grid positions _element.g.xAxis.attr('transform', 'translate(0,' + _scale.y.range()[0] + ')'); _element.g.xAxisGrid.attr('transform', 'translate(0,' + _scale.y.range()[0] + ')'); // Resize the x grid ticks if (_displayOptions.xGrid) { _axis.xGrid.tickSizeInner(-(_height - _margin.top - _margin.bottom)); } else { _axis.xGrid.tickSizeInner(0); } // Resize the y grid ticks if (_displayOptions.yGrid) { _axis.yGrid.tickSizeInner(-(_width - _margin.left - _margin.right)); } else { _axis.yGrid.tickSizeInner(0); } /** * Update the brush */ // Resize and position the brush g element _element.g.brush.selectAll('rect') .attr('y', -1).attr('x', 0) .attr('width', _scale.x.range()[1]) .attr('height', _scale.y.range()[0] + 2); // Resize the brush _brush.brush() .extent([ [ 0, 0 ], [ _scale.x.range()[1], _scale.y.range()[0] + 2 ] ]); updateBrush(brushSelection); return _instance; }; /* * Redraw the graphic */ _instance.redraw = function() { // Need to grab the brush extent before we change anything var brushSelection = getBrush(); // Update the x domain (to the latest time window) _scale.x.domain(_extent.x.getExtent(_data)); // Update the y domain (based on configuration and data) _scale.y.domain(_multiExtent.extent(_extent.y).series(_series).getExtent(_data)); // Update the plot elements updateAxes(); updatePlots(); updatePlotBrushes(); updateMarkers(); updateBrush(brushSelection); return _instance; }; // Basic Getters/Setters _instance.width = function(v) { if (!arguments.length) { return _width; } _width = v; return _instance; }; _instance.height = function(v) { if (!arguments.length) { return _height; } _height = v; return _instance; }; _instance.margin = function(v) { if (!arguments.length) { return _margin; } _margin = v; return _instance; }; _instance.showXGrid = function(v) { if (!arguments.length) { return _displayOptions.xGrid; } _displayOptions.xGrid = v; return _instance; }; _instance.showYGrid = function(v) { if (!arguments.length) { return _displayOptions.yGrid; } _displayOptions.yGrid = v; return _instance; }; _instance.showGrid = function(v) { _displayOptions.xGrid = _displayOptions.yGrid = v; return _instance; }; _instance.pointEvents = function(v) { if (!arguments.length) { return _displayOptions.pointEvents; } _displayOptions.pointEvents = v; return _instance; }; _instance.curve = function(v) { if (!arguments.length) { return _line.curve(); } _line.curve(v); _area.curve(v); return _instance; }; _instance.xAxis = function(v) { if (!arguments.length) { return _axis.x; } _axis.x = v; return _instance; }; _instance.xGridAxis = function(v) { if (!arguments.length) { return _axis.xGrid; } _axis.xGrid = v; return _instance; }; _instance.yAxis = function(v) { if (!arguments.length) { return _axis.y; } _axis.y = v; return _instance; }; _instance.yGridAxis = function(v) { if (!arguments.length) { return _axis.yGrid; } _axis.yGrid = v; return _instance; }; _instance.xScale = function(v) { if (!arguments.length) { return _scale.x; } _scale.x = v; if (null != _axis.x) { _axis.x.scale(v); } if (null != _axis.xGrid) { _axis.xGrid.scale(v); } if (null != _brush) { _brush.scale(v); } return _instance; }; _instance.yScale = function(v) { if (!arguments.length) { return _scale.y; } _scale.y = v; if (null != _axis.y) { _axis.y.scale(v); } if (null != _axis.yGrid) { _axis.yGrid.scale(v); } return _instance; }; _instance.xValue = function(v) { if (!arguments.length) { return _fn.valueX; } _fn.valueX = v; return _instance; }; _instance.yExtent = function(v) { if (!arguments.length) { return _extent.y; } _extent.y = v; return _instance; }; _instance.xExtent = function(v) { if (!arguments.length) { return _extent.x; } _extent.x = v; return _instance; }; _instance.markerXValue = function(v) { if (!arguments.length) { return _fn.markerValueX; } _fn.markerValueX = v; return _instance; }; _instance.markerLabel = function(v) { if (!arguments.length) { return _fn.markerLabel; } _fn.markerLabel = v; return _instance; }; _instance.dispatch = function(v) { if (!arguments.length) { return _dispatch; } return _instance; }; _instance.brush = function(v) { if (!arguments.length) { return _brush.enabled(); } _brush.enabled(v); return _instance; }; _instance.setBrush = function(v) { setBrush(v); return _instance; }; _instance.getBrush = function() { return getBrush(); }; return _instance; }
export const semioticLineChart = ( data: Array<Object>, schema: Object, options: Object ) => { let lineData; const { chart, selectedMetrics, lineType, metrics, primaryKey, colors } = options; const { timeseriesSort } = chart; const sortType = timeseriesSort === "array-order" ? "integer" : schema.fields.find(field => field.name === timeseriesSort).type; const formatting = sortType === "datetime" ? tickValue => tickValue.toLocaleString().split(",")[0] : numeralFormatting; const xScale = sortType === "datetime" ? scaleTime() : scaleLinear(); lineData = metrics .map((metric, index) => { const metricData = timeseriesSort === "array-order" ? data : data.sort( (datapointA, datapointB) => datapointA[timeseriesSort] - datapointB[timeseriesSort] ); return { color: colors[index % colors.length], label: metric.name, type: metric.type, coordinates: metricData.map((datapoint, datapointValue) => ({ value: datapoint[metric.name], x: timeseriesSort === "array-order" ? datapointValue : datapoint[timeseriesSort], label: metric.name, color: colors[index % colors.length], originalData: datapoint })) }; }) .filter( metric => selectedMetrics.length === 0 || selectedMetrics.find(selectedMetric => selectedMetric === metric.label) ); return { lineType: { type: lineType, interpolator: curveMonotoneX }, lines: lineData, xScaleType: xScale, renderKey: (line: Object, index: number) => { return line.coordinates ? `line-${line.label}` : `linepoint=${line.label}-${index}`; }, lineStyle: (line: Object) => ({ fill: lineType === "line" ? "none" : line.color, stroke: line.color, fillOpacity: 0.75 }), pointStyle: (point: Object) => { return { fill: point.color, fillOpacity: 0.75 }; }, axes: [ { orient: "left", tickFormat: numeralFormatting }, { orient: "bottom", ticks: 5, tickFormat: (tickValue: any) => { const label = formatting(tickValue); const rotation = label.length > 4 ? "45" : "0"; const textAnchor = label.length > 4 ? "start" : "middle"; return ( <text transform={`rotate(${rotation})`} textAnchor={textAnchor}> {label} </text> ); } } ], hoverAnnotation: true, xAccessor: "x", yAccessor: "value", showLinePoints: lineType === "line", margin: { top: 20, right: 200, bottom: sortType === "datetime" ? 80 : 40, left: 50 }, legend: { title: "Legend", position: "right", width: 200, legendGroups: [ { label: "", styleFn: (legendItem: Object) => ({ fill: legendItem.color }), items: lineData } ] }, tooltipContent: (hoveredDatapoint: Object) => { return ( <TooltipContent x={hoveredDatapoint.x} y={hoveredDatapoint.y}> <p> {hoveredDatapoint.parentLine && hoveredDatapoint.parentLine.label} </p> <p> {(hoveredDatapoint.value && hoveredDatapoint.value.toLocaleString()) || hoveredDatapoint.value} </p> <p> {timeseriesSort}: {formatting(hoveredDatapoint.x)} </p> {primaryKey.map((pkey, index) => ( <p key={`key-${index}`}> {pkey}:{" "} {(hoveredDatapoint.originalData[pkey].toString && hoveredDatapoint.originalData[pkey].toString()) || hoveredDatapoint.originalData[pkey]} </p> ))} </TooltipContent> ); } }; };
getXScale = (availableWidth /*: number */, flatData /*: Array<Point> */) => scaleTime() .domain(extent(flatData, d => d.x)) .range([0, availableWidth]) .clamp(true);
function Chart(node, initial) { var margin = [10, 10, 30, 20] var width = node.offsetWidth - margin[1] - margin[3] var height = node.offsetHeight - margin[0] - margin[2] var now = Date.now() var data = initial || [] var maxY = 10 var hasNewData = false var x = scale.scaleTime() .domain([now - 60000, now]) .range([0, width]) var y = scale.scaleLinear() .domain([0, maxY]) .range([height, 0]) var line = shape.line() .x(function(d) { return x(d.key) }) .y(function(d) { return y(d.value) }) var xAxis = axis.axisBottom(x) .ticks(time.timeSecond.every(5)) .tickFormat(timeFormat.timeFormat('%I:%M:%S')) var yAxis = axis.axisLeft(y) .ticks(5) .tickSizeInner(-width) var svg = selection.select(node) .append('svg') .attr('width', width + margin[1] + margin[3]) .attr('height', height + margin[0] + margin[2]) .append('g') .attr('transform', 'translate(' + margin[3] + ',' + margin[0] + ')') var yAxisSvg = svg.append('g') .attr('class', 'y-axis') .attr('transform', 'translate(0,0)') .call(yAxis) var xAxisSvg = svg.append('g') .attr('class', 'x-axis') .attr('transform', 'translate(0,' + height + ')') .call(xAxis) var lineSvg = svg.append('path') .datum(data) .attr('class', 'line') .attr('d', line) function tick() { now = Date.now() x.domain([now - 60000, now]) if(!hasNewData) { data.push({key: now, value: 0}) } else { hasNewData = false } lineSvg .attr('d', line) .transition() .duration(1000) .ease(ease.easeLinear) .attr('transform', 'translate(' + x(now - 60000) + ')') .on('end', tick) xAxisSvg .call(xAxis) yAxisSvg .transition() .call(yAxis) for(var i in data) { if(data[i].key < now - 60000) { data.splice(i, 1) } else { break } } } this.start = tick this.insert = function(d) { if(d.value > maxY) { maxY = d.value y.domain([0, maxY]) } for(var i = data.length - 1; i > 0; i--) { if(d.key > data[i].key) { data.splice(i + 1, 0, d) break } } hasNewData = true } }
render() { const { type, data, width, ratio } = this.props; const xAccessor = d => d.date; const start = xAccessor(last(data)); const end = xAccessor(data[Math.max(0, data.length - 150)]); const xExtents = [start, end]; return ( <ChartCanvas height={400} ratio={ratio} width={width} margin={{ left: 80, right: 80, top: 10, bottom: 30 }} type={type} seriesName="MSFT" data={data} xScale={scaleTime()} xAccessor={xAccessor} xExtents={xExtents}> <Chart id={2} yExtents={[d => d.volume]} height={150} origin={(w, h) => [0, h - 150]}> <YAxis axisAt="left" orient="left" ticks={5} tickFormat={format(".0s")}/> <MouseCoordinateY at="left" orient="left" displayFormat={format(".4s")} /> <BarSeries yAccessor={d => d.volume} fill={d => d.close > d.open ? "#6BA583" : "#FF0000"} /> <CurrentCoordinate yAccessor={d => d.volume} fill="#9B0A47" /> <EdgeIndicator itemType="first" orient="left" edgeAt="left" yAccessor={d => d.volume} displayFormat={format(".4s")} fill="#0F0F0F"/> <EdgeIndicator itemType="last" orient="right" edgeAt="right" yAccessor={d => d.volume} displayFormat={format(".4s")} fill="#0F0F0F"/> </Chart> <Chart id={1} yExtents={[d => [d.high, d.low]]} padding={{ top: 40, bottom: 20 }}> <XAxis axisAt="bottom" orient="bottom"/> <YAxis axisAt="right" orient="right" ticks={5} /> <MouseCoordinateX rectWidth={60} at="bottom" orient="bottom" displayFormat={timeFormat("%H:%M:%S")} /> <MouseCoordinateY at="right" orient="right" displayFormat={format(".2f")} /> <CandlestickSeries /> <EdgeIndicator itemType="last" orient="right" edgeAt="right" yAccessor={d => d.close} fill={d => d.close > d.open ? "#6BA583" : "#FF0000"}/> <OHLCTooltip origin={[-40, 0]} xDisplayFormat={timeFormat("%Y-%m-%d %H:%M:%S")}/> </Chart> <CrossHairCursor /> </ChartCanvas> ); }