Skip to content

Commit 2b84769

Browse files
committed
builds
1 parent cb9ea31 commit 2b84769

38 files changed

+13491
-9184
lines changed

package-lock.json

+8,863-6,086
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+17-19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "stream-charts",
3-
"version": "0.2.2-SNAPSHOT",
3+
"version": "1.0.0-SNAPSHOT",
44
"description": "Charts for steaming data",
55
"author": "Rob Philipp",
66
"homepage": "https://robphilipp.github.io/stream-charts/",
@@ -29,24 +29,22 @@
2929
]
3030
},
3131
"devDependencies": {
32-
"@testing-library/jest-dom": "^4.2.4",
33-
"@testing-library/react": "^9.4.0",
34-
"@testing-library/user-event": "^7.2.1",
35-
"@types/d3": "^5.7.2",
36-
"@types/jest": "^24.9.1",
37-
"@types/node": "^12.12.25",
38-
"@types/react": "^16.9.19",
39-
"@types/react-dom": "^16.9.5",
40-
"d3": "^5.15.0",
41-
"jest": "^26.1.0",
42-
"prelude-ts": "^0.8.3",
43-
"react": "^16.12.0",
44-
"react-dom": "^16.12.0",
45-
"rxjs": "^6.5.4",
46-
"ts-jest": "^26.1.1",
47-
"ts-loader": "^7.0.5",
48-
"ts-node": "^8.10.2",
49-
"typescript": "^3.9.3"
32+
"d3": "^7.0.3",
33+
"prelude-ts": "^1.0.3",
34+
"react": "^17.0.2",
35+
"react-dom": "^17.0.2",
36+
"rxjs": "^7.3.0",
37+
"@types/d3": "^7.0.0",
38+
"@types/jest": "^27.0.2",
39+
"@types/node": "^16.9.5",
40+
"@types/react": "^17.0.24",
41+
"@types/react-dom": "^17.0.9",
42+
"ts-jest": "^27.0.5",
43+
"ts-loader": "^9.2.6",
44+
"ts-node": "^10.2.1",
45+
"@testing-library/jest-dom": "^5.14.1",
46+
"jest": "^27.2.1",
47+
"typescript": "^4.4.3"
5048
},
5149
"files": [
5250
"package.json",

src/app/charts/CategoryAxis.tsx

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import * as axes from "./axes"
2+
import {AxesLabelFont, AxisLocation, defaultAxesLabelFont} from "./axes"
3+
import * as d3 from "d3";
4+
import {ScaleBand} from "d3";
5+
import {useChart} from "./hooks/useChart";
6+
import {useEffect, useRef} from "react";
7+
import {Dimensions, Margin} from "./margins";
8+
import {SvgSelection} from "./d3types";
9+
10+
interface Props {
11+
// the unique ID of the axis
12+
axisId: string
13+
// the location of the axis. for y-axes, this mut be either left or right
14+
location: AxisLocation.Left | AxisLocation.Right
15+
// category axes
16+
scale?: ScaleBand<string>
17+
// the min and max values for the axis
18+
categories: Array<string>
19+
// the font for drawing the axis ticks and labels
20+
font?: Partial<AxesLabelFont>
21+
// the axis label
22+
label: string
23+
}
24+
25+
/**
26+
* Category axis, which for the moment is only available as a y-axis. The category axis requires
27+
* a set of categories that will form the y-axis. Generally, these categories should be the name
28+
* of the series used to represent each category.
29+
* @param props The properties for the component
30+
* @return null
31+
* @constructor
32+
*/
33+
export function CategoryAxis(props: Props): null {
34+
const {
35+
chartId,
36+
container,
37+
plotDimensions,
38+
margin,
39+
addYAxis,
40+
color,
41+
} = useChart()
42+
43+
const {
44+
axisId,
45+
location,
46+
categories,
47+
label,
48+
} = props
49+
50+
const axisRef = useRef<axes.CategoryAxis>()
51+
52+
const axisIdRef = useRef<string>(axisId)
53+
const marginRef = useRef<Margin>(margin)
54+
useEffect(
55+
() => {
56+
axisIdRef.current = axisId
57+
marginRef.current = margin
58+
},
59+
[axisId, margin]
60+
)
61+
62+
useEffect(
63+
() => {
64+
if (container) {
65+
const svg = d3.select<SVGSVGElement, any>(container)
66+
const font: AxesLabelFont = {...defaultAxesLabelFont, color, ...props.font}
67+
68+
if (axisRef.current === undefined) {
69+
axisRef.current = addCategoryYAxis(
70+
chartId,
71+
axisId,
72+
svg,
73+
plotDimensions,
74+
categories,
75+
font,
76+
margin,
77+
label,
78+
location
79+
)
80+
// add the y-axis to the chart context
81+
addYAxis(axisRef.current, axisId)
82+
} else {
83+
// update the category size in case the plot dimensions changed
84+
axisRef.current.categorySize = axisRef.current.update(categories, categories.length, plotDimensions, margin)
85+
svg.select(`#${labelIdFor(chartId, location)}`).attr('fill', color)
86+
}
87+
}
88+
},
89+
[addYAxis, axisId, categories, chartId, color, container, label, location, margin, plotDimensions, props.font]
90+
)
91+
92+
return null
93+
}
94+
95+
function labelIdFor(chartId: number, location: AxisLocation.Left | AxisLocation.Right): string {
96+
return `stream-chart-x-axis-${location}-label-${chartId}`
97+
}
98+
99+
function categorySizeFor(dimensions: Dimensions, margin: Margin, numCategories: number): number {
100+
return Math.max(margin.bottom, dimensions.height - margin.bottom) / numCategories
101+
}
102+
103+
function addCategoryYAxis(
104+
chartId: number,
105+
axisId: string,
106+
svg: SvgSelection,
107+
plotDimensions: Dimensions,
108+
categories: Array<string>,
109+
axesLabelFont: AxesLabelFont,
110+
margin: Margin,
111+
axisLabel: string,
112+
location: AxisLocation.Left | AxisLocation.Right,
113+
): axes.CategoryAxis {
114+
const categorySize = categorySizeFor(plotDimensions, margin, categories.length)
115+
const scale = d3.scaleBand()
116+
.domain(categories)
117+
.range([0, categorySize * categories.length]);
118+
119+
// create and add the axes
120+
const generator = location === AxisLocation.Left ? d3.axisLeft(scale) : d3.axisRight(scale)
121+
122+
const selection = svg
123+
.append<SVGGElement>('g')
124+
.attr('id', `y-axis-selection-${chartId}`)
125+
.attr('class', 'y-axis')
126+
.attr('transform', `translate(${xTranslation(location, plotDimensions, margin)}, ${margin.top})`)
127+
.call(generator);
128+
129+
svg
130+
.append<SVGTextElement>('text')
131+
.attr('id', labelIdFor(chartId, location))
132+
.attr('text-anchor', 'middle')
133+
.attr('font-size', axesLabelFont.size)
134+
.attr('fill', axesLabelFont.color)
135+
.attr('font-family', axesLabelFont.family)
136+
.attr('font-weight', axesLabelFont.weight)
137+
.attr('transform', `translate(${labelXTranslation(location, plotDimensions, margin, axesLabelFont)}, ${labelYTranslation(plotDimensions, margin)}) rotate(-90)`)
138+
.text(axisLabel)
139+
140+
const axis = {axisId, selection, location, scale, generator, categorySize, update: () => categorySize}
141+
142+
return {
143+
...axis,
144+
update: (categoryNames, unfilteredSize, dimensions) =>
145+
updateCategoryYAxis(chartId, svg, axis, dimensions, unfilteredSize, categoryNames, axesLabelFont, margin, location)
146+
}
147+
}
148+
149+
function updateCategoryYAxis(
150+
chartId: number,
151+
svg: SvgSelection,
152+
axis: axes.CategoryAxis,
153+
plotDimensions: Dimensions,
154+
unfilteredSize: number,
155+
names: Array<string>,
156+
axesLabelFont: AxesLabelFont,
157+
margin: Margin,
158+
location: AxisLocation.Left | AxisLocation.Right,
159+
): number {
160+
const categorySize = categorySizeFor(plotDimensions, margin, unfilteredSize)
161+
axis.scale
162+
.domain(names)
163+
.range([0, categorySize * names.length])
164+
axis.selection
165+
.attr('transform', `translate(${xTranslation(location, plotDimensions, margin)}, ${margin.top})`)
166+
.call(axis.generator)
167+
168+
svg
169+
.select(`#${labelIdFor(chartId, location)}`)
170+
.attr('transform', `translate(${labelXTranslation(location, plotDimensions, margin, axesLabelFont)}, ${labelYTranslation(plotDimensions, margin)}) rotate(-90)`)
171+
172+
return categorySize
173+
}
174+
175+
176+
function xTranslation(location: AxisLocation.Left | AxisLocation.Right, plotDimensions: Dimensions, margin: Margin): number {
177+
return location === AxisLocation.Left ?
178+
margin.left :
179+
margin.left + plotDimensions.width
180+
}
181+
182+
function labelXTranslation(
183+
location: AxisLocation.Left | AxisLocation.Right,
184+
plotDimensions: Dimensions,
185+
margin: Margin,
186+
axesLabelFont: AxesLabelFont,
187+
): number {
188+
return location === AxisLocation.Left ?
189+
axesLabelFont.size :
190+
margin.left + plotDimensions.width + margin.right - axesLabelFont.size
191+
}
192+
193+
function labelYTranslation(plotDimensions: Dimensions, margin: Margin): number {
194+
return (margin.top + margin.bottom + plotDimensions.height) / 2
195+
}
196+

src/app/charts/Chart.tsx

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import * as React from 'react'
2+
import {useEffect, useMemo, useRef} from 'react'
3+
import {Dimensions, Margin, plotDimensionsFrom} from "./margins";
4+
import {initialSvgStyle, SvgStyle} from "./svgStyle";
5+
import {Datum, Series} from "./datumSeries";
6+
import {Observable, Subscription} from "rxjs";
7+
import {ChartData} from "./chartData";
8+
import {GSelection} from "./d3types";
9+
import ChartProvider, {defaultMargin} from "./hooks/useChart";
10+
import * as d3 from "d3";
11+
import {SeriesLineStyle} from "./axes";
12+
import {createPlotContainer} from "./plot";
13+
14+
// const defaultAxesStyle = {color: '#d2933f'}
15+
const defaultBackground = '#202020';
16+
17+
interface Props {
18+
width: number
19+
height: number
20+
margin?: Partial<Margin>
21+
// axisLabelFont?: Partial<AxesLabelFont>
22+
// axisStyle?: Partial<CSSProperties>
23+
color?: string
24+
backgroundColor?: string
25+
svgStyle?: Partial<SvgStyle>
26+
seriesStyles?: Map<string, SeriesLineStyle>
27+
28+
// initial data
29+
// initialData: Map<string, Series>
30+
initialData: Array<Series>
31+
seriesFilter?: RegExp
32+
33+
// data stream
34+
seriesObservable?: Observable<ChartData>
35+
windowingTime?: number
36+
shouldSubscribe?: boolean
37+
onSubscribe?: (subscription: Subscription) => void
38+
onUpdateData?: (seriesName: string, data: Array<Datum>) => void
39+
onUpdateTime?: (time: number) => void
40+
41+
// regex filter used to select which series are displayed
42+
filter?: RegExp
43+
44+
children: JSX.Element | Array<JSX.Element>;
45+
}
46+
47+
export function Chart(props: Props): JSX.Element {
48+
const {
49+
width,
50+
height,
51+
color = '#d2933f',
52+
backgroundColor = defaultBackground,
53+
seriesStyles = new Map(),
54+
initialData,
55+
seriesFilter = /./,
56+
seriesObservable,
57+
windowingTime = 100,
58+
shouldSubscribe = true,
59+
60+
children,
61+
} = props
62+
63+
// override the defaults with the parent's properties, leaving any unset values as the default value
64+
const margin = {...defaultMargin, ...props.margin}
65+
const svgStyle = useMemo<SvgStyle>(
66+
() => ({...initialSvgStyle, ...props.svgStyle, width: props.width, height: props.height}),
67+
[props.height, props.svgStyle, props.width]
68+
)
69+
70+
// id of the chart to avoid dom conflicts when multiple charts are used in the same app
71+
const chartId = useRef<number>(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER))
72+
73+
// hold a reference to the current width and the plot dimensions
74+
const plotDimRef = useRef<Dimensions>(plotDimensionsFrom(width, height, margin))
75+
76+
// the container that holds the d3 svg element
77+
const mainGRef = useRef<GSelection | null>(null)
78+
const containerRef = useRef<SVGSVGElement>(null)
79+
80+
// creates the main <g> element for the chart if it doesn't already exist, otherwise
81+
// updates the svg element with the updated dimensions or style properties
82+
useEffect(
83+
() => {
84+
if (containerRef.current) {
85+
// create the main SVG element if it doesn't already exist
86+
if (!mainGRef.current) {
87+
mainGRef.current = createPlotContainer(chartId.current, containerRef.current, plotDimRef.current, color)
88+
}
89+
90+
// build up the svg style from the defaults and any svg style object
91+
// passed in as properties
92+
const style = Object.getOwnPropertyNames(svgStyle)
93+
.map(name => `${name}: ${svgStyle[name]}; `)
94+
.join("")
95+
96+
// when the chart "backgroundColor" property is set (i.e. not the default value),
97+
// then we need add it to the styles, overwriting any color that may have been
98+
// set in the svg style object
99+
const background = backgroundColor !== defaultBackground ?
100+
`background-color: ${backgroundColor}; ` :
101+
''
102+
103+
// update the dimension and style
104+
d3.select<SVGSVGElement, any>(containerRef.current)
105+
.attr('width', width)
106+
.attr('height', height)
107+
.attr('style', style + background + ` color: ${color}`)
108+
}
109+
},
110+
[color, backgroundColor, height, svgStyle, width]
111+
)
112+
113+
return (
114+
<>
115+
<svg ref={containerRef}/>
116+
<ChartProvider
117+
chartId={chartId.current}
118+
container={containerRef.current}
119+
mainG={mainGRef.current}
120+
containerDimensions={{width, height}}
121+
margin={margin}
122+
color={color}
123+
seriesStyles={seriesStyles}
124+
initialData={initialData}
125+
seriesFilter={seriesFilter}
126+
127+
seriesObservable={seriesObservable}
128+
windowingTime={windowingTime}
129+
shouldSubscribe={shouldSubscribe}
130+
>
131+
{
132+
// the chart elements are the children
133+
children
134+
}
135+
</ChartProvider>
136+
</>
137+
);
138+
}

0 commit comments

Comments
 (0)