From 0247dbb41a35fa7081e2532aae7d64b713fe6b83 Mon Sep 17 00:00:00 2001 From: zlatan Date: Tue, 15 Jan 2019 11:59:10 +0700 Subject: [PATCH 01/10] add HistogramSlider component --- .../HistogramSlider/Histogram/Histogram.tsx | 146 ++++++++ .../HistogramSlider/Histogram/index.ts | 1 + .../HistogramSlider/HistogramSlider.tsx | 152 ++++++++ .../RangeSlider/RangeSlider.tsx | 349 ++++++++++++++++++ .../HistogramSlider/RangeSlider/index.ts | 1 + src/components/HistogramSlider/index.ts | 1 + 6 files changed, 650 insertions(+) create mode 100644 src/components/HistogramSlider/Histogram/Histogram.tsx create mode 100644 src/components/HistogramSlider/Histogram/index.ts create mode 100644 src/components/HistogramSlider/HistogramSlider.tsx create mode 100644 src/components/HistogramSlider/RangeSlider/RangeSlider.tsx create mode 100644 src/components/HistogramSlider/RangeSlider/index.ts create mode 100644 src/components/HistogramSlider/index.ts diff --git a/src/components/HistogramSlider/Histogram/Histogram.tsx b/src/components/HistogramSlider/Histogram/Histogram.tsx new file mode 100644 index 000000000..cdcf820d3 --- /dev/null +++ b/src/components/HistogramSlider/Histogram/Histogram.tsx @@ -0,0 +1,146 @@ +import React, { Component } from "react"; + +interface HistogramProps { + data: number[]; + maxHeightPx?: number; + value: [number, number]; + min: number; + max: number; + colors: { + in: string; + out: string; + }; +} + +interface HistogramState { + data: number[]; +} + +let maskCount = 0; + +export class Histogram extends Component { + static defaultProps = { + maxHeightPx: 20 + }; + + constructor(props: HistogramProps) { + super(props); + + const { data, maxHeightPx } = this.props; + const max = Math.max(...data); + const heightPxPerUnit = maxHeightPx! / max; + const heightData = data.map(v => Math.round(heightPxPerUnit * v)); + + this.state = { + data: heightData + }; + } + + maskHighlightID: string = `sf-histogram-mask-${maskCount++}`; + maskID: string = `sf-histogram-mask-${maskCount++}`; + numOfColumn: number = this.props.data.length; + + componentWillReceiveProps({ data }: HistogramProps) { + if (data !== this.props.data) { + const max = Math.max(...data); + const heightPxPerUnit = this.props.maxHeightPx! / max; + const heightData = data.map(v => Math.round(heightPxPerUnit * v)); + + this.numOfColumn = data.length; + + this.setState({ + data: heightData + }); + } + } + + render() { + const { min, max, value, colors, maxHeightPx } = this.props; + const [vMin, vMax] = value; + const range = max - min; + const start = ((vMin - min) * this.numOfColumn) / range; + const end = start + ((vMax - vMin) * this.numOfColumn) / range; + + return ( + + + + + > + + + + + + + > + + + + {this.state.data.map((height, index) => ( + + + + + ))} + + + ); + } +} diff --git a/src/components/HistogramSlider/Histogram/index.ts b/src/components/HistogramSlider/Histogram/index.ts new file mode 100644 index 000000000..a0a3c6382 --- /dev/null +++ b/src/components/HistogramSlider/Histogram/index.ts @@ -0,0 +1 @@ +export { Histogram } from './Histogram'; diff --git a/src/components/HistogramSlider/HistogramSlider.tsx b/src/components/HistogramSlider/HistogramSlider.tsx new file mode 100644 index 000000000..fa6dd303e --- /dev/null +++ b/src/components/HistogramSlider/HistogramSlider.tsx @@ -0,0 +1,152 @@ +import React, { Component } from "react"; +import { Histogram } from "./Histogram"; +import { RangeSlider } from "./RangeSlider"; +import { css } from "emotion"; + +interface HistogramSliderProps { + data: number[]; + value: [number, number]; + min: number; + max: number; + step: number; + distance: number; + debounceDelay?: number; + colors: { + in: string; + out: string; + }; + onApply?: (value: [number, number]) => void; + onChange?: (value: [number, number]) => void; +} + +interface HistogramSliderState { + value: [number, number]; +} + +export class HistogramSlider extends Component< + HistogramSliderProps, + HistogramSliderState +> { + state: HistogramSliderState = { + value: [this.props.value[0], this.props.value[1]] + }; + + timeout: number | undefined; + + componentWillReceiveProps(nextProps: HistogramSliderProps) { + const { value } = nextProps; + if (value !== this.props.value) { + this.setState({ value }); + } + } + + reset = (e: React.MouseEvent) => { + e.preventDefault(); + this.setState({ value: [this.props.min, this.props.max] }, () => { + if (typeof this.props.onApply === "function") { + this.props.onApply(this.state.value); + } else if (typeof this.props.onChange === "function") { + this.props.onChange(this.state.value); + } + }); + }; + + isDisabled = () => { + return ( + this.state.value[0] === this.props.min && + this.state.value[1] === this.props.max + ); + }; + + handleSliderChange = (value: [number, number]) => { + this.setState({ value }, () => { + if (typeof this.props.onChange === "function") { + if (this.timeout) { + window.clearTimeout(this.timeout); + } + this.timeout = window.setTimeout(() => { + //@ts-ignore: has been checked outsite + this.props.onChange(this.state.value); + }, this.props.debounceDelay || 500); + } + }); + }; + + handleApply = (e: React.MouseEvent) => { + e.preventDefault(); + if (typeof this.props.onApply === "function") { + this.props.onApply(this.state.value); + } + }; + + render() { + if (this.props.min >= this.props.max) { + console.error( + `The prop "min" should not be greater than the props "max".` + ); + } + + if (this.props.value[0] >= this.props.value[1]) { + console.error( + `The [0] of the prop "value" should not be greater than the [1].` + ); + } + + const isDisabled = this.isDisabled(); + const { data, ...rangeSliderProps } = this.props; + + return ( +
+ +
+ +
+
+
+ ${this.state.value[0]} AUD - ${this.state.value[1]} AUD +
+ + {typeof this.props.onApply === "function" && ( +
+ {!isDisabled && ( + + )} + +
+ )} +
+
+ ); + } +} diff --git a/src/components/HistogramSlider/RangeSlider/RangeSlider.tsx b/src/components/HistogramSlider/RangeSlider/RangeSlider.tsx new file mode 100644 index 000000000..0a7f0f25a --- /dev/null +++ b/src/components/HistogramSlider/RangeSlider/RangeSlider.tsx @@ -0,0 +1,349 @@ +import React, { Component } from "react"; +import { css } from "emotion"; + +interface RangeSliderProps { + min: number; + max: number; + step: number; + value: [number, number]; + distance: number; + onChange?: (value: [number, number]) => void; + colors: { + in: string; + out: string; + }; +} + +interface RangeSliderState { + value: [number, number]; +} + +export class RangeSlider extends Component { + state: RangeSliderState = { + value: [this.props.value[0], this.props.value[1]] + }; + + ref = React.createRef(); + range = this.props.max - this.props.min; + + getKeyboardStep = () => { + let step = Math.floor(this.props.max / 100); + return step < this.props.step ? this.props.step : step; + }; + + triggerMouseMin = () => { + document.addEventListener("mousemove", this.mouseMoveMin); + document.addEventListener("mouseup", this.clearDocumentEvents); + }; + + triggerMouseMax = () => { + document.addEventListener("mousemove", this.mouseMoveMax); + document.addEventListener("mouseup", this.clearDocumentEvents); + }; + + triggerTouchMin = () => { + document.addEventListener("touchmove", this.touchMoveMin); + document.addEventListener("touchend", this.clearDocumentEvents); + document.addEventListener("touchcancel", this.clearDocumentEvents); + }; + + triggerTouchMax = () => { + document.addEventListener("touchmove", this.touchMoveMax); + document.addEventListener("touchend", this.clearDocumentEvents); + document.addEventListener("touchcancel", this.clearDocumentEvents); + }; + + getCordsProperties = () => { + // @ts-ignore + const { x, width } = this.ref.current.getBoundingClientRect(); + return { minX: x, maxX: x + width, width }; + }; + + touchMoveMax = (e: TouchEvent) => { + const { clientX } = e.changedTouches[0]; + this.dragMax(clientX); + }; + + mouseMoveMax = (e: MouseEvent) => { + const { clientX } = e; + this.dragMax(clientX); + }; + + touchMoveMin = (e: TouchEvent) => { + const { clientX } = e.changedTouches[0]; + this.dragMin(clientX); + }; + + mouseMoveMin = (e: MouseEvent) => { + const { clientX } = e; + this.dragMin(clientX); + }; + + dragMin = (clientX: number) => { + const { minX, width } = this.getCordsProperties(); + const percent = clientX < minX ? 0 : (clientX - minX) / width; + let min = percent * this.range; + + this.setState(prevState => { + const [prevStateMin, prevStateMax] = prevState.value; + if (clientX <= minX) { + return { value: [this.props.min, prevStateMax] }; + } + + const delta = (min - prevStateMin + this.props.min) / this.props.step; + let addition = 0; + if (Math.abs(delta) >= 1) { + addition = Math.floor(delta / this.props.step) * this.props.step; + } + min = prevStateMin + addition; + if (min + this.props.distance > prevStateMax) { + min = prevStateMax - this.props.distance; + } + return { value: [min, prevStateMax] }; + }, this.callback); + }; + + dragMax = (clientX: number) => { + const { maxX, minX, width } = this.getCordsProperties(); + const percent = clientX > maxX ? 1 : (clientX - minX) / width; + let max = percent * this.range; + + this.setState((prevState: RangeSliderState) => { + const [prevStateMin, prevStateMax] = prevState.value; + + if (clientX >= maxX) { + return { value: [prevStateMin, this.props.max] }; + } + const delta = (max - prevStateMax + this.props.min) / this.props.step; + let addition = 0; + if (Math.abs(delta) >= 1) { + addition = Math.ceil(delta / this.props.step) * this.props.step; + } + max = prevStateMax + addition; + if (max - this.props.distance < prevStateMin) { + max = prevStateMin + this.props.distance; + } + return { value: [prevStateMin, max] }; + }, this.callback); + }; + + handleMinKeydown = (e: React.KeyboardEvent) => { + const { key } = e; + if (key === "Enter" || key === " ") { + e.preventDefault(); + return; + } + const { distance, min } = this.props; + + if (key === "ArrowRight") { + this.setState((prevState: RangeSliderState) => { + const [prevStateMin, prevStateMax] = prevState.value; + const nextStateMin = + prevStateMin + distance >= prevStateMax + ? prevStateMax - distance + : prevStateMin + this.getKeyboardStep(); + return { value: [nextStateMin, prevStateMax] }; + }, this.callback); + } else if (key === "ArrowLeft") { + this.setState((prevState: RangeSliderState) => { + const [prevStateMin, prevStateMax] = prevState.value; + const nextStateMin = + prevStateMin <= min ? min : prevStateMin - this.getKeyboardStep(); + return { value: [nextStateMin, prevStateMax] }; + }, this.callback); + } + }; + + handleMaxKeydown = (e: React.KeyboardEvent) => { + const { key } = e; + if (key === "Enter" || key === " ") { + e.preventDefault(); + return; + } + const { distance, max } = this.props; + + if (key === "ArrowRight") { + this.setState((prevState: RangeSliderState) => { + const [prevStateMin, prevStateMax] = prevState.value; + const nextStateMax = + prevStateMax >= max ? max : prevStateMax + this.getKeyboardStep(); + return { value: [prevStateMin, nextStateMax] }; + }, this.callback); + } else if (key === "ArrowLeft") { + this.setState((prevState: RangeSliderState) => { + const [prevStateMin, prevStateMax] = prevState.value; + const nextStateMax = + prevStateMax - distance <= prevStateMin + ? prevStateMin + distance + : prevStateMax - this.getKeyboardStep(); + return { value: [prevStateMin, nextStateMax] }; + }, this.callback); + } + }; + + handleBarClick = (e: React.MouseEvent) => { + let point = e.clientX; + const { minX, maxX, width } = this.getCordsProperties(); + if (point < minX) { + point = minX; + } else if (point > maxX) { + point = maxX; + } + const range = + Math.round(((point - minX) * this.range) / width) + this.props.min; + + this.setState((prevState: RangeSliderState) => { + const [prevStateMin, prevStateMax] = prevState.value; + const { distance } = this.props; + if (range <= prevStateMin) { + return { value: [range, prevStateMax] }; + } else if (range >= prevStateMax) { + return { value: [prevStateMin, range] }; + } + if (Math.abs(range - prevStateMin) >= Math.abs(range - prevStateMax)) { + const nextMaxState = + range - prevStateMin < distance ? prevStateMin + distance : range; + return { value: [prevStateMin, nextMaxState] }; + } else { + const nextMinState = + prevStateMax - range < distance ? prevStateMax - distance : range; + return { value: [nextMinState, prevStateMax] }; + } + }, this.callback); + }; + + callback = () => { + if (typeof this.props.onChange === "function") { + const { value } = this.state; + this.props.onChange(value); + } + }; + + clearDocumentEvents = () => { + document.removeEventListener("mouseup", this.clearDocumentEvents); + document.removeEventListener("mousemove", this.mouseMoveMin); + document.removeEventListener("mousemove", this.mouseMoveMax); + document.removeEventListener("touchmove", this.touchMoveMin); + document.removeEventListener("touchmove", this.touchMoveMax); + document.removeEventListener("touchend", this.clearDocumentEvents); + document.removeEventListener("touchcancel", this.clearDocumentEvents); + }; + + componentWillReceiveProps(nextProps: RangeSliderProps) { + const { value, min, max } = nextProps; + if (value !== this.props.value) { + this.setState({ value }); + } + + if (min !== this.props.min || max !== this.props.max) { + this.range = max - min; + } + } + + componentWillUnmount() { + this.clearDocumentEvents(); + } + + render() { + const [minState, maxState] = this.state.value; + const { min, max, colors } = this.props; + const right = 100 - ((maxState - min) * 100) / this.range; + const left = ((minState - min) * 100) / this.range; + + return ( +
+
+
+
+
+ ); + } +} + +const Button = (props: React.HTMLAttributes) => ( + +); diff --git a/src/components/HistogramSlider/RangeSlider/index.ts b/src/components/HistogramSlider/RangeSlider/index.ts new file mode 100644 index 000000000..d14672f0b --- /dev/null +++ b/src/components/HistogramSlider/RangeSlider/index.ts @@ -0,0 +1 @@ +export { RangeSlider } from './RangeSlider'; diff --git a/src/components/HistogramSlider/index.ts b/src/components/HistogramSlider/index.ts new file mode 100644 index 000000000..d91a77994 --- /dev/null +++ b/src/components/HistogramSlider/index.ts @@ -0,0 +1 @@ +export { HistogramSlider } from './HistogramSlider'; From 1b254ee6944c7c4c60d9963e08e33d87e03f2b83 Mon Sep 17 00:00:00 2001 From: zlatan Date: Tue, 15 Jan 2019 14:47:19 +0700 Subject: [PATCH 02/10] histogram: allow to pass width & height --- .../HistogramSlider/Histogram/Histogram.tsx | 74 +++++++++------- .../HistogramSlider/Histogram/index.ts | 2 +- .../HistogramSlider/HistogramSlider.tsx | 29 ++++--- .../RangeSlider/RangeSlider.tsx | 10 ++- .../HistogramSlider/histogramSlider.mdx | 87 +++++++++++++++++++ 5 files changed, 154 insertions(+), 48 deletions(-) create mode 100644 src/components/HistogramSlider/histogramSlider.mdx diff --git a/src/components/HistogramSlider/Histogram/Histogram.tsx b/src/components/HistogramSlider/Histogram/Histogram.tsx index cdcf820d3..13e80a2e2 100644 --- a/src/components/HistogramSlider/Histogram/Histogram.tsx +++ b/src/components/HistogramSlider/Histogram/Histogram.tsx @@ -2,8 +2,9 @@ import React, { Component } from "react"; interface HistogramProps { data: number[]; - maxHeightPx?: number; value: [number, number]; + widthPx: number; + heightPx: number; min: number; max: number; colors: { @@ -19,34 +20,42 @@ interface HistogramState { let maskCount = 0; export class Histogram extends Component { - static defaultProps = { - maxHeightPx: 20 - }; + maskHighlightID: string = `sf-histogram-mask-${maskCount++}`; + maskID: string = `sf-histogram-mask-${maskCount++}`; + viewBoxWidth: number = this.props.data.length; + viewBoxHeight: number = + (this.props.heightPx * this.viewBoxWidth) / this.props.widthPx; constructor(props: HistogramProps) { super(props); - const { data, maxHeightPx } = this.props; + const { data } = this.props; const max = Math.max(...data); - const heightPxPerUnit = maxHeightPx! / max; - const heightData = data.map(v => Math.round(heightPxPerUnit * v)); + const heightPxPerUnit = this.viewBoxHeight / max; + const heightData = data.map(v => + parseFloat((heightPxPerUnit * v).toFixed(2)) + ); this.state = { data: heightData }; } - maskHighlightID: string = `sf-histogram-mask-${maskCount++}`; - maskID: string = `sf-histogram-mask-${maskCount++}`; - numOfColumn: number = this.props.data.length; - componentWillReceiveProps({ data }: HistogramProps) { if (data !== this.props.data) { const max = Math.max(...data); - const heightPxPerUnit = this.props.maxHeightPx! / max; - const heightData = data.map(v => Math.round(heightPxPerUnit * v)); + this.viewBoxWidth = data.length; + this.viewBoxHeight = parseFloat( + ( + (this.props.heightPx * this.viewBoxWidth) / + this.props.widthPx + ).toFixed(2) + ); - this.numOfColumn = data.length; + const heightPxPerUnit = this.viewBoxHeight / max; + const heightData = data.map(v => + parseFloat((heightPxPerUnit * v).toFixed(2)) + ); this.setState({ data: heightData @@ -55,26 +64,27 @@ export class Histogram extends Component { } render() { - const { min, max, value, colors, maxHeightPx } = this.props; + const { min, max, value, colors } = this.props; const [vMin, vMax] = value; const range = max - min; - const start = ((vMin - min) * this.numOfColumn) / range; - const end = start + ((vMax - vMin) * this.numOfColumn) / range; + const start = ((vMin - min) * this.viewBoxWidth) / range; + const end = start + ((vMax - vMin) * this.viewBoxWidth) / range; return ( > { y="0" fill="white" width={start} - height={maxHeightPx} + height={this.viewBoxHeight} /> @@ -104,8 +114,8 @@ export class Histogram extends Component { id={this.maskHighlightID} x="0" y="0" - width={this.numOfColumn} - height={maxHeightPx} + width={this.viewBoxWidth} + height={this.viewBoxHeight} > > { y="0" fill="white" width={end - start} - height={maxHeightPx} + height={this.viewBoxHeight} /> @@ -122,18 +132,16 @@ export class Histogram extends Component { diff --git a/src/components/HistogramSlider/Histogram/index.ts b/src/components/HistogramSlider/Histogram/index.ts index a0a3c6382..3c001d6cf 100644 --- a/src/components/HistogramSlider/Histogram/index.ts +++ b/src/components/HistogramSlider/Histogram/index.ts @@ -1 +1 @@ -export { Histogram } from './Histogram'; +export { Histogram } from "./Histogram"; diff --git a/src/components/HistogramSlider/HistogramSlider.tsx b/src/components/HistogramSlider/HistogramSlider.tsx index fa6dd303e..e064ca416 100644 --- a/src/components/HistogramSlider/HistogramSlider.tsx +++ b/src/components/HistogramSlider/HistogramSlider.tsx @@ -11,6 +11,8 @@ interface HistogramSliderProps { step: number; distance: number; debounceDelay?: number; + widthPx?: number; + heightPx?: number; colors: { in: string; out: string; @@ -27,6 +29,11 @@ export class HistogramSlider extends Component< HistogramSliderProps, HistogramSliderState > { + static defaultProps = { + widthPx: 300, + heightPx: 70 + }; + state: HistogramSliderState = { value: [this.props.value[0], this.props.value[1]] }; @@ -93,15 +100,15 @@ export class HistogramSlider extends Component< } const isDisabled = this.isDisabled(); - const { data, ...rangeSliderProps } = this.props; + const { data, widthPx, ...rangeSliderProps } = this.props; return (
+ -
- -
{ return (
{ className={css({ position: "absolute", top: "0px", - height: "4px", + height: "5px", borderRadius: "999px", backgroundColor: colors.in })} diff --git a/src/components/HistogramSlider/histogramSlider.mdx b/src/components/HistogramSlider/histogramSlider.mdx new file mode 100644 index 000000000..52c293890 --- /dev/null +++ b/src/components/HistogramSlider/histogramSlider.mdx @@ -0,0 +1,87 @@ +--- +name: Histogram Slider +menu: Components +route: /components/histogram-slider +--- + +import { PropsTable, Playground } from "docz"; +import { HistogramSlider } from "./HistogramSlider.tsx"; + +# Histogram Slider + +```javascript +// Import the HistogramSlider component +import { HistogramSlider } from "@sajari/sdk-react"; +``` + +## Usage + +### With default props + +With the `default` props, the ``. + + + + From 17815dd972dd943580e87e251b2472856fb0499c Mon Sep 17 00:00:00 2001 From: zlatan Date: Tue, 15 Jan 2019 16:05:40 +0700 Subject: [PATCH 03/10] histogram: allow to pass custom info component --- .../HistogramSlider/HistogramSlider.tsx | 95 +++++++++++++------ 1 file changed, 66 insertions(+), 29 deletions(-) diff --git a/src/components/HistogramSlider/HistogramSlider.tsx b/src/components/HistogramSlider/HistogramSlider.tsx index e064ca416..a01af41bd 100644 --- a/src/components/HistogramSlider/HistogramSlider.tsx +++ b/src/components/HistogramSlider/HistogramSlider.tsx @@ -4,21 +4,34 @@ import { RangeSlider } from "./RangeSlider"; import { css } from "emotion"; interface HistogramSliderProps { + /** Array of numbers used to render graph */ data: number[]; + /** Default value [start, end] */ value: [number, number]; min: number; max: number; step: number; + /** Minimum range between `start` and `end` */ distance: number; - debounceDelay?: number; widthPx?: number; heightPx?: number; - colors: { + /** `in` color for the selected part */ + colors?: { in: string; out: string; }; + /** Custom component for render UI to reflect [start, end], pass `null` if you don't want to show the part */ + InfoRenderComponent?: + | null + | (( + props: { value: [number, number]; min: number; max: number } + ) => JSX.Element); + /** Showing an `apply` & `reset` button if a function was passed to the prop*/ onApply?: (value: [number, number]) => void; + /** Callback function while the range changed */ onChange?: (value: [number, number]) => void; + /** The delay time for `onChange` get called from the last */ + debounceDelay?: number; } interface HistogramSliderState { @@ -31,12 +44,43 @@ export class HistogramSlider extends Component< > { static defaultProps = { widthPx: 300, - heightPx: 70 + heightPx: 70, + colors: { + in: "#D7D8D8", + out: "#EEEEEE" + }, + InfoRenderComponent: ({ value }: { value: [number, number] }) => ( +
+ ${value[0]} AUD - ${value[1]} AUD +
+ ) }; - state: HistogramSliderState = { - value: [this.props.value[0], this.props.value[1]] - }; + constructor(props: HistogramSliderProps) { + super(props); + + if (this.props.min >= this.props.max) { + console.error( + `The prop "min" should not be greater than the props "max".` + ); + } + + if (this.props.value[0] >= this.props.value[1]) { + console.error( + `The [0] of the prop "value" should not be greater than the [1].` + ); + } + + this.state = { + value: [this.props.value[0], this.props.value[1]] + }; + } timeout: number | undefined; @@ -87,20 +131,13 @@ export class HistogramSlider extends Component< }; render() { - if (this.props.min >= this.props.max) { - console.error( - `The prop "min" should not be greater than the props "max".` - ); - } - - if (this.props.value[0] >= this.props.value[1]) { - console.error( - `The [0] of the prop "value" should not be greater than the [1].` - ); - } - const isDisabled = this.isDisabled(); - const { data, widthPx, ...rangeSliderProps } = this.props; + const { + data, + widthPx, + InfoRenderComponent, + ...rangeSliderProps + } = this.props; return (
+
-
- ${this.state.value[0]} AUD - ${this.state.value[1]} AUD -
+ {InfoRenderComponent && ( + + )} {typeof this.props.onApply === "function" && (
Date: Tue, 15 Jan 2019 16:06:34 +0700 Subject: [PATCH 04/10] histogram: add doc --- .../HistogramSlider/histogramSlider.mdx | 107 ++++++++---------- src/components/HistogramSlider/sampleData.ts | 52 +++++++++ 2 files changed, 102 insertions(+), 57 deletions(-) create mode 100644 src/components/HistogramSlider/sampleData.ts diff --git a/src/components/HistogramSlider/histogramSlider.mdx b/src/components/HistogramSlider/histogramSlider.mdx index 52c293890..2bfbcb7f3 100644 --- a/src/components/HistogramSlider/histogramSlider.mdx +++ b/src/components/HistogramSlider/histogramSlider.mdx @@ -6,6 +6,8 @@ route: /components/histogram-slider import { PropsTable, Playground } from "docz"; import { HistogramSlider } from "./HistogramSlider.tsx"; +import { HistogramSlider as PropsHistogramSlider } from "./HistogramSlider.tsx"; +import sampleData from "./sampleData.ts"; # Histogram Slider @@ -16,72 +18,63 @@ import { HistogramSlider } from "@sajari/sdk-react"; ## Usage -### With default props +### With default width & height -With the `default` props, the ``. + + + + +### Custom width, height & colors + + +### Custom Info Render & opApply call + + + + data={sampleData} + InfoRenderComponent={({ value }) => ( +
+ From {value[0]} to {value[1]} +
+ )} + onApply={value => { + if(value[0] !== 0 || value[1] !== 1000){ + alert(value); + } + }} + +/> +
+ +## Props + + diff --git a/src/components/HistogramSlider/sampleData.ts b/src/components/HistogramSlider/sampleData.ts new file mode 100644 index 000000000..d70d9b101 --- /dev/null +++ b/src/components/HistogramSlider/sampleData.ts @@ -0,0 +1,52 @@ +export default [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 4, + 9, + 16, + 44, + 31, + 40, + 42, + 32, + 23, + 17, + 26, + 39, + 14, + 23, + 14, + 17, + 14, + 13, + 18, + 18, + 20, + 11, + 7, + 15, + 11, + 8, + 8, + 8, + 8, + 8, + 5, + 6, + 6, + 3, + 5, + 3, + 6, + 2, + 4, + 0, + 5, + 63 +]; From a76365904d177e797905fa6909ea1d6f8d68d759 Mon Sep 17 00:00:00 2001 From: zlatan Date: Tue, 15 Jan 2019 16:13:34 +0700 Subject: [PATCH 05/10] histogram: add export --- src/components/index.ts | 2 ++ src/index.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/index.ts b/src/components/index.ts index 14658bee6..2d7bd7120 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -24,3 +24,5 @@ export { Checkbox } from "./Checkbox"; export { Radio } from "./Radio"; export { Search } from "./Search"; + +export { HistogramSlider } from "./HistogramSlider"; diff --git a/src/index.ts b/src/index.ts index 1e8326c2c..ad20bec3e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,7 +30,8 @@ export { Select, Checkbox, Radio, - Search + Search, + HistogramSlider } from "./components"; export { Overlay } from "./containers"; From 449c8abc6acc233db570d9c7bb5337669ab549cd Mon Sep 17 00:00:00 2001 From: zlatan Date: Wed, 16 Jan 2019 10:27:10 +0700 Subject: [PATCH 06/10] histogram: define DefaultInfoRenderComponent out of class --- .../HistogramSlider/HistogramSlider.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/HistogramSlider/HistogramSlider.tsx b/src/components/HistogramSlider/HistogramSlider.tsx index a01af41bd..8166b80d8 100644 --- a/src/components/HistogramSlider/HistogramSlider.tsx +++ b/src/components/HistogramSlider/HistogramSlider.tsx @@ -38,6 +38,18 @@ interface HistogramSliderState { value: [number, number]; } +const DefaultInfoRenderComponent = ({ value }: { value: [number, number] }) => ( +
+ ${value[0]} AUD - ${value[1]} AUD +
+); + export class HistogramSlider extends Component< HistogramSliderProps, HistogramSliderState @@ -49,17 +61,7 @@ export class HistogramSlider extends Component< in: "#D7D8D8", out: "#EEEEEE" }, - InfoRenderComponent: ({ value }: { value: [number, number] }) => ( -
- ${value[0]} AUD - ${value[1]} AUD -
- ) + InfoRenderComponent: DefaultInfoRenderComponent }; constructor(props: HistogramSliderProps) { From b8cbd031dd4e6acf859052847d311753b8645a08 Mon Sep 17 00:00:00 2001 From: zlatan Date: Wed, 16 Jan 2019 14:27:33 +0700 Subject: [PATCH 07/10] histogram: remove RangeSlider internal state --- .../RangeSlider/RangeSlider.tsx | 249 ++++++++++-------- 1 file changed, 137 insertions(+), 112 deletions(-) diff --git a/src/components/HistogramSlider/RangeSlider/RangeSlider.tsx b/src/components/HistogramSlider/RangeSlider/RangeSlider.tsx index 26bbd2711..b39cd2680 100644 --- a/src/components/HistogramSlider/RangeSlider/RangeSlider.tsx +++ b/src/components/HistogramSlider/RangeSlider/RangeSlider.tsx @@ -7,25 +7,20 @@ interface RangeSliderProps { step: number; value: [number, number]; distance: number; - onChange?: (value: [number, number]) => void; + onChange: (value: [number, number]) => void; colors: { in: string; out: string; }; } -interface RangeSliderState { - value: [number, number]; -} +export class RangeSlider extends Component { + ref = React.createRef(); -export class RangeSlider extends Component { - state: RangeSliderState = { - value: [this.props.value[0], this.props.value[1]] + getRange = () => { + return this.props.max - this.props.min; }; - ref = React.createRef(); - range = this.props.max - this.props.min; - getKeyboardStep = () => { let step = Math.floor(this.props.max / 100); return step < this.props.step ? this.props.step : step; @@ -82,49 +77,69 @@ export class RangeSlider extends Component { dragMin = (clientX: number) => { const { minX, width } = this.getCordsProperties(); const percent = clientX < minX ? 0 : (clientX - minX) / width; - let min = percent * this.range; - - this.setState(prevState => { - const [prevStateMin, prevStateMax] = prevState.value; - if (clientX <= minX) { - return { value: [this.props.min, prevStateMax] }; - } - - const delta = (min - prevStateMin + this.props.min) / this.props.step; + const { + onChange, + value: [prevStateMin, prevStateMax], + min, + step, + distance + } = this.props; + let nextStateMin = percent * this.getRange(); + let value: [number, number]; + + if (clientX <= minX) { + value = [min, prevStateMax]; + } else { + const delta = (nextStateMin - prevStateMin + min) / step; let addition = 0; if (Math.abs(delta) >= 1) { - addition = Math.floor(delta / this.props.step) * this.props.step; + addition = Math.floor(delta / step) * step; } - min = prevStateMin + addition; - if (min + this.props.distance > prevStateMax) { - min = prevStateMax - this.props.distance; + nextStateMin = prevStateMin + addition; + if (nextStateMin + distance > prevStateMax) { + nextStateMin = prevStateMax - distance; } - return { value: [min, prevStateMax] }; - }, this.callback); + value = [nextStateMin, prevStateMax]; + } + + this.setState({ value }, () => { + onChange(value); + }); }; dragMax = (clientX: number) => { const { maxX, minX, width } = this.getCordsProperties(); const percent = clientX > maxX ? 1 : (clientX - minX) / width; - let max = percent * this.range; - - this.setState((prevState: RangeSliderState) => { - const [prevStateMin, prevStateMax] = prevState.value; - - if (clientX >= maxX) { - return { value: [prevStateMin, this.props.max] }; - } - const delta = (max - prevStateMax + this.props.min) / this.props.step; + let nextStateMax = percent * this.getRange(); + + const { + onChange, + value: [prevStateMin, prevStateMax], + step, + min, + max, + distance + } = this.props; + + let value: [number, number]; + if (clientX >= maxX) { + value = [prevStateMin, max]; + } else { + const delta = (nextStateMax - prevStateMax + min) / step; let addition = 0; if (Math.abs(delta) >= 1) { - addition = Math.ceil(delta / this.props.step) * this.props.step; + addition = Math.ceil(delta / step) * step; } - max = prevStateMax + addition; - if (max - this.props.distance < prevStateMin) { - max = prevStateMin + this.props.distance; + nextStateMax = prevStateMax + addition; + if (nextStateMax - distance < prevStateMin) { + nextStateMax = prevStateMin + distance; } - return { value: [prevStateMin, max] }; - }, this.callback); + value = [prevStateMin, nextStateMax]; + } + + this.setState({ value }, () => { + onChange(value); + }); }; handleMinKeydown = (e: React.KeyboardEvent) => { @@ -133,24 +148,32 @@ export class RangeSlider extends Component { e.preventDefault(); return; } - const { distance, min } = this.props; + const { + distance, + min, + value: [prevStateMin, prevStateMax], + onChange + } = this.props; + let value: [number, number]; if (key === "ArrowRight") { - this.setState((prevState: RangeSliderState) => { - const [prevStateMin, prevStateMax] = prevState.value; - const nextStateMin = - prevStateMin + distance >= prevStateMax - ? prevStateMax - distance - : prevStateMin + this.getKeyboardStep(); - return { value: [nextStateMin, prevStateMax] }; - }, this.callback); + const nextStateMin = + prevStateMin + distance >= prevStateMax + ? prevStateMax - distance + : prevStateMin + this.getKeyboardStep(); + + value = [nextStateMin, prevStateMax]; + this.setState({ value }, () => { + onChange(value); + }); } else if (key === "ArrowLeft") { - this.setState((prevState: RangeSliderState) => { - const [prevStateMin, prevStateMax] = prevState.value; - const nextStateMin = - prevStateMin <= min ? min : prevStateMin - this.getKeyboardStep(); - return { value: [nextStateMin, prevStateMax] }; - }, this.callback); + const nextStateMin = + prevStateMin <= min ? min : prevStateMin - this.getKeyboardStep(); + + value = [nextStateMin, prevStateMax]; + this.setState({ value }, () => { + onChange(value); + }); } }; @@ -160,24 +183,32 @@ export class RangeSlider extends Component { e.preventDefault(); return; } - const { distance, max } = this.props; + const { + distance, + max, + value: [prevStateMin, prevStateMax], + onChange + } = this.props; + let value: [number, number]; if (key === "ArrowRight") { - this.setState((prevState: RangeSliderState) => { - const [prevStateMin, prevStateMax] = prevState.value; - const nextStateMax = - prevStateMax >= max ? max : prevStateMax + this.getKeyboardStep(); - return { value: [prevStateMin, nextStateMax] }; - }, this.callback); + const nextStateMax = + prevStateMax >= max ? max : prevStateMax + this.getKeyboardStep(); + value = [prevStateMin, nextStateMax]; + + this.setState({ value }, () => { + onChange(value); + }); } else if (key === "ArrowLeft") { - this.setState((prevState: RangeSliderState) => { - const [prevStateMin, prevStateMax] = prevState.value; - const nextStateMax = - prevStateMax - distance <= prevStateMin - ? prevStateMin + distance - : prevStateMax - this.getKeyboardStep(); - return { value: [prevStateMin, nextStateMax] }; - }, this.callback); + const nextStateMax = + prevStateMax - distance <= prevStateMin + ? prevStateMin + distance + : prevStateMax - this.getKeyboardStep(); + value = [prevStateMin, nextStateMax]; + + this.setState({ value }, () => { + onChange(value); + }); } }; @@ -189,34 +220,34 @@ export class RangeSlider extends Component { } else if (point > maxX) { point = maxX; } - const range = - Math.round(((point - minX) * this.range) / width) + this.props.min; - - this.setState((prevState: RangeSliderState) => { - const [prevStateMin, prevStateMax] = prevState.value; - const { distance } = this.props; - if (range <= prevStateMin) { - return { value: [range, prevStateMax] }; - } else if (range >= prevStateMax) { - return { value: [prevStateMin, range] }; - } - if (Math.abs(range - prevStateMin) >= Math.abs(range - prevStateMax)) { - const nextMaxState = - range - prevStateMin < distance ? prevStateMin + distance : range; - return { value: [prevStateMin, nextMaxState] }; - } else { - const nextMinState = - prevStateMax - range < distance ? prevStateMax - distance : range; - return { value: [nextMinState, prevStateMax] }; - } - }, this.callback); - }; - - callback = () => { - if (typeof this.props.onChange === "function") { - const { value } = this.state; - this.props.onChange(value); + const { + onChange, + value: [prevStateMin, prevStateMax], + min + } = this.props; + const range = Math.round(((point - minX) * this.getRange()) / width) + min; + + let value: [number, number]; + const { distance } = this.props; + if (range <= prevStateMin) { + value = [range, prevStateMax]; + } else if (range >= prevStateMax) { + value = [prevStateMin, range]; + } else if ( + Math.abs(range - prevStateMin) >= Math.abs(range - prevStateMax) + ) { + const nextMaxState = + range - prevStateMin < distance ? prevStateMin + distance : range; + value = [prevStateMin, nextMaxState]; + } else { + const nextMinState = + prevStateMax - range < distance ? prevStateMax - distance : range; + value = [nextMinState, prevStateMax]; } + + this.setState({ value }, () => { + onChange(value); + }); }; clearDocumentEvents = () => { @@ -229,26 +260,20 @@ export class RangeSlider extends Component { document.removeEventListener("touchcancel", this.clearDocumentEvents); }; - componentWillReceiveProps(nextProps: RangeSliderProps) { - const { value, min, max } = nextProps; - if (value !== this.props.value) { - this.setState({ value }); - } - - if (min !== this.props.min || max !== this.props.max) { - this.range = max - min; - } - } - componentWillUnmount() { this.clearDocumentEvents(); } render() { - const [minState, maxState] = this.state.value; - const { min, max, colors } = this.props; - const right = 100 - ((maxState - min) * 100) / this.range; - const left = ((minState - min) * 100) / this.range; + const range = this.getRange(); + const { + min, + max, + colors, + value: [minState, maxState] + } = this.props; + const right = 100 - ((maxState - min) * 100) / range; + const left = ((minState - min) * 100) / range; return (
Date: Wed, 16 Jan 2019 15:25:39 +0700 Subject: [PATCH 08/10] histogram: migrate componentWillReceiveProps to getDerivedStateFromProps --- .../HistogramSlider/Histogram/Histogram.tsx | 93 +++++++++---------- .../HistogramSlider/HistogramSlider.tsx | 24 +++-- 2 files changed, 62 insertions(+), 55 deletions(-) diff --git a/src/components/HistogramSlider/Histogram/Histogram.tsx b/src/components/HistogramSlider/Histogram/Histogram.tsx index 13e80a2e2..0882d1e88 100644 --- a/src/components/HistogramSlider/Histogram/Histogram.tsx +++ b/src/components/HistogramSlider/Histogram/Histogram.tsx @@ -15,6 +15,7 @@ interface HistogramProps { interface HistogramState { data: number[]; + prevPropData: number[]; } let maskCount = 0; @@ -22,53 +23,47 @@ let maskCount = 0; export class Histogram extends Component { maskHighlightID: string = `sf-histogram-mask-${maskCount++}`; maskID: string = `sf-histogram-mask-${maskCount++}`; - viewBoxWidth: number = this.props.data.length; - viewBoxHeight: number = - (this.props.heightPx * this.viewBoxWidth) / this.props.widthPx; - constructor(props: HistogramProps) { - super(props); - - const { data } = this.props; - const max = Math.max(...data); - const heightPxPerUnit = this.viewBoxHeight / max; - const heightData = data.map(v => - parseFloat((heightPxPerUnit * v).toFixed(2)) - ); - - this.state = { - data: heightData - }; - } - - componentWillReceiveProps({ data }: HistogramProps) { - if (data !== this.props.data) { - const max = Math.max(...data); - this.viewBoxWidth = data.length; - this.viewBoxHeight = parseFloat( - ( - (this.props.heightPx * this.viewBoxWidth) / - this.props.widthPx - ).toFixed(2) - ); + state: HistogramState = { + prevPropData: [], + data: [] + }; - const heightPxPerUnit = this.viewBoxHeight / max; - const heightData = data.map(v => - parseFloat((heightPxPerUnit * v).toFixed(2)) - ); + static getDerivedStateFromProps( + nextProps: HistogramProps, + prevState: HistogramState + ) { + if (nextProps.data !== prevState.prevPropData) { + const viewBoxWidth = nextProps.data.length; + const viewBoxHeight = + (nextProps.heightPx * viewBoxWidth) / nextProps.widthPx; + const max = Math.max(...nextProps.data); + const heightPxPerUnit = viewBoxHeight / max; - this.setState({ - data: heightData - }); + return { + data: nextProps.data.map(v => + parseFloat((heightPxPerUnit * v).toFixed(2)) + ) + }; } + + return null; } + getViewboxSize = () => { + const viewBoxWidth = this.props.data.length; + const viewBoxHeight = + (this.props.heightPx * viewBoxWidth) / this.props.widthPx; + return { viewBoxHeight, viewBoxWidth }; + }; + render() { const { min, max, value, colors } = this.props; const [vMin, vMax] = value; const range = max - min; - const start = ((vMin - min) * this.viewBoxWidth) / range; - const end = start + ((vMax - vMin) * this.viewBoxWidth) / range; + const { viewBoxWidth, viewBoxHeight } = this.getViewboxSize(); + const start = ((vMin - min) * viewBoxWidth) / range; + const end = start + ((vMax - vMin) * viewBoxWidth) / range; return ( @@ -76,15 +71,15 @@ export class Histogram extends Component { display="block" width="100%" xmlns="http://www.w3.org/2000/svg" - viewBox={`0 0 ${this.viewBoxWidth} ${this.viewBoxHeight}`} + viewBox={`0 0 ${viewBoxWidth} ${viewBoxHeight}`} > > { y="0" fill="white" width={start} - height={this.viewBoxHeight} + height={viewBoxHeight} /> @@ -114,8 +109,8 @@ export class Histogram extends Component { id={this.maskHighlightID} x="0" y="0" - width={this.viewBoxWidth} - height={this.viewBoxHeight} + width={viewBoxWidth} + height={viewBoxHeight} > > { y="0" fill="white" width={end - start} - height={this.viewBoxHeight} + height={viewBoxHeight} /> @@ -132,7 +127,7 @@ export class Histogram extends Component { { ( @@ -80,17 +81,28 @@ export class HistogramSlider extends Component< } this.state = { - value: [this.props.value[0], this.props.value[1]] + value: [this.props.value[0], this.props.value[1]], + prevPropValue: [this.props.value[0], this.props.value[1]] }; } timeout: number | undefined; - componentWillReceiveProps(nextProps: HistogramSliderProps) { - const { value } = nextProps; - if (value !== this.props.value) { - this.setState({ value }); + static getDerivedStateFromProps( + nextProps: HistogramSliderProps, + prevState: HistogramSliderState + ) { + if ( + nextProps.value[0] !== prevState.prevPropValue[0] || + nextProps.value[1] !== prevState.prevPropValue[1] + ) { + return { + value: [nextProps.value[0], nextProps.value[1]], + prevPropValue: [nextProps.value[0], nextProps.value[1]] + }; } + + return null; } reset = (e: React.MouseEvent) => { @@ -119,7 +131,7 @@ export class HistogramSlider extends Component< } this.timeout = window.setTimeout(() => { //@ts-ignore: has been checked outsite - this.props.onChange(this.state.value); + this.props.onChange(value); }, this.props.debounceDelay || 500); } }); From 0e9b9888de225029957979456bbc15f2ce3a6643 Mon Sep 17 00:00:00 2001 From: zlatan Date: Tue, 29 Jan 2019 13:56:52 +0700 Subject: [PATCH 09/10] histogram: allow passing custom drag button via prop --- .../HistogramSlider/HistogramSlider.tsx | 6 ++ .../RangeSlider/RangeSlider.tsx | 97 ++++++++++++++----- 2 files changed, 78 insertions(+), 25 deletions(-) diff --git a/src/components/HistogramSlider/HistogramSlider.tsx b/src/components/HistogramSlider/HistogramSlider.tsx index f16c68683..f95b01847 100644 --- a/src/components/HistogramSlider/HistogramSlider.tsx +++ b/src/components/HistogramSlider/HistogramSlider.tsx @@ -26,6 +26,10 @@ interface HistogramSliderProps { | (( props: { value: [number, number]; min: number; max: number } ) => JSX.Element); + /** Custom component for render drag button UI, pass `null` if you don't want to show the part */ + ButtonRenderComponent?: + | null + | ((props: { focused?: boolean }) => JSX.Element); /** Showing an `apply` & `reset` button if a function was passed to the prop*/ onApply?: (value: [number, number]) => void; /** Callback function while the range changed */ @@ -150,6 +154,7 @@ export class HistogramSlider extends Component< data, widthPx, InfoRenderComponent, + ButtonRenderComponent, ...rangeSliderProps } = this.props; @@ -176,6 +181,7 @@ export class HistogramSlider extends Component< colors={this.props.colors!} value={this.state.value} onChange={this.handleSliderChange} + ButtonRenderComponent={ButtonRenderComponent} />
diff --git a/src/components/HistogramSlider/RangeSlider/RangeSlider.tsx b/src/components/HistogramSlider/RangeSlider/RangeSlider.tsx index b39cd2680..7661128fe 100644 --- a/src/components/HistogramSlider/RangeSlider/RangeSlider.tsx +++ b/src/components/HistogramSlider/RangeSlider/RangeSlider.tsx @@ -1,4 +1,4 @@ -import React, { Component } from "react"; +import React, { Component, PureComponent } from "react"; import { css } from "emotion"; interface RangeSliderProps { @@ -8,6 +8,9 @@ interface RangeSliderProps { value: [number, number]; distance: number; onChange: (value: [number, number]) => void; + ButtonRenderComponent?: + | null + | ((props: { focused?: boolean }) => JSX.Element); colors: { in: string; out: string; @@ -264,16 +267,23 @@ export class RangeSlider extends Component { this.clearDocumentEvents(); } + preventDefaultClick = (e: React.MouseEvent) => { + e.preventDefault(); + }; + render() { const range = this.getRange(); const { min, max, colors, - value: [minState, maxState] + value: [minState, maxState], + ButtonRenderComponent } = this.props; const right = 100 - ((maxState - min) * 100) / range; const left = ((minState - min) * 100) / range; + const ButtonInnerComponent = + ButtonRenderComponent || DefaultRenderButtonComponent; return (
{
@@ -359,8 +367,20 @@ export class RangeSlider extends Component { } } -const Button = (props: React.HTMLAttributes) => ( - + ); + +class Button extends React.PureComponent< + { + ButtonRenderComponent: ((props: { focused?: boolean }) => JSX.Element); + } & React.HTMLAttributes +> { + state = { + focused: false + }; + + handleFocus = () => { + this.setState({ focused: true }); + }; + + handleBlur = () => { + this.setState({ focused: false }); + }; + + render() { + const { ButtonRenderComponent, ...rest } = this.props; + return ( + + ); + } +} From cb7b7e97d0a4770cd080e460e2833332d89fdcce Mon Sep 17 00:00:00 2001 From: zlatan Date: Tue, 29 Jan 2019 13:59:41 +0700 Subject: [PATCH 10/10] histogram: update Info to render only values --- src/components/HistogramSlider/HistogramSlider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HistogramSlider/HistogramSlider.tsx b/src/components/HistogramSlider/HistogramSlider.tsx index f95b01847..1fbd944d4 100644 --- a/src/components/HistogramSlider/HistogramSlider.tsx +++ b/src/components/HistogramSlider/HistogramSlider.tsx @@ -51,7 +51,7 @@ const DefaultInfoRenderComponent = ({ value }: { value: [number, number] }) => ( color: "#666666" })} > - ${value[0]} AUD - ${value[1]} AUD + {value[0]} - {value[1]}
);