Javascript required
Skip to content Skip to sidebar Skip to footer

Victory Domain Padding Cannot Read Property Concat

photo by William Iven on Unsplash

Recently, I was assigned to a project where nosotros needed to build a graph with some pretty lofty specs. D3 is often considered the best graphing library for frontend developers, only on a cursory glance, the documentation was a flake unclear and seemed denser than I had time to parse, given the scope and timeframe allotted. I shopped around for other graphing libraries and came across Recharts, a React-specific graphing library built on summit of D3 that had been used more oftentimes and had a better rating than Nivo, Victory, or react-vis.

While I know adept documentation for D3 exists and there are widespread use cases of it in the wild, Recharts piqued my interest. The website for it was snappy and clean, the documentation seemed pretty robust, at that place were a good number of customizable props, and it followed the aforementioned blueprint of importing components and giving them prop values, which would reduce the amount of time I'd have to spend bootstrapping. And then Recharts was the winner.

The Project

The customer for this projection gave us the following specs:

  • The graph is a bar nautical chart that tin brandish both positive and negative values with the domain limited to -2000 to +2000.
  • Bars with positive values should be blue, and bars with negative values should be scarlet.
  • Confined should be selectable.
  • When a bar is selected, information technology is a slightly darker color, and a tooltip should appear over information technology, which displays the bar'due south value.
  • If a bar's value exceeds -2000 or +2000, it remains capped at that peak and has a perpetually visible characterization displaying its actual value.
  • Bars with large values in either the positive or negative direction are a more vibrant blue or red than unselected or selected colour.
  • The graph volition continually grow to the right, so its container will gyre once it reaches a certain length.
  • The app that utilizes this graph is primarily for use on a tablet through a spider web browser.
  • The Y-axis should remain static while you lot're scrolling through the 10-axis.
  • The characterization for the information signal counts upwardly incrementally by i.
  • When the userId changes, i.eastward. the event is recorded by User B rather than User A, the label for the series starts back at i and counts upward from there.

And this is what the graph ended up looking similar:

Screenshot of a graph with red negative bars and blue positive bars

The Limitations of Recharts

While using this library to fulfill the requirements, I ran into a few roadblocks:

  • The built in <Tooltip /> component for Recharts only works on hover, which isn't feasible for a tablet.
  • While y'all can specify a domain for your graph, the graph does not truncate bar values to autumn within the domain. That is to say, if the maximum of the domain was 2000, and the value of a given information indicate was 2500, the positive side of the axis would automatically scale to 2500.
  • The conditional colour of the bars could non be handled simply in the component. It required a pure function to summate the color for the fill up of the object.
  • Many of the customizable props aren't documented with examples in Recharts. The solutions I came upwards with to meet the specs were based on reading the Github Issues for the library'southward repository, or looking at third-party examples on CodePen and jsFiddle.
  • The Y-axis component has no "static" property. If you lot coil along the 10-axis, the Y-axis will eventually coil out of view

So How Did I Exercise It?

Transforming the data

In a perfect globe, the data from the backend would come in exactly the correct shape to be easily consumed past Recharts graph, but alas, it did not. The information started like this:

                          interface              ResultShape              {              readonly              resultId:              cord              readonly              userId:              cord              readonly              score:              number              }              const              results              =              [              {              resultId:              "22f0-23as9-9adf-98sfd"              ,              userId:              "user1"              ,              score:              1000              ,              }              ,              {              resultId:              "18q0-23as3-9adq-98sq"              ,              userId:              "user1"              ,              score:              -              200              ,              }              ,              {              resultId:              "9748-2737-1747-7759"              ,              userId:              "user2"              ,              score:              1500              ,              }              ,              ]                      

And using this function:

                          interface              Coordinate              {              readonly              label:              string              readonly              value:              number              }              export              const              convertTablesResultsToGraph              =              (              results:              ReadonlyArray<ResultShape>              ,              )              :              ReadonlyArray<Coordinate>              =>              {              // create an ordered sort assortment of userIds              const              userIds              =              results.              map              (              r              =>              r.userId)              const              sortOrderOfUserIds              =              [              ...              new              Gear up              (              [              ...userIds]              )              ]              // make a basemap of all results by userId              const              basemap              =              results.              reduce              (              (acc,              side by side)              :              BaseMap<              ReadonlyArray<Coordinate>              >              =>              {              const              id              =              next.userId              const              contents              =              !              !acc[id]              ?              [              ...acc[id]              ,              next]              :              [side by side]              const              toReturn              =              {              ...acc,              [id]              :              contents,              }              render              toReturn              }              ,              {              }              )              // convert this to an array of arrays.              const              basemaps:              ReadonlyArray<              ReadonlyArray<ResultShape>              >              =              sortOrderOfUserIds.              map              (              id              =>              basemap[id]              )              const              coordinates:              ReadonlyArray<Coordinate>              =              basemaps              .              map              (              rounds              =>              {              const              cc:              ReadonlyArray<Coordinate>              =              rounds.              map              (              (              item,                i              )              =>              {              const              {              score              }              =              particular              return              {              characterization:                              `                                  ${i                  +                  1                  }                                `                            ,              value:              score,              }              }              )              return              cc              }              )              .              reduce              (              (              acc,                next              )              =>              acc.              concat              (next)              ,              [              ]              )              render              coordinates              }                      

It became this:

                          const              coordinates              =              [              {              characterization:              "1"              ,              value:              2500              }              ,              {              label:              "ii"              ,              value:              100              }              ,              {              label:              "3"              ,              value:              -              1200              }              ,              {              label:              "4"              ,              value:              -              4000              }              ,              {              label:              "5"              ,              value:              1500              }              ,              ]                      

Selecting a bar

Browsing the examples in the Recharts documentation, I came across this jsFiddle, which demonstrated to me that you can employ the component'south local state to track the index of the selected bar. Information technology concluded up looking like this:

                          interface              ResultGraphState              {              readonly              selectedIndex:              number              |              nix              }              ...              constructor              (              props:                ResultGraphProps              )              {              super              (props)              this              .state:              ResultGraphState              =              {              selectedIndex:              null              }              }              ...                      

It also demonstrated a way to make the fill of the bar change color conditionally, using the <Jail cell /> component. In the example, they utilize a ternary, just in my case, I needed a function which could output half dozen unlike colors.

Note: we keep all of our colors in an appColors object equally hexcode strings.

                          export              const              colorSelector              =              (              value:              number              ,              trueValue:              number              ,              index:              number              ,              selectedIndex:              number              |              cypher              ,              )              :                              string                            =>              {              if              (trueValue              >=              2000              )              {              return              appColors.modalPositiveLargeValue              }              if              (trueValue              <=              -              2000              )              {              render              appColors.modalNegativeLargeValue              }              if              (index              !==              selectedIndex)              {              return              value              >              0              ?              appColors.modalPositiveGraphValue              :              appColors.modalNegativeGraphValue              }              return              value              >              0              ?              appColors.modalPositiveGraphValueSelected              :              appColors.modalNegativeGraphValueSelected              }                      

I spun upwardly the click functionality for toggling the choice of the bar in a class method and passed it into the <Bar /> component as shown in the jsFiddle.

Creating and rendering a custom tooltip

The showtime thing I needed for this office was a custom tooltip, which I built using styled-components and this CSS generator as a resource for giving the tooltip its signature pointy flake.

            const CustomTooltip = styled.div`              display              :              flex;              justify-content              :              centre;              align-items              :              heart;     $              {              (              {              isPositive              }              :              DisplayProps)              => !isPositive &&              "margin-pinnacle: 10px;"              }              .tooltip-body              {              width              :              100%;              position              :              relative;         background: $              {appColors.tableDataOverviewBackground}              ;         border: 1px solid $              {appColors.tableLabelText}              ;              text-align              :              center;              padding              :              1px 3px;              edge-radius              :              2px;          &:afterwards              {              content              :              ""              ;              position              :              absolute;              left              :              50%;              width              :              0;              height              :              0;              edge              :              10px solid transparent;              margin-left              :              -10px;             $              {              (              {              isPositive              }              :              DisplayProps)              =>                 isPositive                     ? `bottom              :              0;             border-peak-color: $              {appColors.tableLabelText}              ;              edge-bottom              :              0;              margin-bottom              :              -10px;`              :              `top              :              0;             border-bottom-colour: $              {appColors.tableLabelText}              ;              border-top              :              0;              margin-top              :              -10px;`}              }              p              {              font-size              :              0.75em;             colour: $              {appColors.tableLabelText}              ;              margin              :              0;              marshal-self              :              center;              }              }              `          

I probably could have used the blueprintjs library for the tooltip component, simply I ended upwards making my own to ensure that it would be lightweight and not contain any additional animations or properties I didn't need. The app was already calculating a data transform on a big swath of data, and keeping the graph performant was a key office of the feature.

Originally, I tossed the tooltip inside of the <Bar> component tags and constitute out the hard way that you can't but shoehorn a non-Recharts component as a child to a Recharts component, especially given that the whole Recharts graph is really an SVG. I tried to detect a premade tooltip SVG that was free to utilize, but didn't encounter anything that really matched the mockups or seemed easy to implement.

Upon digging through the documentation, I came across the <LabelList />component, which thankfully can take a React element as a content prop, and fifty-fifty came with some other handy jsFiddle.

This jsFiddle shows that the function that creates the customized label receives props from the parent <LabelList /> component, which contains the x and y coordinates of where the bar sits relative to the SVG, the width and elevation of the bar, and the data's value. From there, I could identify the tooltip and feed it the correct data.

The but problem was trying to render the div. It wasn't showing up, only I knew that something was being rendered based on what was happening in the Chrome dev tools tab. So I started looking into an element called <foreignObject>, which through some magic allows HTML to be nested inside an SVG tag. I added conditional rendering for selected vs. not-selected index with my selectedIndex from state, fed the component, and concluded upward with this:

gif of the graph being clicked on which prompts a tooltip to appear

Rendering the true value of a truncated bar

Initially, I was stumped by this trouble, until I remembered that the graph coordinate objects didn't accept to but be an "X" and "Y" value. The coordinate could whatever shape that I needed it to be, as long equally I specified the dataKey prop in the library components.

I concluded up updating the coordinate shape from this:

                          interface              Coordinate              {              readonly              characterization:              cord              readonly              value:              number              }                      

to this:

                          interface              Coordinate              {              readonly              characterization:              cord              readonly              displayedValue:              number              readonly              trueValue:              number              }                      

and updated my data transform utility like so:

                          export              const              convertTablesResultsToGraph              =              (              results:              ReadonlyArray<ResultShape>              ,              )              :              ReadonlyArray<Coordinate>              =>              {              ...              const              coordinates:              ReadonlyArray<Coordinate>              =              basemaps              .              map              (              rounds              =>              {              const              cc:              ReadonlyArray<Coordinate>              =              rounds.              map              (              (              item,                i              )              =>              {              const              {              score              }              =              item              const              isLargeValue              =              isValueTooLarge              (              score,              2000              ,              )              const              displayedValue              =              calculateDisplayValue              (              score,              isLargeValue,              2000              ,              )              render              {              characterization:                              `                                  ${i                  +                  1                  }                                `                            ,              displayedValue,              trueValue:              score,              }              }              ,              )              return              cc              }              )              .              reduce              (              (              acc,                next              )              =>              acc.              concat              (next)              ,              [              ]              )              render              coordinates              }              ...                      

Using the updated data transformation utility, I was able to write an additional <LabelList> component to track the "true" value of the coordinate while maintaining its height at the very maximum of the graph's domain, which ended upwardly looking like this:

moving image of the graph scrolling to the right and opening up tooltips

Creating a static Y Axis

I volition be the first to admit that this is the hackiest part of the whole process. But information technology performs well, and it looks pretty skilful, if I exercise say so myself.

I opened the modal containing my graph, opened the dev tools, and straight up re-create-pasted the whole <g> grouping of lines that made up the Y-axis, the Y-axis labels, and all of the ticks into a new functional component, which looked something like this (with the bulk of it omitted considering visual clutter):

                          export              const              CustomYAxis              =              (              )              :              JSX              .              Element              =>              {              return              (              <div className=              "y-axis-wrapper"              >              <              SVG              width=              "56"              height=              "360"              viewBox=              "v 0 56 420"              >              <g className=              "recharts-layer recharts-cartesian-axis recharts-yAxis yAxis"              >              ...              <              /g>              <              /              SVG              >              <              /div>              )              }                      

The SVG props for width, pinnacle, and viewBox were copied from the whole Recharts graph, and then modified to trim as much whitespace as possible. In social club to maintain the same sizing and scale, I didn't delete the original <YAxis> from the graph. I just gave information technology the belongings hide={truthful}.

The whole render method for the graph ended up looking like this:

                          return              (              <div className=              "graph-wrapper"              >              <CustomYAxis              /              >              <BarChart              {              ...props}              >              ...              <              /BarChart>              <              /div>              )                      

and the CSS (styled from the parent component) looked like this:

                          > div > .graph-wrapper              {              brandish              :              flex;              flex-direction              :              row;              padding-superlative              :              1em;              max-top              :              435px;              overflow-ten              :              scroll;              overflow-y              :              hidden;      > .y-centrality-wrapper              {              groundwork              :              white;              position              :              glutinous;              left              :              0;              z-index              :              1003;              }              }                      

The result?

Conclusion

In general, I'd say that while Recharts lacked a lot of features I needed to fulfill the specs of my project, it's still a good entry-level graphing library. The animations are super shine (though I turned them off to prevent any potential lag), and at that place are a lot of component props to customize the graph that I'd exist interested in exploring in future projects.

The well-nigh of import, and the most fun part, was getting the take a chance to flex my problem-solving skills. I enjoyed having the opportunity to come up up with ways around the limitations I encountered, and I promise that the documentation of my process helps other people running into problems with Recharts or helps the devs improve the library in the next version.

Anything I missed in my analysis or could've done better? Feel complimentary to reach out in the comments!

eatockdifusest.blogspot.com

Source: https://www.olioapps.com/blog/graph-hacking/