diff --git a/README.md b/README.md index a7ff13a4..84822dcb 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,17 @@ const chartConfig = { color: (opacity = 1) => `rgba(26, 255, 146, ${opacity})`, strokeWidth: 2, // optional, default 3 barPercentage: 0.5, - useShadowColorFromDataset: false // optional + useShadowColorFromDataset: false, // optional + gutterTop: 10, // optional, default dynamic size: 10% * innerHeight after paddingTop and paddingBottom + horizontalLabelWidth: 30, // optional, default dynamic size:20% * innerHeight after paddingTop and paddingBottom + verticalLabelHeight: 30, // optional, default dynamic size: 15% * innerWidth after paddingLeft and paddingRight + chartStyle: { //optional + borderRadius: 10, //default 0 + paddingTop: 10, //default 0 + paddingBottom: 10, + paddingLeft: 10, + paddingRight: 10, + }, }; ``` @@ -108,6 +118,9 @@ const chartConfig = { | barRadius | Number | Defines the radius of each bar | | propsForBackgroundLines | props | Override styles of the background lines, refer to react-native-svg's Line documentation | | propsForLabels | props | Override styles of the labels, refer to react-native-svg's Text documentation | +| gutterTop | number | Define the gap between highest coordinate and padding | +| horizontalLabelWidth | number | Define the width of horizontal labels | +| verticalLabelHeight | number | Define the height of vertical labels | ## Responsive charts @@ -415,6 +428,15 @@ const commitsData = [ chartConfig={chartConfig} /> ``` +Extra chartStyle for heatmap +```js +const chartConfig = { + chartStyle: { + justifyContent: 'start' || 'center' || 'end', //optional, defualt is 'start'; + alignItems: 'start' || 'center' || 'end', //optional, default is 'start'; + }, +}; +``` | Property | Type | Description | | ------------------ | -------- | ------------------------------------------------------------------------------------------- | @@ -422,7 +444,7 @@ const commitsData = [ | width | Number | Width of the chart, use 'Dimensions' library to get the width of your screen for responsive | | height | Number | Height of the chart | | gutterSize | Number | Size of the gutters between the squares in the chart | -| squareSize | Number | Size of the squares in the chart | +| squareSize | Number | Optional, Size of the squares in the chart, dynamic size will be auto applied if prop is not provided | | horizontal | boolean | Should graph be laid out horizontally? Defaults to `true` | | showMonthLabels | boolean | Should graph include labels for the months? Defaults to `true` | | showOutOfRangeDays | boolean | Should graph be filled with squares, including days outside the range? Defaults to `false` | diff --git a/index.d.ts b/index.d.ts index 62419077..f0701a74 100644 --- a/index.d.ts +++ b/index.d.ts @@ -68,6 +68,8 @@ export interface LineChartProps { * Show inner dashed lines - default: True. */ + defMax?: number; + defMin?: number; withScrollableDot?: boolean; withInnerLines?: boolean; /** @@ -230,7 +232,8 @@ export interface BarChartProps { data: ChartData; width: number; height: number; - fromZero?: boolean; + defMax?: number; + defMin?: number; withInnerLines?: boolean; yAxisLabel: string; yAxisSuffix: string; @@ -238,12 +241,17 @@ export interface BarChartProps { style?: ViewStyle; horizontalLabelRotation?: number; verticalLabelRotation?: number; + hideLabelsAtIndex?: (number | null)[]; + barWidth?: number; + decorator?: ({}:any) => JSX.Element; /** * The number of horizontal lines */ segments?: number; showBarTops?: boolean; showValuesOnTopOfBars?: boolean; + withHorizontalLabels?: boolean; + withVerticalLabels?: boolean; } export class BarChart extends React.Component {} @@ -306,6 +314,7 @@ export class PieChart extends React.Component {} // ContributionGraph export interface ContributionGraphProps { + style?: ViewStyle; values: Array; endDate: Date; numDays: number; @@ -320,6 +329,9 @@ export interface ContributionGraphProps { accessor?: string; getMonthLabel?: (monthIndex: number) => string; onDayPress?: ({ count: number, date: Date }) => void; + toggleTooltip?: boolean; + tooltipContent?: (dateInfo: { date: string, [accessor: string]: string}, + args: {x:number, y:number, index:number}) => JSX.Element; } export class ContributionGraph extends React.Component< diff --git a/src/abstract-chart.js b/src/abstract-chart.js index 945de84d..c24e3e93 100644 --- a/src/abstract-chart.js +++ b/src/abstract-chart.js @@ -3,17 +3,18 @@ import React, { Component } from "react"; import { LinearGradient, Line, Text, Defs, Stop } from "react-native-svg"; class AbstractChart extends Component { + calcScaler = data => { - if (this.props.fromZero) { - return Math.max(...data, 0) - Math.min(...data, 0) || 1; - } else { - return Math.max(...data) - Math.min(...data) || 1; - } + const defMin = this.props.defMin ?? Math.min(...data); + const defMax = this.props.defMax ?? Math.max(...data); + return Math.max(...data, defMin, defMax) - Math.min(...data, defMin, defMax) || 1; }; calcBaseHeight = (data, height) => { - const min = Math.min(...data); - const max = Math.max(...data); + const defMin = this.props.defMin ?? Math.min(...data); + const defMax = this.props.defMax ?? Math.max(...data); + const min = Math.min(...data, defMin, defMax); + const max = Math.max(...data, defMin, defMax); if (min >= 0 && max >= 0) { return height; } else if (min < 0 && max <= 0) { @@ -24,18 +25,16 @@ class AbstractChart extends Component { }; calcHeight = (val, data, height) => { - const max = Math.max(...data); - const min = Math.min(...data); + const defMin = this.props.defMin ?? Math.min(...data); + const defMax = this.props.defMax ?? Math.max(...data); + const max = Math.max(...data, defMin, defMax); + const min = Math.min(...data, defMin, defMax); if (min < 0 && max > 0) { return height * (val / this.calcScaler(data)); } else if (min >= 0 && max >= 0) { - return this.props.fromZero - ? height * (val / this.calcScaler(data)) - : height * ((val - min) / this.calcScaler(data)); + return height * ((val - min) / this.calcScaler(data)); } else if (min < 0 && max <= 0) { - return this.props.fromZero - ? height * (val / this.calcScaler(data)) - : height * ((val - max) / this.calcScaler(data)); + return height * ((val - max) / this.calcScaler(data)); } }; @@ -58,22 +57,27 @@ class AbstractChart extends Component { return { fontSize: 12, fill: labelColor(0.8), - ...propsForLabels + ...propsForLabels, }; } renderHorizontalLines = config => { - const { count, width, height, paddingTop, paddingRight } = config; - const basePosition = height - height / 4; - - return [...new Array(count + 1)].map((_, i) => { - const y = (basePosition / count) * i + paddingTop; + const { count, width, height, gutterTop, horizontalLabelWidth, verticalLabelHeight, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, + } = config; + const basePosition = height - verticalLabelHeight - paddingBottom; + const totalLineHeight = basePosition - paddingTop - gutterTop; + const x1 = horizontalLabelWidth + paddingLeft; + const x2 = width - paddingRight; + const lineGap = (count - 1) || 1; //handle divided by zero + return [...new Array(count)].map((_, i) => { + const y = basePosition - totalLineHeight / lineGap * i return ( @@ -81,65 +85,49 @@ class AbstractChart extends Component { }); }; - renderHorizontalLine = config => { - const { width, height, paddingTop, paddingRight } = config; - return ( - - ); - }; - renderHorizontalLabels = config => { const { count, data, height, - paddingTop, - paddingRight, + gutterTop, + horizontalLabelWidth, horizontalLabelRotation = 0, + verticalLabelHeight, decimalPlaces = 2, - formatYLabel = yLabel => yLabel + formatYLabel = yLabel => yLabel, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, } = config; + const { yAxisLabel = "", yAxisSuffix = "", - yLabelsOffset = 12 + yLabelsOffset = 12, + defMin = Math.min(...data), + defMax = Math.max(...data), } = this.props; - return [...Array(count === 1 ? 1 : count + 1).keys()].map((i, _) => { + const basePosition = height - verticalLabelHeight - paddingBottom; + const totalLineHeight = basePosition - paddingTop - gutterTop; + const lineGap = (count - 1) || 1; + + return [...Array(count === 1 ? 1 : count).keys()].map((i, _) => { let yLabel = i * count; - if (count === 1) { - yLabel = `${yAxisLabel}${formatYLabel( - data[0].toFixed(decimalPlaces) - )}${yAxisSuffix}`; - } else { - const label = this.props.fromZero - ? (this.calcScaler(data) / count) * i + Math.min(...data, 0) - : (this.calcScaler(data) / count) * i + Math.min(...data); - yLabel = `${yAxisLabel}${formatYLabel( - label.toFixed(decimalPlaces) - )}${yAxisSuffix}`; - } + const label = this.calcScaler(data) / lineGap * i + Math.min(...data, defMin, defMax); + yLabel = `${yAxisLabel}${formatYLabel( + label.toFixed(decimalPlaces) + )}${yAxisSuffix}`; + + const x = horizontalLabelWidth - yLabelsOffset; + const y = basePosition - totalLineHeight / lineGap * i; - const basePosition = height - height / 4; - const x = paddingRight - yLabelsOffset; - const y = - count === 1 && this.props.fromZero - ? paddingTop + 4 - : (height * 3) / 4 - (basePosition / count) * i + paddingTop; return ( xLabel + formatXLabel = xLabel => xLabel, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, + midPoint = 0, } = config; + const { xAxisLabel = "", xLabelsOffset = 0, - hidePointsAtIndex = [] + hideLabelsAtIndex = [] } = this.props; const fontSize = 12; let fac = 1; if (stackedBar) { fac = 0.71; } + + const labelWidth = (width - horizontalLabelWidth - paddingRight - paddingLeft) / labels.length; + + const y = height - paddingBottom - verticalLabelHeight + xLabelsOffset + fontSize*1.5; + return labels.map((label, i) => { - if (hidePointsAtIndex.includes(i)) { + if (hideLabelsAtIndex.includes(i)) { return null; } - const x = - (((width - paddingRight) / labels.length) * i + - paddingRight + - horizontalOffset) * - fac; - const y = (height * 3) / 4 + paddingTop + fontSize * 2 + xLabelsOffset; + + const x = (paddingLeft + horizontalLabelWidth + labelWidth * i + midPoint + horizontalOffset) * fac; + return ( { - const { data, width, height, paddingTop, paddingRight } = config; - const { yAxisInterval = 1 } = this.props; - return [...new Array(Math.ceil(data.length / yAxisInterval))].map( + const { data, width, height, gutterTop, horizontalLabelWidth, verticalLabelHeight, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, + } = config; + const { + yAxisInterval = 1, + adjustment = 1, + innerLines, + } = this.props; + const innerWidth = width - horizontalLabelWidth - paddingLeft - paddingRight; + + const lineNum = innerLines || data.length; + + const gap = innerWidth / (lineNum / yAxisInterval); + + return [...new Array(Math.ceil(lineNum / yAxisInterval))].map( (_, i) => { return ( ); @@ -223,20 +227,6 @@ class AbstractChart extends Component { ); }; - renderVerticalLine = config => { - const { height, paddingTop, paddingRight } = config; - return ( - - ); - }; - renderDefs = config => { const { width, @@ -246,9 +236,11 @@ class AbstractChart extends Component { useShadowColorFromDataset, data } = config; + const fromOpacity = config.hasOwnProperty("backgroundGradientFromOpacity") ? config.backgroundGradientFromOpacity : 1.0; + const toOpacity = config.hasOwnProperty("backgroundGradientToOpacity") ? config.backgroundGradientToOpacity : 1.0; diff --git a/src/bar-chart.js b/src/bar-chart.js index bc74544c..68e6cae2 100644 --- a/src/bar-chart.js +++ b/src/bar-chart.js @@ -3,7 +3,11 @@ import { View } from "react-native"; import { Svg, Rect, G, Text } from "react-native-svg"; import AbstractChart from "./abstract-chart"; -const barWidth = 32; +const BAR_RATIO = { + gutterTop: 0.1 , + horizontalLabelWidth: 0.2, + verticalLabelHeight: 0.15, +} class BarChart extends AbstractChart { getBarPercentage = () => { @@ -11,27 +15,37 @@ class BarChart extends AbstractChart { return barPercentage; }; + barPosSetup = (config) => { + const { data, width, height, gutterTop, horizontalLabelWidth, verticalLabelHeight, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, + } = config; + const innerHeight = (height - paddingTop - paddingBottom - verticalLabelHeight - gutterTop); + const baseHeight = this.calcBaseHeight(data, innerHeight); + const labelWidth = (width - horizontalLabelWidth - paddingRight - paddingLeft) / data.length; + const midPoint = labelWidth / 2; + const barWidth = this.props.barWidth * this.getBarPercentage(); + return { innerHeight, baseHeight, labelWidth, midPoint, barWidth }; + } + renderBars = config => { - const { data, width, height, paddingTop, paddingRight, barRadius } = config; - const baseHeight = this.calcBaseHeight(data, height); - return data.map((x, i) => { - const barHeight = this.calcHeight(x, data, height); - const barWidth = 32 * this.getBarPercentage(); + const { data, width, height, gutterTop, horizontalLabelWidth, verticalLabelHeight, barRadius, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, + } = config; + + const { innerHeight, baseHeight, labelWidth, midPoint, barWidth } = this.barPosSetup(config); + + return data.map((value, i) => { + const barHeight = this.calcHeight(value, data, innerHeight); + const x = horizontalLabelWidth + paddingLeft + labelWidth * i + midPoint - barWidth/2; + const y = (barHeight > 0 ? baseHeight - barHeight : baseHeight) + gutterTop + paddingTop; return ( 0 ? baseHeight - barHeight : baseHeight) / 4) * 3 + - paddingTop - } + x={x} + y={y} rx={barRadius} width={barWidth} - height={(Math.abs(barHeight) / 4) * 3} + height={Math.abs(barHeight)} fill="url(#fillShadowGradient)" /> ); @@ -39,20 +53,21 @@ class BarChart extends AbstractChart { }; renderBarTops = config => { - const { data, width, height, paddingTop, paddingRight } = config; - const baseHeight = this.calcBaseHeight(data, height); - return data.map((x, i) => { - const barHeight = this.calcHeight(x, data, height); - const barWidth = 32 * this.getBarPercentage(); + const { data, width, height, gutterTop, horizontalLabelWidth, verticalLabelHeight, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, + } = config; + + const { innerHeight, baseHeight, labelWidth, midPoint, barWidth } = this.barPosSetup(config); + + return data.map((value, i) => { + const barHeight = this.calcHeight(value, data, innerHeight); + const x = horizontalLabelWidth + paddingLeft + labelWidth * i + midPoint - barWidth/2; + const y = baseHeight - barHeight + gutterTop + paddingTop; return ( { - const { data, width, height, paddingTop, paddingRight } = config; - const baseHeight = this.calcBaseHeight(data, height); - return data.map((x, i) => { - const barHeight = this.calcHeight(x, data, height); - const barWidth = 32 * this.getBarPercentage(); + const { data, width, height, gutterTop, horizontalLabelWidth, verticalLabelHeight, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, + } = config; + + const { innerHeight, baseHeight, labelWidth, midPoint, barWidth } = this.barPosSetup(config); + + return data.map((value, i) => { + const barHeight = this.calcHeight(value, data, innerHeight); + const x = horizontalLabelWidth + paddingLeft + labelWidth * i + midPoint; + const y = baseHeight - barHeight + gutterTop + paddingTop - 2; return ( + {this.renderDefs({ ...config, - ...this.props.chartConfig + ...this.props.chartConfig, })} @@ -141,7 +184,6 @@ class BarChart extends AbstractChart { ? this.renderHorizontalLines({ ...config, count: segments, - paddingTop }) : null} @@ -151,8 +193,6 @@ class BarChart extends AbstractChart { ...config, count: segments, data: data.datasets[0].data, - paddingTop, - paddingRight }) : null} @@ -161,9 +201,7 @@ class BarChart extends AbstractChart { ? this.renderVerticalLabels({ ...config, labels: data.labels, - paddingRight, - paddingTop, - horizontalOffset: barWidth * this.getBarPercentage() + midPoint: labelWidth / 2, }) : null} @@ -171,8 +209,6 @@ class BarChart extends AbstractChart { {this.renderBars({ ...config, data: data.datasets[0].data, - paddingTop, - paddingRight })} @@ -180,8 +216,6 @@ class BarChart extends AbstractChart { this.renderValuesOnTopOfBars({ ...config, data: data.datasets[0].data, - paddingTop, - paddingRight })} @@ -189,8 +223,6 @@ class BarChart extends AbstractChart { this.renderBarTops({ ...config, data: data.datasets[0].data, - paddingTop, - paddingRight })} @@ -199,4 +231,8 @@ class BarChart extends AbstractChart { } } +BarChart.defaultProps = { + barWidth: 32, +} + export default BarChart; diff --git a/src/contribution-graph/index.js b/src/contribution-graph/index.js index be958343..ea45e1b9 100755 --- a/src/contribution-graph/index.js +++ b/src/contribution-graph/index.js @@ -14,32 +14,26 @@ import { convertToDate } from "./dateHelpers"; -const SQUARE_SIZE = 20; -const MONTH_LABEL_GUTTER_SIZE = 8; -const paddingLeft = 32; - function mapValue(x, in_min, in_max, out_min, out_max) { - return ((x - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min; + const diff = (in_max - in_min) || 1; //prevent divided by 0 if in_max == in_min + return ((x - in_min) * (out_max - out_min)) / diff + out_min; } class ContributionGraph extends AbstractChart { constructor(props) { super(props); - let { maxValue, minValue, valueCache } = this.getValueCache(props.values); - this.state = { - maxValue, - minValue, - valueCache + maxValue: +Infinity, + minValue: -Infinity, + valueCache: {}, }; } - UNSAFE_componentWillReceiveProps(nextProps) { + setupValueCacheFromProps() { let { maxValue, minValue, valueCache } = this.getValueCache( - nextProps.values + this.props.values ); - this.setState({ maxValue, minValue, @@ -47,19 +41,84 @@ class ContributionGraph extends AbstractChart { }); } + componentDidMount() { + this.setupValueCacheFromProps(); + } + + componentDidUpdate(prevProps, prevState) { + const large = (prevProps.values.length < this.props.values.length) ? this.props.values : prevProps.values; + const small = (prevProps.values.length < this.props.values.length) ? prevProps.values : this.props.values; + var res = large.filter(item1 => + !small.some(item2 => (item2['date'] === item1['date'] && item2['value'] === item1['value']))); + if(res.length > 0) { + this.setupValueCacheFromProps(); + } + } + + setContentLayout(contentWidth, contentHeight) { + const { width, height } = this.props; + const { paddingTop, paddingLeft, paddingRight, paddingBottom, justifyContent, alignItems } = this.props.chartConfig.chartStyle; + var justifyOffset, alignOffset; + + // vertical + switch(justifyContent) { + case "start": justifyOffset = 0 + paddingTop; break; + case "center": justifyOffset = (height - contentHeight) / 2; break; + case "end": justifyOffset = height - contentHeight - paddingBottom; break; + default: justifyOffset = 0; + } + // horizontal + switch(alignItems) { + case "start": alignOffset = 0 + paddingLeft; break; + case "center": alignOffset = (width - contentWidth) / 2; break; + case "end": alignOffset = width - contentWidth - paddingRight; break; + default: alignOffset = 0; + } + + return [justifyOffset, alignOffset]; + } + + mainLayoutSetup() { + const { paddingTop, paddingLeft, paddingRight, paddingBottom } = this.props.chartConfig.chartStyle; + const [x,y] = this.getViewBox(); + this.props.width = this.props.width > paddingLeft + paddingRight + x ? this.props.width : paddingLeft + paddingRight + x; + this.props.height = this.props.height > paddingTop + paddingBottom + y ? this.props.height : paddingTop + paddingBottom + y; + } + + setDynamicSquareSize() { + const { paddingTop, paddingLeft, paddingRight, paddingBottom } = this.props.chartConfig.chartStyle; + const innerHeight = this.props.height - paddingTop - paddingBottom; + const innerWidth = this.props.width - paddingLeft - paddingRight; + const labelOrientedSize = (DAYS_IN_WEEK - 1) * this.props.gutterSize + this.props.month_label_gutter_size; + const WeekOrientedSize = (this.getWeekCount() - 1) * this.props.gutterSize; + + if(this.props.horizontal) { + // + 1 becuase label itself is + const height = (innerHeight - labelOrientedSize) / (DAYS_IN_WEEK + 1); + const width = (innerWidth - WeekOrientedSize) / this.getWeekCount(); + return height < width ? height : width; + }else { + const height = (innerHeight - WeekOrientedSize) / this.getWeekCount(); + // minus extra gutter size and divided by extra 1 respected to getMonthLabelSize() for vertical layout + const width = (innerWidth - labelOrientedSize - this.props.month_label_gutter_size) / (DAYS_IN_WEEK + 2); + return height < width ? height : width; + } + return 0; + } + getSquareSizeWithGutter() { - return (this.props.squareSize || SQUARE_SIZE) + this.props.gutterSize; + return this.props.squareSize + this.props.gutterSize; } getMonthLabelSize() { - let { squareSize = SQUARE_SIZE } = this.props; + let { squareSize } = this.props; if (!this.props.showMonthLabels) { return 0; } if (this.props.horizontal) { - return squareSize + MONTH_LABEL_GUTTER_SIZE; + return squareSize + this.props.month_label_gutter_size; } - return 2 * (squareSize + MONTH_LABEL_GUTTER_SIZE); + return 2 * (squareSize + this.props.month_label_gutter_size); } getStartDate() { @@ -114,10 +173,9 @@ class ContributionGraph extends AbstractChart { return { valueCache: values.reduce((memo, value) => { const date = convertToDate(value.date); - const index = Math.floor( + const index = Math.ceil( (date - this.getStartDateWithEmptyDays()) / MILLISECONDS_IN_ONE_DAY ); - minValue = Math.min(value[this.props.accessor], minValue); maxValue = Math.max(value[this.props.accessor], maxValue); @@ -189,16 +247,20 @@ class ContributionGraph extends AbstractChart { getTransformForWeek(weekIndex) { if (this.props.horizontal) { - return [weekIndex * this.getSquareSizeWithGutter(), 50]; + const [justifyOffset, alignOffset] = this.setContentLayout(this.getWidth(), this.getHeight()); + return [weekIndex * this.getSquareSizeWithGutter() + alignOffset, + this.getMonthLabelSize() + justifyOffset]; } - return [10, weekIndex * this.getSquareSizeWithGutter()]; + const [justifyOffset, alignOffset] = this.setContentLayout(this.getHeight(), this.getWidth()); + return [this.getMonthLabelSize() + alignOffset, + weekIndex * this.getSquareSizeWithGutter() + justifyOffset]; } getTransformForMonthLabels() { if (this.props.horizontal) { return null; } - return `${this.getWeekWidth() + MONTH_LABEL_GUTTER_SIZE}, 0`; + return `${this.getWeekWidth() + this.props.month_label_gutter_size}, 0`; } getTransformForAllWeeks() { @@ -210,9 +272,9 @@ class ContributionGraph extends AbstractChart { getViewBox() { if (this.props.horizontal) { - return `${this.getWidth()} ${this.getHeight()}`; + return [this.getWidth(), this.getHeight()]; } - return `${this.getHeight()} ${this.getWidth()}`; + return [this.getHeight(), this.getWidth()]; } getSquareCoordinates(dayIndex) { @@ -223,16 +285,19 @@ class ContributionGraph extends AbstractChart { } getMonthLabelCoordinates(weekIndex) { + const { paddingTop, paddingLeft, paddingRight, paddingBottom, justifyContent, alignItems } = this.props.chartConfig.chartStyle; if (this.props.horizontal) { + const [justifyOffset, alignOffset] = this.setContentLayout(this.getWidth(), this.getHeight()); return [ - weekIndex * this.getSquareSizeWithGutter(), - this.getMonthLabelSize() - MONTH_LABEL_GUTTER_SIZE + weekIndex * this.getSquareSizeWithGutter() + alignOffset, + this.getMonthLabelSize() - this.props.month_label_gutter_size + justifyOffset ]; } const verticalOffset = -2; + const [justifyOffset, alignOffset] = this.setContentLayout(this.getHeight(), this.getWidth()); return [ - 0, - (weekIndex + 1) * this.getSquareSizeWithGutter() + verticalOffset + 0 + alignOffset, + (weekIndex + 1) * this.getSquareSizeWithGutter() + verticalOffset + justifyOffset ]; } @@ -246,25 +311,50 @@ class ContributionGraph extends AbstractChart { } const [x, y] = this.getSquareCoordinates(dayIndex); - const { squareSize = SQUARE_SIZE } = this.props; + const { squareSize } = this.props; return ( - { - this.handleDayPress(index); - }} - {...this.getTooltipDataAttrsForIndex(index)} - /> + toggleColor={this.props.chartConfig.toggleColor} + from={ (toggleVisible, fill=this.getClassNameForIndex(index)) => + {this.handleDayPress(index);}} + onPressIn={toggleVisible} + onPressOut={toggleVisible} + title={this.getTitleForIndex(index)} + fill={fill} + /> + } + toggleTooltip={this.props.toggleTooltip} + > + { + this.squareTooltip({index, x, y}) + } + ); } + squareTooltip(args) { + const { index, x, y } = args; + if(!this.props.toggleTooltip && this.props.tooltipContent){ + return; + } + + const dateInfo = this.state.valueCache[index] && this.state.valueCache[index].value + ? this.state.valueCache[index].value + : { + [this.props.accessor]: 0, + date: new Date(this.getStartDate().valueOf() + + index * MILLISECONDS_IN_ONE_DAY).toISOString().split('T')[0] + } + return this.props.tooltipContent(dateInfo, args); + } + handleDayPress(index) { if (!this.props.onDayPress) { return; @@ -276,7 +366,7 @@ class ContributionGraph extends AbstractChart { [this.props.accessor]: 0, date: new Date( this.getStartDate().valueOf() + index * MILLISECONDS_IN_ONE_DAY - ) + ).toISOString().split('T')[0] } ); } @@ -299,6 +389,7 @@ class ContributionGraph extends AbstractChart { } renderMonthLabels() { + const { paddingTop, paddingBottom, paddingLeft, paddingRight } = this.props.chartConfig.chartStyle; if (!this.props.showMonthLabels) { return null; } @@ -309,11 +400,12 @@ class ContributionGraph extends AbstractChart { (weekIndex + 1) * DAYS_IN_WEEK ); const [x, y] = this.getMonthLabelCoordinates(weekIndex); + return endOfWeek.getDate() >= 1 && endOfWeek.getDate() <= DAYS_IN_WEEK ? ( {this.props.getMonthLabel @@ -326,11 +418,45 @@ class ContributionGraph extends AbstractChart { render() { const { style = {} } = this.props; - let { borderRadius = 0 } = style; - if (!borderRadius && this.props.chartConfig.style) { - const stupidXo = this.props.chartConfig.style.borderRadius; - borderRadius = stupidXo; + const defaultChartStyle = { + borderRadius: 0, + paddingTop: 0, + paddingBottom: 0, + paddingRight: 0, + paddingLeft: 0, + justifyContent: 'start', + justifyOffset: 0, + alignItems: 'start', + alignOffset: 0, + } + + this.props.chartConfig = { + ...this.props.chartConfig, + chartStyle: { + ...defaultChartStyle, + ...this.props.chartConfig.chartStyle, + }, } + + // setup dynamic size if user does not provide their own squareSize + this.props.squareSize = this.props.squareSize || this.setDynamicSquareSize() || 20; + this.mainLayoutSetup(); + + // reserved code for future if want to handle special gesture events + // this._panResponder = PanResponder.create({ + // onStartShouldSetPanResponder: (evt, gestureState) => true, + // onStartShouldSetPanResponderCapture: (evt, gestureState) => true, + // onMoveShouldSetPanResponder: (evt, gestureState) => true, + // onMoveShouldSetPanResponderCapture: (evt, gestureState) => true, + // onPanResponderGrant: (evt, gestureState) => { + // console.log(evt.nativeEvent.locationX,evt.nativeEvent.locationY); + // }, + // onPanResponderMove: (evt, gestureState) => { + // // X position relative to the page + // console.log(evt.nativeEvent.locationX,evt.nativeEvent.locationY); + // } + // }); + return ( @@ -342,8 +468,8 @@ class ContributionGraph extends AbstractChart { {this.renderMonthLabels()} @@ -354,16 +480,41 @@ class ContributionGraph extends AbstractChart { } } +class Tooltip extends React.Component { + constructor(props) { + super(props); + this.state = { + isVisible: false + }; + } + + toggleVisible = () => { + this.setState(prevState => ({isVisible: !prevState.isVisible})); + } + render() { + const fill = this.props.toggleTooltip && this.state.isVisible ? this.props.toggleColor : undefined; + + return ( + <> + { this.props.from(this.toggleVisible, fill) } + { this.props.toggleTooltip && this.state.isVisible ? this.props.children : null } + + ) + } +} + ContributionGraph.defaultProps = { numDays: 200, endDate: new Date(), gutterSize: 1, - squareSize: SQUARE_SIZE, + month_label_gutter_size: 8, + squareSize: 0, horizontal: true, showMonthLabels: true, showOutOfRangeDays: false, accessor: "count", - classForValue: value => (value ? "black" : "#8cc665") + toggleTooltip: false, + classForValue: value => (value ? "black" : "#8cc665"), }; export default ContributionGraph; diff --git a/src/line-chart/line-chart.js b/src/line-chart/line-chart.js index ed3aa11f..b328db57 100644 --- a/src/line-chart/line-chart.js +++ b/src/line-chart/line-chart.js @@ -18,6 +18,12 @@ import { import AbstractChart from "../abstract-chart"; import { LegendItem } from "./legend-item"; +const GRAPH_RATIO = { + gutterTop: 0.1 , + horizontalLabelWidth: 0.2, + verticalLabelHeight: 0.15, +} + let AnimatedCircle = Animated.createAnimatedComponent(Circle); class LineChart extends AbstractChart { @@ -27,7 +33,7 @@ class LineChart extends AbstractChart { scrollableDotHorizontalOffset: new Animated.Value(0) }; - getColor = (dataset, opacity) => { + getColor = (dataset={}, opacity) => { return (dataset.color || this.props.chartConfig.color)(opacity); }; @@ -38,6 +44,26 @@ class LineChart extends AbstractChart { getDatas = data => data.reduce((acc, item) => (item.data ? [...acc, ...item.data] : acc), []); + linePositionHelper = config => { + const { + width, + height, + data, + linejoinType, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, + verticalLabelHeight, + horizontalLabelWidth, + gutterTop, + } = config; + + const datas = this.getDatas(data); + const innerHeight = height - paddingTop - paddingBottom - verticalLabelHeight - gutterTop; + const innerWidth = width - horizontalLabelWidth - paddingRight - paddingLeft; + const baseHeight = this.calcBaseHeight(datas, innerHeight) + + return { datas, innerHeight, innerWidth, baseHeight }; + } + getPropsForDots = (x, i) => { const { getDotProps, chartConfig = {} } = this.props; if (typeof getDotProps === "function") { @@ -46,18 +72,20 @@ class LineChart extends AbstractChart { const { propsForDots = {} } = chartConfig; return { r: "4", ...propsForDots }; }; + renderDots = config => { const { data, width, height, - paddingTop, - paddingRight, - onDataPointClick + onDataPointClick, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, + verticalLabelHeight, + horizontalLabelWidth, + gutterTop, } = config; const output = []; - const datas = this.getDatas(data); - const baseHeight = this.calcBaseHeight(datas, height); + const { datas, innerHeight, innerWidth, baseHeight } = this.linePositionHelper(config); const { getDotColor, hidePointsAtIndex = [], @@ -73,11 +101,10 @@ class LineChart extends AbstractChart { if (hidePointsAtIndex.includes(i)) { return; } - const cx = - paddingRight + (i * (width - paddingRight)) / dataset.data.length; - const cy = - ((baseHeight - this.calcHeight(x, datas, height)) / 4) * 3 + - paddingTop; + const lineHeight = this.calcHeight(x, datas, innerHeight); + const gapWidth = innerWidth / dataset.data.length; + const cx = i * gapWidth + horizontalLabelWidth + paddingLeft; + const cy = baseHeight - lineHeight + gutterTop + paddingTop; const onPress = () => { if (!onDataPointClick || hidePointsAtIndex.includes(i)) { return; @@ -126,25 +153,25 @@ class LineChart extends AbstractChart { data, width, height, - paddingTop, - paddingRight, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, scrollableDotHorizontalOffset, scrollableDotFill, scrollableDotStrokeColor, scrollableDotStrokeWidth, scrollableDotRadius, - scrollableInfoViewStyle, - scrollableInfoTextStyle, - scrollableInfoSize, - scrollableInfoOffset + scrollableInfoViewStyle = {}, + scrollableInfoTextStyle = {}, + scrollableInfoSize = {height:20, width:20}, + scrollableInfoOffset = 10, + verticalLabelHeight, + horizontalLabelWidth, + gutterTop, } = config; const output = []; - const datas = this.getDatas(data); - const baseHeight = this.calcBaseHeight(datas, height); - + const { datas, innerHeight, innerWidth, baseHeight } = this.linePositionHelper(config); let vl = []; - const perData = width / data[0].data.length; + const perData = innerWidth / data[0].data.length; for (let index = 0; index < data[0].data.length; index++) { vl.push(index * perData); } @@ -206,7 +233,7 @@ class LineChart extends AbstractChart { data.forEach(dataset => { if (dataset.withScrollableDot == false) return; - const perData = width / dataset.data.length; + const perData = innerWidth / dataset.data.length; let values = []; let yValues = []; let xValues = []; @@ -216,26 +243,15 @@ class LineChart extends AbstractChart { for (let index = 0; index < dataset.data.length; index++) { values.push(index * perData); - const yval = - ((baseHeight - - this.calcHeight( - dataset.data[dataset.data.length - index - 1], - datas, - height - )) / - 4) * - 3 + - paddingTop; + + const lineHeight = this.calcHeight(dataset.data[dataset.data.length - index - 1], datas, innerHeight); + const gapWidth = innerWidth / dataset.data.length; + const yval = baseHeight - lineHeight + gutterTop + paddingTop; + const xval = paddingLeft + horizontalLabelWidth + (dataset.data.length - index - 1) * gapWidth; + yValues.push(yval); - const xval = - paddingRight + - ((dataset.data.length - index - 1) * (width - paddingRight)) / - dataset.data.length; xValues.push(xval); - - yValuesLabel.push( - yval - (scrollableInfoSize.height + scrollableInfoOffset) - ); + yValuesLabel.push(yval + scrollableInfoOffset); xValuesLabel.push(xval - scrollableInfoSize.width / 2); } @@ -308,16 +324,15 @@ class LineChart extends AbstractChart { return this.renderBezierShadow(config); } - const { - data, - width, - height, - paddingRight, - paddingTop, - useColorFromDataset + const { data, width, height, useColorFromDataset, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, + verticalLabelHeight, + horizontalLabelWidth, + gutterTop, } = config; - const datas = this.getDatas(data); - const baseHeight = this.calcBaseHeight(datas, height); + + const { datas, innerHeight, innerWidth, baseHeight } = this.linePositionHelper(config); + return config.data.map((dataset, index) => { return ( { - const x = - paddingRight + - (i * (width - paddingRight)) / dataset.data.length; - const y = - ((baseHeight - this.calcHeight(d, datas, height)) / 4) * 3 + - paddingTop; + const lineHeight = this.calcHeight(d, datas, innerHeight); + const gapWidth = innerWidth / dataset.data.length; + const x = paddingLeft + horizontalLabelWidth + i * gapWidth; + const y = baseHeight - lineHeight + gutterTop + paddingTop; return `${x},${y}`; }) .join(" ") + - ` ${paddingRight + - ((width - paddingRight) / dataset.data.length) * - (dataset.data.length - 1)},${(height / 4) * 3 + - paddingTop} ${paddingRight},${(height / 4) * 3 + paddingTop}` + ` ${paddingLeft + horizontalLabelWidth + (innerWidth / dataset.data.length) * (dataset.data.length - 1)}, + ${paddingTop + gutterTop + innerHeight} ${paddingLeft + horizontalLabelWidth}, + ${paddingTop + gutterTop + innerHeight}` } fill={`url(#fillShadowGradient${ useColorFromDataset ? `_${index}` : "" @@ -356,23 +368,24 @@ class LineChart extends AbstractChart { const { width, height, - paddingRight, - paddingTop, data, - linejoinType + linejoinType, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, + verticalLabelHeight, + horizontalLabelWidth, + gutterTop, } = config; + const output = []; - const datas = this.getDatas(data); - const baseHeight = this.calcBaseHeight(datas, height); + + const { datas, innerHeight, innerWidth, baseHeight } = this.linePositionHelper(config); let lastPoint; data.forEach((dataset, index) => { const points = dataset.data.map((d, i) => { - if (d === null) return lastPoint; - const x = - (i * (width - paddingRight)) / dataset.data.length + paddingRight; - const y = - ((baseHeight - this.calcHeight(d, datas, height)) / 4) * 3 + - paddingTop; + const lineHeight = this.calcHeight(d, datas, innerHeight); + const gapWidth = innerWidth / dataset.data.length; + const x = i * gapWidth + horizontalLabelWidth + paddingLeft; + const y = baseHeight - lineHeight + gutterTop + paddingTop; lastPoint = `${x},${y}`; return `${x},${y}`; }); @@ -392,20 +405,25 @@ class LineChart extends AbstractChart { }; getBezierLinePoints = (dataset, config) => { - const { width, height, paddingRight, paddingTop, data } = config; + const { width, height, data, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, + verticalLabelHeight, + horizontalLabelWidth, + gutterTop, + } = config; + if (dataset.data.length === 0) { return "M0,0"; } - const datas = this.getDatas(data); + const { datas, innerHeight, innerWidth, baseHeight } = this.linePositionHelper(config); + const gapWidth = innerWidth / dataset.data.length; + const x = i => - Math.floor( - paddingRight + (i * (width - paddingRight)) / dataset.data.length - ); - const baseHeight = this.calcBaseHeight(datas, height); + Math.floor(paddingLeft + horizontalLabelWidth + i * gapWidth); const y = i => { - const yHeight = this.calcHeight(dataset.data[i], datas, height); - return Math.floor(((baseHeight - yHeight) / 4) * 3 + paddingTop); + const yHeight = this.calcHeight(dataset.data[i], datas, innerHeight); + return Math.floor(baseHeight - yHeight + paddingTop + gutterTop); }; return [`M${x(0)},${y(0)}`] @@ -440,21 +458,23 @@ class LineChart extends AbstractChart { }; renderBezierShadow = config => { - const { - width, - height, - paddingRight, - paddingTop, - data, - useColorFromDataset + const { width, height, data, useColorFromDataset, + chartStyle: { paddingTop, paddingLeft, paddingRight, paddingBottom }, + verticalLabelHeight, + horizontalLabelWidth, + gutterTop, } = config; + + const { datas, innerHeight, innerWidth, baseHeight } = this.linePositionHelper(config); + return data.map((dataset, index) => { + const gapWidth = innerWidth / dataset.data.length; const d = this.getBezierLinePoints(dataset, config) + - ` L${paddingRight + - ((width - paddingRight) / dataset.data.length) * - (dataset.data.length - 1)},${(height / 4) * 3 + - paddingTop} L${paddingRight},${(height / 4) * 3 + paddingTop} Z`; + ` L${paddingLeft + horizontalLabelWidth + + gapWidth * (dataset.data.length - 1)} + ,${innerHeight + gutterTop + paddingTop} L${paddingLeft + horizontalLabelWidth}, + ${innerHeight + gutterTop + paddingTop} Z`; return ( yLabel, formatXLabel = xLabel => xLabel, segments, transparent = false, chartConfig = {} } = this.props; + + const defaultChartStyle = { + borderRadius: 0, + paddingTop: 0, + paddingBottom: 0, + paddingRight: 0, + paddingLeft: 0, + } + const { scrollableDotHorizontalOffset } = this.state; const { labels = [] } = data; - const { - borderRadius = 0, - paddingTop = 16, - paddingRight = 64, - margin = 0, - marginRight = 0, - paddingBottom = 0 - } = style; const config = { width, height, + chartStyle: { + ...defaultChartStyle, + ...this.props.chartConfig.chartStyle, + }, verticalLabelRotation, horizontalLabelRotation }; + //auto dynamic size if user dont set the following props + config.gutterTop = this.props.chartConfig.gutterTop ?? + (height - config.chartStyle.paddingTop - config.chartStyle.paddingBottom) * GRAPH_RATIO.gutterTop; + + config.horizontalLabelWidth = this.props.chartConfig.horizontalLabelWidth ?? + (width - config.chartStyle.paddingRight - config.chartStyle.paddingLeft) * GRAPH_RATIO.horizontalLabelWidth; + + config.verticalLabelHeight = this.props.chartConfig.verticalLabelHeight ?? + (height - config.chartStyle.paddingTop - config.chartStyle.paddingBottom) * GRAPH_RATIO.verticalLabelHeight; + const datas = this.getDatas(data.datasets); - let count = Math.min(...datas) === Math.max(...datas) ? 1 : 4; + let count = Math.min(...datas) === Math.max(...datas) ? 2 : 5; if (segments) { count = segments; } @@ -539,20 +575,22 @@ class LineChart extends AbstractChart { return ( - {this.props.data.legend && - this.renderLegend(config.width, legendOffset)} - + { + this.props.data.legend && + this.renderLegend(config.width, legendOffset) + } + {this.renderDefs({ ...config, ...chartConfig, @@ -563,14 +601,6 @@ class LineChart extends AbstractChart { ? this.renderHorizontalLines({ ...config, count: count, - paddingTop, - paddingRight - }) - : withOuterLines - ? this.renderHorizontalLine({ - ...config, - paddingTop, - paddingRight }) : null} @@ -580,8 +610,6 @@ class LineChart extends AbstractChart { ...config, count: count, data: datas, - paddingTop, - paddingRight, formatYLabel, decimalPlaces: chartConfig.decimalPlaces }) @@ -592,14 +620,6 @@ class LineChart extends AbstractChart { ? this.renderVerticalLines({ ...config, data: data.datasets[0].data, - paddingTop, - paddingRight - }) - : withOuterLines - ? this.renderVerticalLine({ - ...config, - paddingTop, - paddingRight }) : null} @@ -608,19 +628,15 @@ class LineChart extends AbstractChart { ? this.renderVerticalLabels({ ...config, labels, - paddingRight, - paddingTop, formatXLabel }) : null} {this.renderLine({ - ...config, ...chartConfig, - paddingRight, - paddingTop, - data: data.datasets + ...config, + data: data.datasets, })} @@ -628,9 +644,7 @@ class LineChart extends AbstractChart { this.renderShadow({ ...config, data: data.datasets, - paddingRight, - paddingTop, - useColorFromDataset: chartConfig.useShadowColorFromDataset + useColorFromDataset: chartConfig.useShadowColorFromDataset, })} @@ -638,19 +652,15 @@ class LineChart extends AbstractChart { this.renderDots({ ...config, data: data.datasets, - paddingTop, - paddingRight, onDataPointClick })} {withScrollableDot && this.renderScrollableDot({ - ...config, ...chartConfig, + ...config, data: data.datasets, - paddingTop, - paddingRight, onDataPointClick, scrollableDotHorizontalOffset })} @@ -660,13 +670,12 @@ class LineChart extends AbstractChart { decorator({ ...config, data: data.datasets, - paddingTop, - paddingRight })} - {withScrollableDot && ( + { + withScrollableDot && ( - )} + ) + } ); }