Search code examples
reactjsreact-nativereact-native-svgreact-native-chart-kit

How do I add Gradient Colour to this React Native Chart?


Current Implementation:

enter image description here

This is a Chart using react-native-svg library with a tool-tip added to it. I want to make the color of the line into a gradient rather than a single color

Code:

import React, { useState } from 'react'
import { View, Text, Dimensions } from 'react-native'
import { LineChart } from 'react-native-chart-kit'
import { Rect, Text as TextSVG, Svg } from "react-native-svg";

const Charts = () => {
    let [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0, visible: false, value: 0 })

    return (
        <View>
            <LineChart
                data={{
                    labels: ["January", "February", "March", "April", "May", "June"],
                    datasets: [
                        {
                            data: [
                                100, 110, 90, 130, 80, 103
                            ]
                        }
                    ]
                }}
                width={Dimensions.get("window").width}
                height={250}
                yAxisLabel="$"
                yAxisSuffix="k"
                yAxisInterval={1}
                chartConfig={{
                    backgroundColor: "white",
                    backgroundGradientFrom: "#fbfbfb",
                    backgroundGradientTo: "#fbfbfb",
                    decimalPlaces: 2,
                    color: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
                    labelColor: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
                    style: {
                        borderRadius: 0
                    },
                    propsForDots: {
                        r: "6",
                        strokeWidth: "0",
                        stroke: "#fbfbfb"
                    }
                }}
                bezier
                style={{
                    marginVertical: 8,
                    borderRadius: 6
                }}

                decorator={() => {
                    return tooltipPos.visible ? <View>
                        <Svg>
                            <Rect x={tooltipPos.x - 15} 
                                y={tooltipPos.y + 10} 
                                width="40" 
                                height="30"
                                fill="black" />
                                <TextSVG
                                    x={tooltipPos.x + 5}
                                    y={tooltipPos.y + 30}
                                    fill="white"
                                    fontSize="16"
                                    fontWeight="bold"
                                    textAnchor="middle">
                                    {tooltipPos.value}
                                </TextSVG>
                        </Svg>
                    </View> : null
                }}

                onDataPointClick={(data) => {

                    let isSamePoint = (tooltipPos.x === data.x 
                                        && tooltipPos.y === data.y)

                    isSamePoint ? setTooltipPos((previousState) => {
                        return { 
                                  ...previousState,
                                  value: data.value,
                                  visible: !previousState.visible
                               }
                    })
                        : 
                    setTooltipPos({ x: data.x, value: data.value, y: data.y, visible: true });

                }}
            />
        </View>
    )
}

export default Charts

QUESTION: Instead of the chart having just grey, I want the color of the chart as red when it goes above a specific threshold, yellow for another value and green for the lowest value.

Like this somewhat: enter image description here

What I found:

render() {

    const data = [ 50, 10, 40, 95, -4, -24, 85, 91, 35, 53, -53, 24, 50, -20, -80 ]

    const Gradient = () => (
      <Defs key={'gradient'}>
        <LinearGradient id={'gradient'} x1={'0'} y={'0%'} x2={'100%'} y2={'0%'}>
          <Stop offset={'0%'} stopColor={'rgb(134, 65, 244)'}/>
          <Stop offset={'100%'} stopColor={'rgb(66, 194, 244)'}/>
        </LinearGradient>
      </Defs>
    )

    return (
      <LineChart
        style={ { height: 200 } }
        data={ data }
        contentInset={ { top: 20, bottom: 20 } }
        svg={{
          strokeWidth: 2,
          stroke: 'url(#gradient)',
        }}
      >
        <Grid/>
        <Gradient/>
      </LineChart>
    )
  }

I am not able to integrate the two. Please help.


Solution

  • It's quite involved but we can create our own CustomLineChart component that inherits from the LineChart react-native-chart-kit provides:

    class CustomLineChart extends LineChart {
      render() {
        const {
          width,
          height,
          data,
          withScrollableDot = false,
          withShadow = true,
          withDots = true,
          withInnerLines = true,
          withOuterLines = true,
          withHorizontalLines = true,
          withVerticalLines = true,
          withHorizontalLabels = true,
          withVerticalLabels = true,
          style = {},
          decorator,
          onDataPointClick,
          verticalLabelRotation = 0,
          horizontalLabelRotation = 0,
          formatYLabel = (yLabel) => yLabel,
          formatXLabel = (xLabel) => xLabel,
          segments,
          transparent = false,
          chartConfig,
        } = this.props;
    
        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,
          verticalLabelRotation,
          horizontalLabelRotation,
        };
    
        const datas = this.getDatas(data.datasets);
    
        let count = Math.min(...datas) === Math.max(...datas) ? 1 : 4;
        if (segments) {
          count = segments;
        }
    
        const legendOffset = this.props.data.legend ? height * 0.15 : 0;
    
        return (
          <View style={style}>
            <Svg
              height={height + paddingBottom + legendOffset}
              width={width - margin * 2 - marginRight}>
              <Defs>
                <LinearGradient id="grad" x1="0" y1="0" x2="0" y2="1">
                  <Stop offset="0" stopColor="red" stopOpacity="1" />
                  <Stop offset="1" stopColor="blue" stopOpacity="1" />
                </LinearGradient>
              </Defs>
              <Rect
                width="100%"
                height={height + legendOffset}
                rx={borderRadius}
                ry={borderRadius}
                fill="white"
                fillOpacity={transparent ? 0 : 1}
              />
              {this.props.data.legend &&
                this.renderLegend(config.width, legendOffset)}
              <G x="0" y={legendOffset}>
                <G>
                  {withHorizontalLines &&
                    (withInnerLines
                      ? this.renderHorizontalLines({
                          ...config,
                          count: count,
                          paddingTop,
                          paddingRight,
                        })
                      : withOuterLines
                      ? this.renderHorizontalLine({
                          ...config,
                          paddingTop,
                          paddingRight,
                        })
                      : null)}
                </G>
                <G>
                  {withHorizontalLabels &&
                    this.renderHorizontalLabels({
                      ...config,
                      count: count,
                      data: datas,
                      paddingTop: paddingTop,
                      paddingRight: paddingRight,
                      formatYLabel,
                      decimalPlaces: chartConfig.decimalPlaces,
                    })}
                </G>
                <G>
                  {withVerticalLines &&
                    (withInnerLines
                      ? this.renderVerticalLines({
                          ...config,
                          data: data.datasets[0].data,
                          paddingTop: paddingTop,
                          paddingRight: paddingRight,
                        })
                      : withOuterLines
                      ? this.renderVerticalLine({
                          ...config,
                          paddingTop: paddingTop,
                          paddingRight: paddingRight,
                        })
                      : null)}
                </G>
                <G>
                  {withVerticalLabels &&
                    this.renderVerticalLabels({
                      ...config,
                      labels,
                      paddingTop: paddingTop,
                      paddingRight: paddingRight,
                      formatXLabel,
                    })}
                </G>
                <G>
                  {this.renderLine({
                    ...config,
                    ...chartConfig,
                    paddingRight: paddingRight,
                    paddingTop: paddingTop,
                    data: data.datasets,
                  })}
                </G>
                <G>
                  {withDots &&
                    this.renderDots({
                      ...config,
                      data: data.datasets,
                      paddingTop: paddingTop,
                      paddingRight: paddingRight,
                      onDataPointClick,
                    })}
                </G>
                <G>
                  {withScrollableDot &&
                    this.renderScrollableDot({
                      ...config,
                      ...chartConfig,
                      data: data.datasets,
                      paddingTop: paddingTop,
                      paddingRight: paddingRight,
                      onDataPointClick,
                      scrollableDotHorizontalOffset,
                    })}
                </G>
                <G>
                  {decorator &&
                    decorator({
                      ...config,
                      data: data.datasets,
                      paddingTop,
                      paddingRight,
                    })}
                </G>
              </G>
            </Svg>
            {withScrollableDot && (
              <ScrollView
                style={StyleSheet.absoluteFill}
                contentContainerStyle={{width: width * 2}}
                showsHorizontalScrollIndicator={false}
                scrollEventThrottle={16}
                onScroll={Animated.event([
                  {
                    nativeEvent: {
                      contentOffset: {x: scrollableDotHorizontalOffset},
                    },
                  },
                ])}
                horizontal
                bounces={false}
              />
            )}
          </View>
        );
      }
    }
    
    function App() {
      return (
        <CustomLineChart
          data={{
            labels: ['January', 'February', 'March', 'April', 'May', 'June'],
            datasets: [
              {
                data: [100, 110, 90, 130, 80, 103],
              },
            ],
          }}
          width={Dimensions.get('window').width}
          height={250}
          chartConfig={{
            backgroundGradientFrom: '#fbfbfb',
            backgroundGradientTo: '#fbfbfb',
            color: (opacity = 1) => 'url(#grad)',
            labelColor: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
          }}
          bezier
          withInnerLines={false}
          withOuterLines={false}
        />
      );
    }
    

    Most of the code in the render the same as the official LineChart component (https://github.com/indiespirit/react-native-chart-kit/blob/master/src/line-chart/LineChart.tsx).

    The important parts in this code are the linear gradient that is added and this line in the chartConfig:

    color: (opacity = 1) => 'url(#grad)'
    

    The color refers to the LinearGradient we've defined by its id grad.


    The result looks something like this:

    showcase


    You can also add more colors and play around with the offset prop of each.

    You can also use percentages for the gradient, but it's recommended to use exact values as it has performance advantages over using percentages according to the documentation: https://github.com/react-native-community/react-native-svg#lineargradient.


    Important to note is that this implementation doesn't have a fillShadowGradient as a background and it doesn't implement the shadow. I've also left out some of your other code like the tooltip code that isn't relevant to the question.