avatarEli Elad Elrom

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

14822

Abstract

arams"></span>) => { <span class="hljs-keyword">const</span> [data, setData] = useState<<span class="hljs-title class_">Types</span>.<span class="hljs-property">Data</span>[]>([{ <span class="hljs-attr">date</span>: <span class="hljs-string">''</span>, <span class="hljs-attr">y</span>: <span class="hljs-number">0</span> }])

<span class="hljs-keyword">const</span> { width, height } = <span class="hljs-title function_">useWindowDimensions</span>()

<span class="hljs-keyword">const</span> dimensions = <span class="hljs-title function_">useRef</span>() <span class="hljs-keyword">as</span> { <span class="hljs-attr">current</span>: <span class="hljs-title class_">Types</span>.<span class="hljs-property">Dimensions</span> } dimensions.<span class="hljs-property">current</span> = <span class="hljs-title class_">LineChartDateBisectorHelper</span>.<span class="hljs-title function_">getDimensions</span>(width * <span class="hljs-number">0.9</span>, height * <span class="hljs-number">0.9</span>, <span class="hljs-number">30</span>, <span class="hljs-number">50</span>, <span class="hljs-number">10</span>, <span class="hljs-number">20</span>)

<span class="hljs-comment">// resize</span> <span class="hljs-title function_">useEffect</span>(<span class="hljs-function">() =></span> { ((dimensions <span class="hljs-keyword">as</span> <span class="hljs-built_in">unknown</span>) <span class="hljs-keyword">as</span> { <span class="hljs-attr">current</span>: <span class="hljs-title class_">Types</span>.<span class="hljs-property">Dimensions</span> }).<span class="hljs-property">current</span> = <span class="hljs-title class_">LineChartDateBisectorHelper</span>.<span class="hljs-title function_">getDimensions</span>(width * <span class="hljs-number">0.9</span>, height * <span class="hljs-number">0.9</span>, <span class="hljs-number">30</span>, <span class="hljs-number">50</span>, <span class="hljs-number">10</span>, <span class="hljs-number">20</span>) <span class="hljs-comment">// console.log(dimensions.current)</span> }, [width, height])

<span class="hljs-keyword">const</span> <span class="hljs-title function_">loadData</span> = (<span class="hljs-params"></span>) => { d3.<span class="hljs-title function_">dsv</span>(<span class="hljs-string">','</span>, <span class="hljs-string">'/data/line.csv'</span>, <span class="hljs-function">(<span class="hljs-params">d</span>) =></span> { <span class="hljs-keyword">return</span> (d <span class="hljs-keyword">as</span> <span class="hljs-built_in">unknown</span>) <span class="hljs-keyword">as</span> <span class="hljs-title class_">Types</span>.<span class="hljs-property">Data</span>[] }).<span class="hljs-title function_">then</span>(<span class="hljs-function">(<span class="hljs-params">d</span>) =></span> { <span class="hljs-title function_">setData</span>((d <span class="hljs-keyword">as</span> <span class="hljs-built_in">unknown</span>) <span class="hljs-keyword">as</span> <span class="hljs-title class_">Types</span>.<span class="hljs-property">Data</span>[]) }) }

<span class="hljs-title function_">useEffect</span>(<span class="hljs-function">() =></span> { <span class="hljs-keyword">if</span> (data.<span class="hljs-property">length</span> <= <span class="hljs-number">1</span>) <span class="hljs-title function_">loadData</span>() })

<span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag"><></span> {data.length > 1 ? <span class="hljs-tag"><<span class="hljs-name">LineChartDateBisector</span> <span class="hljs-attr">dimensions</span>=<span class="hljs-string">{dimensions.current}</span> <span class="hljs-attr">data</span>=<span class="hljs-string">{data}</span> <span class="hljs-attr">propertiesNames</span>=<span class="hljs-string">{[</span>'<span class="hljs-attr">date</span>', '<span class="hljs-attr">y</span>']} /></span> : <span class="hljs-tag"><></span>Loading<span class="hljs-tag"></></span></span>} </> } <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-title class_">LineChartDateBisectorWidget</span></pre></div><p id="1d50">WindowDimensions.tsx hook;</p><div id="ebdb"><pre><span class="hljs-regexp">//</span> src<span class="hljs-regexp">/hooks/</span>WindowDimensions.tsx</pre></div><div id="4a47"><pre><span class="hljs-keyword">import</span> { useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span></pre></div><div id="7fe9"><pre><span class="hljs-function">function <span class="hljs-title">getWindowDimensions</span><span class="hljs-params">()</span> </span>{ <span class="hljs-type">const</span> { innerWidth: width, innerHeight: height } = window <span class="hljs-keyword">return</span> { width, height, } }</pre></div><div id="1b1f"><pre><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">useWindowDimensions</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">const</span> [windowDimensions, setWindowDimensions] = <span class="hljs-title function_">useState</span>(<span class="hljs-title function_">getWindowDimensions</span>())</pre></div><div id="8272"><pre> <span class="hljs-function"><span class="hljs-title">useEffect</span>(() => { <span class="hljs-variable">function</span> <span class="hljs-title">handleResize</span>() { <span class="hljs-title">setWindowDimensions</span>(<span class="hljs-title">getWindowDimensions</span>())</span> }</pre></div><div id="8e89"><pre> <span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'resize'</span>, handleResize) <span class="hljs-keyword">return</span> () => <span class="hljs-built_in">window</span>.removeEventListener(<span class="hljs-string">'resize'</span>, handleResize) }, [])</pre></div><div id="dea3"><pre> <span class="hljs-keyword">return</span> windowDimensions }</pre></div><h1 id="66af">LineChartDateBisector.tsx</h1><p id="8c1f">The LineChartDateBisector is the actual chart. Notice that I am creating: memoizedUpdateCallback with <code>useCallback</code>.</p><p id="7621">That way I can use them in multiple <code>useEffects</code> instead of copy-paste the code multiple times.</p><p id="8af5">I keep track of data change, and the previous width and height of the document so I can redraw the chart on a resize event.</p><p id="9b03">Inside <code>memoizedUpdateCallback</code>, I am drawing the chart and setting up the <i>Peripherals.</i></p><p id="c811">Lastly, notice that I am setting most of the elements as JSX instead of adding them with d3 via append. d3 will append the text and circle once hover, but the rest is pure JSX.</p><div id="7dea"><pre><span class="hljs-comment">// src/component/LineChart/LineChartDateBisector.tsx</span>

<span class="hljs-keyword">import</span> <span class="hljs-title class_">React</span>, { useEffect, useCallback } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span> <span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> d3 <span class="hljs-keyword">from</span> <span class="hljs-string">'d3'</span> <span class="hljs-keyword">import</span> { <span class="hljs-title class_">Types</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../widgets/LineChartDateBisectorWidget/types'</span> <span class="hljs-keyword">import</span> <span class="hljs-title class_">LineChartDateBisectorHelper</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'./LineChartDateBisectorHelper'</span>

<span class="hljs-keyword">const</span> <span class="hljs-title function_">LineChartDateBisector</span> = (<span class="hljs-params">props: ILineChartProps</span>) => { <span class="hljs-keyword">const</span> memoizedUpdateCallback = <span class="hljs-title function_">useCallback</span>(<span class="hljs-function">() =></span> { <span class="hljs-keyword">const</span> scales = <span class="hljs-title class_">LineChartDateBisectorHelper</span>.<span class="hljs-title function_">getScales</span>(props.<span class="hljs-property">data</span>, props.<span class="hljs-property">dimensions</span>.<span class="hljs-property">boundedWidth</span>, props.<span class="hljs-property">dimensions</span>.<span class="hljs-property">boundedHeight</span>, props.<span class="hljs-property">propertiesNames</span>) <span class="hljs-keyword">const</span> bounds = d3.<span class="hljs-title function_">select</span>(<span class="hljs-string">'#bounds'</span>)

<span class="hljs-keyword">const</span> helper = <span class="hljs-keyword">new</span> <span class="hljs-title class_">LineChartDateBisectorHelper</span>(props.<span class="hljs-property">propertiesNames</span>)

<span class="hljs-comment">// draw chart</span>
<span class="hljs-keyword">const</span> linesGenerator = d3
  .<span class="hljs-title function_">line</span>()
  <span class="hljs-comment">// @ts-ignore</span>
  .<span class="hljs-title function_">x</span>(<span class="hljs-function">(<span class="hljs-params">d</span>) =&gt;</span> scales.<span class="hljs-title function_">xScale</span>(helper.<span class="hljs-title function_">xAccessor</span>(d)))
  <span class="hljs-comment">// @ts-ignore</span>
  .<span class="hljs-title function_">y</span>(<span class="hljs-function">(<span class="hljs-params">d</span>) =&gt;</span> scales.<span class="hljs-title function_">yScale</span>(helper.<span class="hljs-title function_">yAccessor</span>(d)))

d3.<span class="hljs-title function_">select</span>(<span class="hljs-string">'#path'</span>)
  .<span class="hljs-title function_">attr</span>(<span class="hljs-string">'fill'</span>, <span class="hljs-string">'none'</span>)
  .<span class="hljs-title function_">attr</span>(<span class="hljs-string">'stroke'</span>, <span class="hljs-string">'tomato'</span>)
  <span class="hljs-comment">// @ts-ignore</span>
  .<span class="hljs-title function_">attr</span>(<span class="hljs-string">'d'</span>, <span class="hljs-title function_">linesGenerator</span>(props.<span class="hljs-property">data</span>))

<span class="hljs-comment">// Peripherals</span>

<span class="hljs-comment">// yAxis</span>
<span class="hljs-keyword">const</span> yAxisGenerator = d3.<span class="hljs-title function_">axisLeft</span>(scales.<span class="hljs-property">yScale</span>)
bounds
  .<span class="hljs-title function_">select</span>(<span class="hljs-string">'#y-axis'</span>)
  <span class="hljs-comment">// @ts-ignore</span>
  .<span class="hljs-title function_">call</span>(yAxisGenerator)

<span class="hljs-comment">// xAxis</span>
<span class="hljs-keyword">const</span> xAxisGenerator = d3.<span class="hljs-title function_">axisBottom</span>(scales.<span class="hljs-property">xScale</span>)
bounds
  .<span class="hljs-title function_">select</span>(<span class="hljs-string">'#x-axis'</span>)
  <span class="hljs-comment">// @ts-ignore</span>
  .<span class="hljs-title function_">call</span>(xAxisGenerator)
  .<span class="hljs-title function_">style</span>(<span class="hljs-string">'transform'</span>, <span class="hljs-string">`translateY(<span class="hljs-subst">${props.dimensions.boundedHeight}</span>px)`</span>)

<span class="hljs-comment">// @ts-ignore</span>
<span class="hljs-keyword">const</span> bisect = d3.<span class="hljs-title function_">bisector</span>(<span class="hljs-function">(<span class="hljs-params">d</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> currentDateSplit =  (d <span class="hljs-keyword">as</span> {<span class="hljs-attr">date</span>: string}).<span class="hljs-property">date</span>.<span class="hljs-title function_">split</span>(<span class="hljs-string">'/'</span>)
  <span class="hljs-keyword">const</span> currentDate = {
    <span class="hljs-attr">year</span>: <span class="hljs-built_in">parseInt</span>(currentDateSplit[<span class="hljs-number">2</span>], <span class="hljs-number">10</span>),
    <span class="hljs-attr">month</span>: <span class="hljs-built_in">parseInt</span>(currentDateSplit[<span class="hljs-number">0</span>], <span class="hljs-number">10</span>),
    <span class="hljs-attr">day</span>: <span class="hljs-built_in">parseInt</span>(currentDateSplit[<span class="hljs-number">1</span>], <span class="hljs-number">10</span>),
  }
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(currentDate.<span class="hljs-property">year</span>, currentDate.<span class="hljs-property">month</span>, currentDate.<span class="hljs-property">day</span>)
})

<span class="hljs-keyword">const</span> focus = bounds
  .<span class="hljs-title function_">append</span>(<span class="hljs-string">'g'</span>)
  .<span class="hljs-title function_">append</span>(<span class="hljs-string">'circle'</span>)
  .<span class="hljs-title function_">style</span>(<span class="hljs-string">'fill'</span>, <span class="hljs-string">'none'</span>)
  .<span class="hljs-title function_">attr</span>(<span class="hljs-string">'stroke'</span>, <span class="hljs-string">'white'</span>)
  .<span class="hljs-title function_">attr</span>(<span class="hljs-string">'r'</span>, <span class="hljs-number">8.5</span>)
  .<span class="hljs-title function_">style</span>(<span class="hljs-string">'opacity'</span>, <span class="hljs-number">0</span>)

<span class="hljs-keyword">const</span> focusText = bounds
  .<span class="hljs-title function_">append</span>(<span class="hljs-string">'g'</span>)
  .<span class="hljs-title function_">append</span>(<span class="hljs-string">'text'</span>)
  .<span class="hljs-title function_">style</span>(<span class="hljs-string">'opacity'</span>, <span class="hljs-number">0</span>)
  .<span class="hljs-title function_">style</span>(<span class="hljs-string">'fill'</span>, <span class="hljs-string">'white'</span>)
  .<span class="hljs-title function_">attr</span>(<span class="hljs-string">'text-anchor'</span>, <span class="hljs-string">'left'</span>)
  .<span class="hljs-title function_">attr</span>(<span class="hljs-string">'alignment-baseline'</span>, <span class="hljs-string">'middle'</span>)

bounds
  .<span class="hljs-title function_">append</span>(<span class="hljs-string">'rect'</span>)
  .<span class="hljs-title function_">style</span>(<span class="hl

Options

js-string">'fill'</span>, <span class="hljs-string">'none'</span>) .<span class="hljs-title function_">style</span>(<span class="hljs-string">'pointer-events'</span>, <span class="hljs-string">'all'</span>) .<span class="hljs-title function_">attr</span>(<span class="hljs-string">'width'</span>, props.<span class="hljs-property">dimensions</span>.<span class="hljs-property">width</span>) .<span class="hljs-title function_">attr</span>(<span class="hljs-string">'height'</span>, props.<span class="hljs-property">dimensions</span>.<span class="hljs-property">height</span>) .<span class="hljs-title function_">on</span>(<span class="hljs-string">'mouseover'</span>, mouseover) .<span class="hljs-title function_">on</span>(<span class="hljs-string">'mousemove'</span>, mousemove) .<span class="hljs-title function_">on</span>(<span class="hljs-string">'mouseout'</span>, mouseout);

<span class="hljs-keyword">function</span> <span class="hljs-title function_">mouseover</span>(<span class="hljs-params"></span>) {
  focus.<span class="hljs-title function_">style</span>(<span class="hljs-string">'opacity'</span>, <span class="hljs-number">1</span>)
  focusText.<span class="hljs-title function_">style</span>(<span class="hljs-string">'opacity'</span>, <span class="hljs-number">1</span>)
}

<span class="hljs-keyword">function</span> <span class="hljs-title function_">mousemove</span>(<span class="hljs-params">event: React.MouseEvent</span>) {
  <span class="hljs-keyword">const</span> [x, y] = d3.<span class="hljs-title function_">pointer</span>(event)
  <span class="hljs-keyword">const</span> x0 = scales.<span class="hljs-property">xScale</span>.<span class="hljs-title function_">invert</span>(x);
  <span class="hljs-keyword">const</span> currentDateSplit = x0.<span class="hljs-title function_">toISOString</span>().<span class="hljs-title function_">split</span>(<span class="hljs-string">'T'</span>)[<span class="hljs-number">0</span>].<span class="hljs-title function_">split</span>(<span class="hljs-string">'-'</span>)
  <span class="hljs-keyword">const</span> currentDate = {
    <span class="hljs-attr">year</span>: <span class="hljs-built_in">parseInt</span>(currentDateSplit[<span class="hljs-number">0</span>], <span class="hljs-number">10</span>),
    <span class="hljs-attr">month</span>: <span class="hljs-built_in">parseInt</span>(currentDateSplit[<span class="hljs-number">1</span>], <span class="hljs-number">10</span>),
    <span class="hljs-attr">day</span>: <span class="hljs-built_in">parseInt</span>(currentDateSplit[<span class="hljs-number">2</span>], <span class="hljs-number">10</span>),
  }

  <span class="hljs-keyword">let</span> selectedData = props.<span class="hljs-property">data</span>[props.<span class="hljs-property">data</span>.<span class="hljs-property">length</span> - <span class="hljs-number">1</span>]
  <span class="hljs-keyword">const</span> i = bisect.<span class="hljs-title function_">right</span>( props.<span class="hljs-property">data</span>, <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(currentDate.<span class="hljs-property">year</span>, currentDate.<span class="hljs-property">month</span>, currentDate.<span class="hljs-property">day</span>) )
  <span class="hljs-keyword">if</span> (i &lt;= props.<span class="hljs-property">data</span>.<span class="hljs-property">length</span> - <span class="hljs-number">1</span>)
    selectedData = props.<span class="hljs-property">data</span>[i]

  focus
    .<span class="hljs-title function_">attr</span>(<span class="hljs-string">'cx'</span>, scales.<span class="hljs-title function_">xScale</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(selectedData.<span class="hljs-property">date</span>)))
    .<span class="hljs-title function_">attr</span>(<span class="hljs-string">'cy'</span>, scales.<span class="hljs-title function_">yScale</span>(selectedData.<span class="hljs-property">y</span>))
  focusText
    .<span class="hljs-title function_">html</span>(<span class="hljs-string">`x:<span class="hljs-subst">${  selectedData.date  }</span>  -  y:<span class="hljs-subst">${  selectedData.y}</span>`</span>)
    .<span class="hljs-title function_">attr</span>(<span class="hljs-string">'x'</span>, scales.<span class="hljs-title function_">xScale</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(selectedData.<span class="hljs-property">date</span>))+<span class="hljs-number">15</span>)
    .<span class="hljs-title function_">attr</span>(<span class="hljs-string">'y'</span>, scales.<span class="hljs-title function_">yScale</span>(selectedData.<span class="hljs-property">y</span>))
}
<span class="hljs-keyword">function</span> <span class="hljs-title function_">mouseout</span>(<span class="hljs-params"></span>) {
  focus.<span class="hljs-title function_">style</span>(<span class="hljs-string">'opacity'</span>, <span class="hljs-number">0</span>)
  focusText.<span class="hljs-title function_">style</span>(<span class="hljs-string">'opacity'</span>, <span class="hljs-number">0</span>)
}

}, [props.<span class="hljs-property">data</span>, props.<span class="hljs-property">dimensions</span>, props.<span class="hljs-property">propertiesNames</span>])

<span class="hljs-title function_">useEffect</span>(<span class="hljs-function">() =></span> { <span class="hljs-title function_">memoizedUpdateCallback</span>() }, [memoizedUpdateCallback, props.<span class="hljs-property">data</span>])

<span class="hljs-keyword">return</span> ( <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"div"</span>></span> <span class="hljs-tag"><<span class="hljs-name">svg</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"wrapper"</span> <span class="hljs-attr">width</span>=<span class="hljs-string">{props.dimensions.width}</span> <span class="hljs-attr">height</span>=<span class="hljs-string">{props.dimensions.height}</span>></span> <span class="hljs-tag"><<span class="hljs-name">g</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"bounds"</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">transform:</span> <span class="hljs-attr">translate</span>(${<span class="hljs-attr">props.dimensions.margin.left</span>}<span class="hljs-attr">px</span>, ${<span class="hljs-attr">props.dimensions.margin.top</span>}<span class="hljs-attr">px</span>) }}></span> <span class="hljs-tag"><<span class="hljs-name">path</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"path"</span> /></span> <span class="hljs-tag"><<span class="hljs-name">g</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"x-axis"</span> /></span> <span class="hljs-tag"><<span class="hljs-name">g</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"y-axis"</span> /></span> <span class="hljs-tag"></<span class="hljs-name">g</span>></span> <span class="hljs-tag"></<span class="hljs-name">svg</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span></span> ) }

interface <span class="hljs-title class_">ILineChartProps</span> { <span class="hljs-attr">dimensions</span>: <span class="hljs-title class_">Types</span>.<span class="hljs-property">Dimensions</span> <span class="hljs-attr">data</span>: <span class="hljs-title class_">Types</span>.<span class="hljs-property">Data</span>[] <span class="hljs-attr">propertiesNames</span>: string[] }

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-title class_">LineChartDateBisector</span></pre></div><p id="ea6a">Few code lines I want to highlight, which is the core of dealing with date scale;</p><p id="bb96">The <code>d3.<i>bisector</i></code> allows finding the closest X index of the mouse. I am using the date split and creating a new date to match the format so the d3 bisector can compare the values. Having a different format of date between what you have in your data set and what d3.<i>bisector </i>will calculate will have your code fail silently.</p><div id="ce82"><pre> <span class="hljs-keyword">const</span> bisect = d3.<span class="hljs-title function_">bisector</span>(<span class="hljs-function">(<span class="hljs-params">d</span>) =></span> { <span class="hljs-keyword">const</span> currentDateSplit = (d <span class="hljs-keyword">as</span> {<span class="hljs-attr">date</span>: <span class="hljs-built_in">string</span>}).<span class="hljs-property">date</span>.<span class="hljs-title function_">split</span>(<span class="hljs-string">'/'</span>) <span class="hljs-keyword">const</span> currentDate = { <span class="hljs-attr">year</span>: <span class="hljs-built_in">parseInt</span>(currentDateSplit[<span class="hljs-number">2</span>], <span class="hljs-number">10</span>), <span class="hljs-attr">month</span>: <span class="hljs-built_in">parseInt</span>(currentDateSplit[<span class="hljs-number">0</span>], <span class="hljs-number">10</span>), <span class="hljs-attr">day</span>: <span class="hljs-built_in">parseInt</span>(currentDateSplit[<span class="hljs-number">1</span>], <span class="hljs-number">10</span>), } <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(currentDate.<span class="hljs-property">year</span>, currentDate.<span class="hljs-property">month</span>, currentDate.<span class="hljs-property">day</span>) }) </pre></div><p id="f6d5">The other difference between working with a linear scale and date is that on mouse move, to figure out the annotations we can recover the coordinate using invert and then convert the date format to <code>toISOString</code> and split the date from time and then I can use the <code>bisect.right</code> to calculate the closest right data to where the mouse is at.</p><div id="9682"><pre> <span class="hljs-keyword">function</span> <span class="hljs-title function_">mousemove</span>(<span class="hljs-params">event: React.MouseEvent</span>) { <span class="hljs-keyword">const</span> [x, y] = d3.<span class="hljs-title function_">pointer</span>(event) <span class="hljs-comment">// recover coordinate we need</span> <span class="hljs-keyword">const</span> x0 = scales.<span class="hljs-property">xScale</span>.<span class="hljs-title function_">invert</span>(x); <span class="hljs-keyword">const</span> currentDateSplit = x0.<span class="hljs-title function_">toISOString</span>().<span class="hljs-title function_">split</span>(<span class="hljs-string">'T'</span>)[<span class="hljs-number">0</span>].<span class="hljs-title function_">split</span>(<span class="hljs-string">'-'</span>) <span class="hljs-keyword">const</span> currentDate = { <span class="hljs-attr">year</span>: <span class="hljs-built_in">parseInt</span>(currentDateSplit[<span class="hljs-number">0</span>], <span class="hljs-number">10</span>), <span class="hljs-attr">month</span>: <span class="hljs-built_in">parseInt</span>(currentDateSplit[<span class="hljs-number">1</span>], <span class="hljs-number">10</span>), <span class="hljs-attr">day</span>: <span class="hljs-built_in">parseInt</span>(currentDateSplit[<span class="hljs-number">2</span>], <span class="hljs-number">10</span>), }</pre></div><p id="0e77"><b>App.tsx</b></p><p id="2d5f">On the entry point <code>App.tsx</code>, just include the widget;</p><div id="c617"><pre><span class="hljs-comment">// src/App.tsx</span>

<span class="hljs-keyword">import</span> <span class="hljs-title class_">React</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span> <span class="hljs-keyword">import</span> <span class="hljs-string">'./App.scss'</span> <span class="hljs-keyword">import</span> <span class="hljs-title class_">LineChartDateBisectorWidget</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'./widgets/LineChartDateBisectorWidget/LineChartDateBisectorWidget'</span>

<span class="hljs-keyword">function</span> <span class="hljs-title function_">App</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">return</span> ( <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"App"</span>></span> <span class="hljs-tag"><<span class="hljs-name">header</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"App-header"</span>></span> <span class="hljs-tag"><<span class="hljs-name">LineChartDateBisectorWidget</span> /></span> <span class="hljs-tag"></<span class="hljs-name">header</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span></span> ) }

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-title class_">App</span></pre></div><figure id="ad93"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*6IsxxcarFhmERUAz0co6XA.png"><figcaption></figcaption></figure><h1 id="5ffd">Summary</h1><p id="680d">React and d3 are the two top libraries in their respective areas. Giving React control over the DOM with the help of memorizing methods and writing the elements as JSX, while letting D3 handle the heavy lifting for calculations and interactivity can help you get the best of both worlds and create great data visualization charts to help tell your story better.</p><p id="4514"><a href="https://github.com/EliEladElrom/react-tutorials">Source code</a>.</p><h1 id="dd46">Where to go from here</h1><p id="20d7">Need help with integrating React d3 project or learning? I am here to help.</p><ol><li>Take the <a href="https://www.udemy.com/course/integrating-d3js-with-react/?referralCode=4C1ADE35AB8633B90205">React + d3 interactive course</a>.</li><li>Buy the <a href="https://www.apress.com/gp/book/9781484270516">d3 + React book</a>.</li><li>Read related articles: <a href="https://readmedium.com/setting-up-professional-react-project-with-must-have-reactjs-libraries-2020-9358edf9acb3">https://readmedium.com/setting-up-professional-react-project-with-must-have-reactjs-libraries-2020-9358edf9acb3</a></li></ol><p id="40cc">If you like this article, don’t be shy to clap 🙏✌</p></article></body>

Line Chart using React.js d3.js & TypeScript with the help of d3.bisector interaction — Part II

To create interactivity of the mouse following the plotted data there is a need to do a calculation of the closest point to the mouse. Luckily, d3 has a method d3.bisector that can help us with these calculations. In this two-part tutorial, I will show you how to work with d3.bisector.

In the first part of this two-part series, I was working with two linear scale values as the metric and in this part, part two I will be working with value and time metrics using d3.bisector.

I created each tutorial so it can be used as standalone and use the part I, if you need two linear scales as metrics, or part II for time, value metrics.

You can see the final result of this tutorial below;

Setting up your project

I will be using CRA (SPA) using the MHL template to get TS, SCSS, formatting, templates, linting, etc.

$ yarn create react-app bisector --template must-have-libraries

Add d3, d3-cloud and types;

$ yarn add d3 @types/d3

data.csv

A good place to start when working with charts is from the data. The data is made out of three metrics: x, y values, and the date. We will be using here the date as time scale and the y-value.

/public/data/line.csv

x,y,date
1,0.03,1/1/2021
2,0.04,1/2/2021
3,0.06,1/3/2021
4,0.09,1/4/2021
5,0.13,1/5/2021
6,0.18,1/6/2021
7,0.25,1/7/2021
8,0.33,1/8/2021
9,0.45,1/9/2021
...

To creating the components we will be using my templates;

$ npx generate-react-cli component LineChartDateBisector --type=d3WidgetComponent
$ npx generate-react-cli component LineChartDateBisectorWidget --type=d3Widget

These templates will create for you the following files we need;

  • widgets/LineChartDateBisectorWidget/types.ts
  • components/LineChartDateBisector/LineChartDateHelper.tsx
  • widgets/LineChartDateBisectorWidget/LineChartDateBisectorWidget.tsx
  • components/LineChartDateBisector/LineChartDateBisector.tsx

types.ts

Next, we want to set some types for TypeScript. The Data object matches the data.csv metric format by setting up the y-value and date as the metrics. The Dimensions object will help us pass some props values from the widget to the actual chart components;

// src/widgets/LineChartWidget/types.ts
export namespace Types {
  export type Data = {
    date: string
    y: number
  }
  export type Dimensions = {
    width: number
    height: number
    margin: {
      left: number
      right: number
      top: number
      bottom: number
    }
    boundedWidth: number
    boundedHeight: number
  }
}

LineChartDateBisectorHelper.tsx

In the LineChartDateBisectorHelper, I am setting some of the classes with private members so the class can be set so we can change the data metrics that the chart will be using. Other members can be set as static so the helper doesn’t need to be initialized.

The helper includes the layout, setting up dimensions and scales, take a look;

// src/component/LineChartDateHelper/LineChartDateHelper.tsx

import * as d3 from 'd3'
import { Types } from '../../widgets/LineChartDateBisectorWidget/types'

export default class LineChartDateBisectorHelper {
  private readonly metric: string[]

  constructor(metric: string[]) {
    this.metric = metric
  }

  static dateParser = d3.timeParse('%m/%d/%Y')

  // @ts-ignore
  public xAccessor = (d: Types.Data) => LineChartDateBisectorHelper.dateParser(d[this.metric[0]] as string)

  // @ts-ignore
  public yAccessor = (d: Types.Data) => d[this.metric[1]]

  static getDimensions = (width: number, height: number, left: number, right: number, top: number, bottom: number) => {
    const dimensions = {
      width,
      height,
      margin: {
        left,
        right,
        top,
        bottom,
      },
      boundedWidth: 0,
      boundedHeight: 0,
    }
    dimensions.boundedWidth = dimensions.width - dimensions.margin.left - dimensions.margin.right
    dimensions.boundedHeight = dimensions.height - dimensions.margin.top - dimensions.margin.bottom

    return dimensions
  }

  static getScales = (data: Types.Data[], width: number, height: number, metric: string[]) => {
    const helper = new LineChartDateBisectorHelper(metric)
    return {
      xScale: d3
        .scaleTime()
        .domain(d3.extent(data, helper.xAccessor) as [Date, Date])
        .range([0, width])
        .nice(),
      yScale: d3
        .scaleLinear()
        .domain([
          0,
          d3.max(data, (d) => {
            return Math.max(...data.map(helper.yAccessor), 0)
          }),
        ] as [number, number])
        .range([height, 0]),
    }
  }
}

For the yScale, I am using a scaleLinear and Math.max to calculate the value range. For xScale I will be using the scaleTime as its date values.

LineChartDateBisectorWidget.tsx

The LineChartDateBisectorWidget, is the parent component to hold the chart as well as any other elements that I need to interact with, for example, it can include other charts, lists, UI elements, and title elements.

The widget component loads the data and allows sharing the data between the components, so there is no need for redundancy. This can be tight in with the data management library like Redux or Recoil.

I am also setting up a loader to make sure the data is loaded. This can be replaced with a spinner or anything you need.

The dimensions are set as a reference and WindowDimensions a hook is used to listen to resize events from the DOM. It’s set as a ref so it can be used inside the useEffect, without the function being called all the time.

// src/widgets/LineChartWidget/LineChartDateBisectorWidget.tsx

import React, { useEffect, useRef, useState } from 'react'
import * as d3 from 'd3'
import { Types } from './types'
import useWindowDimensions from '../../hooks/WindowDimensions'

import LineChartDateBisectorHelper from '../../components/LineChartDateBisector/LineChartDateBisectorHelper'
import LineChartDateBisector from '../../components/LineChartDateBisector/LineChartDateBisector'

const LineChartDateBisectorWidget = () => {
  const [data, setData] = useState<Types.Data[]>([{ date: '', y: 0 }])

  const { width, height } = useWindowDimensions()

  const dimensions = useRef() as { current: Types.Dimensions }
  dimensions.current = LineChartDateBisectorHelper.getDimensions(width * 0.9, height * 0.9, 30, 50, 10, 20)

  // resize
  useEffect(() => {
    ((dimensions as unknown) as { current: Types.Dimensions }).current = LineChartDateBisectorHelper.getDimensions(width * 0.9, height * 0.9, 30, 50, 10, 20)
    // console.log(dimensions.current)
  }, [width, height])

  const loadData = () => {
    d3.dsv(',', '/data/line.csv', (d) => {
      return (d as unknown) as Types.Data[]
    }).then((d) => {
      setData((d as unknown) as Types.Data[])
    })
  }

  useEffect(() => {
    if (data.length <= 1) loadData()
  })

  return <>
    {data.length > 1
      ?
      <LineChartDateBisector
        dimensions={dimensions.current}
        data={data}
        propertiesNames={['date', 'y']}
      />
      :
      <>Loading</>}
    </>
}
export default LineChartDateBisectorWidget

WindowDimensions.tsx hook;

// src/hooks/WindowDimensions.tsx
import { useState, useEffect } from 'react'
function getWindowDimensions() {
  const { innerWidth: width, innerHeight: height } = window
  return {
    width,
    height,
  }
}
export default function useWindowDimensions() {
  const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions())
  useEffect(() => {
    function handleResize() {
      setWindowDimensions(getWindowDimensions())
    }
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])
  return windowDimensions
}

LineChartDateBisector.tsx

The LineChartDateBisector is the actual chart. Notice that I am creating: memoizedUpdateCallback with useCallback.

That way I can use them in multiple useEffects instead of copy-paste the code multiple times.

I keep track of data change, and the previous width and height of the document so I can redraw the chart on a resize event.

Inside memoizedUpdateCallback, I am drawing the chart and setting up the Peripherals.

Lastly, notice that I am setting most of the elements as JSX instead of adding them with d3 via append. d3 will append the text and circle once hover, but the rest is pure JSX.

// src/component/LineChart/LineChartDateBisector.tsx

import React, { useEffect, useCallback } from 'react'
import * as d3 from 'd3'
import { Types } from '../../widgets/LineChartDateBisectorWidget/types'
import LineChartDateBisectorHelper from './LineChartDateBisectorHelper'

const LineChartDateBisector = (props: ILineChartProps) => {
  const memoizedUpdateCallback = useCallback(() => {
    const scales = LineChartDateBisectorHelper.getScales(props.data, props.dimensions.boundedWidth, props.dimensions.boundedHeight, props.propertiesNames)
    const bounds = d3.select('#bounds')

    const helper = new LineChartDateBisectorHelper(props.propertiesNames)

    // draw chart
    const linesGenerator = d3
      .line()
      // @ts-ignore
      .x((d) => scales.xScale(helper.xAccessor(d)))
      // @ts-ignore
      .y((d) => scales.yScale(helper.yAccessor(d)))

    d3.select('#path')
      .attr('fill', 'none')
      .attr('stroke', 'tomato')
      // @ts-ignore
      .attr('d', linesGenerator(props.data))

    // Peripherals

    // yAxis
    const yAxisGenerator = d3.axisLeft(scales.yScale)
    bounds
      .select('#y-axis')
      // @ts-ignore
      .call(yAxisGenerator)

    // xAxis
    const xAxisGenerator = d3.axisBottom(scales.xScale)
    bounds
      .select('#x-axis')
      // @ts-ignore
      .call(xAxisGenerator)
      .style('transform', `translateY(${props.dimensions.boundedHeight}px)`)

    // @ts-ignore
    const bisect = d3.bisector((d) => {
      const currentDateSplit =  (d as {date: string}).date.split('/')
      const currentDate = {
        year: parseInt(currentDateSplit[2], 10),
        month: parseInt(currentDateSplit[0], 10),
        day: parseInt(currentDateSplit[1], 10),
      }
      return new Date(currentDate.year, currentDate.month, currentDate.day)
    })

    const focus = bounds
      .append('g')
      .append('circle')
      .style('fill', 'none')
      .attr('stroke', 'white')
      .attr('r', 8.5)
      .style('opacity', 0)

    const focusText = bounds
      .append('g')
      .append('text')
      .style('opacity', 0)
      .style('fill', 'white')
      .attr('text-anchor', 'left')
      .attr('alignment-baseline', 'middle')

    bounds
      .append('rect')
      .style('fill', 'none')
      .style('pointer-events', 'all')
      .attr('width', props.dimensions.width)
      .attr('height', props.dimensions.height)
      .on('mouseover', mouseover)
      .on('mousemove', mousemove)
      .on('mouseout', mouseout);

    function mouseover() {
      focus.style('opacity', 1)
      focusText.style('opacity', 1)
    }

    function mousemove(event: React.MouseEvent) {
      const [x, y] = d3.pointer(event)
      const x0 = scales.xScale.invert(x);
      const currentDateSplit = x0.toISOString().split('T')[0].split('-')
      const currentDate = {
        year: parseInt(currentDateSplit[0], 10),
        month: parseInt(currentDateSplit[1], 10),
        day: parseInt(currentDateSplit[2], 10),
      }

      let selectedData = props.data[props.data.length - 1]
      const i = bisect.right( props.data, new Date(currentDate.year, currentDate.month, currentDate.day) )
      if (i <= props.data.length - 1)
        selectedData = props.data[i]

      focus
        .attr('cx', scales.xScale(new Date(selectedData.date)))
        .attr('cy', scales.yScale(selectedData.y))
      focusText
        .html(`x:${  selectedData.date  }  -  y:${  selectedData.y}`)
        .attr('x', scales.xScale(new Date(selectedData.date))+15)
        .attr('y', scales.yScale(selectedData.y))
    }
    function mouseout() {
      focus.style('opacity', 0)
      focusText.style('opacity', 0)
    }

  }, [props.data, props.dimensions, props.propertiesNames])

  useEffect(() => {
    memoizedUpdateCallback()
  }, [memoizedUpdateCallback, props.data])

  return (
    <div id="div">
      <svg id="wrapper" width={props.dimensions.width} height={props.dimensions.height}>
        <g id="bounds" style={{ transform: `translate(${props.dimensions.margin.left}px, ${props.dimensions.margin.top}px)` }}>
          <path id="path" />
          <g id="x-axis" />
          <g id="y-axis" />
        </g>
      </svg>
    </div>
  )
}

interface ILineChartProps {
  dimensions: Types.Dimensions
  data: Types.Data[]
  propertiesNames: string[]
}

export default LineChartDateBisector

Few code lines I want to highlight, which is the core of dealing with date scale;

The d3.bisector allows finding the closest X index of the mouse. I am using the date split and creating a new date to match the format so the d3 bisector can compare the values. Having a different format of date between what you have in your data set and what d3.bisector will calculate will have your code fail silently.

    const bisect = d3.bisector((d) => {
      const currentDateSplit =  (d as {date: string}).date.split('/')
      const currentDate = {
        year: parseInt(currentDateSplit[2], 10),
        month: parseInt(currentDateSplit[0], 10),
        day: parseInt(currentDateSplit[1], 10),
      }
      return new Date(currentDate.year, currentDate.month, currentDate.day)
    })

The other difference between working with a linear scale and date is that on mouse move, to figure out the annotations we can recover the coordinate using invert and then convert the date format to toISOString and split the date from time and then I can use the bisect.right to calculate the closest right data to where the mouse is at.

    function mousemove(event: React.MouseEvent) {
      const [x, y] = d3.pointer(event)
      // recover coordinate we need
      const x0 = scales.xScale.invert(x);
      const currentDateSplit = x0.toISOString().split('T')[0].split('-')
      const currentDate = {
        year: parseInt(currentDateSplit[0], 10),
        month: parseInt(currentDateSplit[1], 10),
        day: parseInt(currentDateSplit[2], 10),
      }

App.tsx

On the entry point App.tsx, just include the widget;

// src/App.tsx

import React from 'react'
import './App.scss'
import LineChartDateBisectorWidget from './widgets/LineChartDateBisectorWidget/LineChartDateBisectorWidget'

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <LineChartDateBisectorWidget />
      </header>
    </div>
  )
}

export default App

Summary

React and d3 are the two top libraries in their respective areas. Giving React control over the DOM with the help of memorizing methods and writing the elements as JSX, while letting D3 handle the heavy lifting for calculations and interactivity can help you get the best of both worlds and create great data visualization charts to help tell your story better.

Source code.

Where to go from here

Need help with integrating React d3 project or learning? I am here to help.

  1. Take the React + d3 interactive course.
  2. Buy the d3 + React book.
  3. Read related articles: https://readmedium.com/setting-up-professional-react-project-with-must-have-reactjs-libraries-2020-9358edf9acb3

If you like this article, don’t be shy to clap 🙏✌

Data Visualization
Reactjs
D3js
Typescript
Charts
Recommended from ReadMedium