Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit cdf9443

Browse files
committedDec 22, 2020
Remote data provider PoC
1 parent 787c99d commit cdf9443

9 files changed

+9737
-0
lines changed
 

‎server/package-lock.json

+1,896
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎server/package.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "server",
3+
"version": "0.0.1",
4+
"description": "Test server for stock example",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"author": "",
10+
"license": "Apache-2.0",
11+
"dependencies": {
12+
"@types/crossfilter": "0.0.34",
13+
"@types/d3": "^6.2.0",
14+
"@types/express": "^4.17.9",
15+
"@types/node": "^14.14.12",
16+
"cors": "^2.8.5",
17+
"crossfilter2": "^1.5.4",
18+
"d3": "^6.3.1",
19+
"express": "^4.17.1",
20+
"nodemon": "^2.0.6",
21+
"ts-node": "^9.1.1",
22+
"typescript": "^4.1.3"
23+
}
24+
}

‎server/src/data-server.ts

+268
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import crossfilter from 'crossfilter2';
2+
import fs from 'fs';
3+
import * as d3 from 'd3';
4+
import { CFDataCapHelper, CFMultiAdapter, CFSimpleAdapter } from '../../src/data';
5+
import { FilterStorage } from '../../src/core/filter-storage';
6+
7+
interface DataElement {
8+
volume: number;
9+
open: number;
10+
close: number;
11+
month: Date;
12+
date: string;
13+
dd: Date;
14+
}
15+
16+
export function loadAndProcessData(dataFilePath) {
17+
const csvBuffer = fs.readFileSync(dataFilePath, 'utf8');
18+
// @ts-ignore
19+
const data: DataElement[] = d3.csvParse(csvBuffer);
20+
21+
// Since its a csv file we need to format the data a bit.
22+
const dateFormatSpecifier = '%m/%d/%Y';
23+
const dateFormatParser = d3.timeParse(dateFormatSpecifier);
24+
25+
data.forEach(d => {
26+
d.dd = dateFormatParser(d.date);
27+
d.month = d3.timeMonth(d.dd); // pre-calculate month for better performance
28+
d.close = +d.close; // coerce to number
29+
d.open = +d.open;
30+
});
31+
return data;
32+
}
33+
34+
export function creatAdapter(data: DataElement[]) {
35+
//### Create Crossfilter Dimensions and Groups
36+
37+
//See the [crossfilter API](https://github.com/square/crossfilter/wiki/API-Reference) for reference.
38+
const ndx = crossfilter(data);
39+
const all = ndx.groupAll();
40+
41+
// Dimension by year
42+
const yearlyDimension = ndx.dimension(d => d3.timeYear(d.dd).getFullYear());
43+
44+
interface YearlyPerformanceGroupItem {
45+
fluctuationPercentage: number;
46+
percentageGain: number;
47+
avgIndex: number;
48+
sumIndex: number;
49+
fluctuation: number;
50+
absGain: number;
51+
count: number;
52+
}
53+
// Maintain running tallies by year as filters are applied or removed
54+
const yearlyPerformanceGroup = yearlyDimension.group().reduce(
55+
/* callback for when data is added to the current filter results */
56+
(p: YearlyPerformanceGroupItem, v) => {
57+
++p.count;
58+
p.absGain += v.close - v.open;
59+
p.fluctuation += Math.abs(v.close - v.open);
60+
p.sumIndex += (v.open + v.close) / 2;
61+
p.avgIndex = p.sumIndex / p.count;
62+
p.percentageGain = p.avgIndex ? (p.absGain / p.avgIndex) * 100 : 0;
63+
p.fluctuationPercentage = p.avgIndex ? (p.fluctuation / p.avgIndex) * 100 : 0;
64+
return p;
65+
},
66+
/* callback for when data is removed from the current filter results */
67+
(p: YearlyPerformanceGroupItem, v) => {
68+
--p.count;
69+
p.absGain -= v.close - v.open;
70+
p.fluctuation -= Math.abs(v.close - v.open);
71+
p.sumIndex -= (v.open + v.close) / 2;
72+
p.avgIndex = p.count ? p.sumIndex / p.count : 0;
73+
p.percentageGain = p.avgIndex ? (p.absGain / p.avgIndex) * 100 : 0;
74+
p.fluctuationPercentage = p.avgIndex ? (p.fluctuation / p.avgIndex) * 100 : 0;
75+
return p;
76+
},
77+
/* initialize p */
78+
() => ({
79+
count: 0,
80+
absGain: 0,
81+
fluctuation: 0,
82+
fluctuationPercentage: 0,
83+
sumIndex: 0,
84+
avgIndex: 0,
85+
percentageGain: 0,
86+
})
87+
);
88+
89+
// Dimension by full date
90+
const dateDimension = ndx.dimension(d => d.dd);
91+
92+
// Dimension by month
93+
const moveMonths = ndx.dimension(d => d.month);
94+
// Group by total movement within month
95+
const monthlyMoveGroup = moveMonths.group().reduceSum(d => Math.abs(d.close - d.open));
96+
// Group by total volume within move, and scale down result
97+
const volumeByMonthGroup = moveMonths.group().reduceSum(d => d.volume / 500000);
98+
99+
interface IndexAvgByMonthGroupItem {
100+
avg: number;
101+
total: number;
102+
days: number;
103+
}
104+
const indexAvgByMonthGroup = moveMonths.group().reduce(
105+
(p: IndexAvgByMonthGroupItem, v) => {
106+
++p.days;
107+
p.total += (v.open + v.close) / 2;
108+
p.avg = Math.round(p.total / p.days);
109+
return p;
110+
},
111+
(p: IndexAvgByMonthGroupItem, v) => {
112+
--p.days;
113+
p.total -= (v.open + v.close) / 2;
114+
p.avg = p.days ? Math.round(p.total / p.days) : 0;
115+
return p;
116+
},
117+
() => ({ days: 0, total: 0, avg: 0 })
118+
);
119+
120+
// Create categorical dimension
121+
const gainOrLoss = ndx.dimension(d => (d.open > d.close ? 'Loss' : 'Gain'));
122+
// Produce counts records in the dimension
123+
const gainOrLossGroup = gainOrLoss.group();
124+
125+
// Determine a histogram of percent changes
126+
const fluctuation = ndx.dimension(d => Math.round(((d.close - d.open) / d.open) * 100));
127+
const fluctuationGroup = fluctuation.group();
128+
129+
// Summarize volume by quarter
130+
const quarter = ndx.dimension(d => {
131+
const month = d.dd.getMonth();
132+
if (month <= 2) {
133+
return 'Q1';
134+
} else if (month > 2 && month <= 5) {
135+
return 'Q2';
136+
} else if (month > 5 && month <= 8) {
137+
return 'Q3';
138+
} else {
139+
return 'Q4';
140+
}
141+
});
142+
const quarterGroup = quarter.group().reduceSum(d => d.volume);
143+
144+
// Counts per weekday
145+
const dayOfWeek = ndx.dimension(d => {
146+
const day = d.dd.getDay();
147+
const name = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
148+
return `${day}.${name[day]}`;
149+
});
150+
const dayOfWeekGroup = dayOfWeek.group();
151+
152+
//### Data providers
153+
const yearlyBubbleDataProvider = new CFSimpleAdapter({
154+
dimension: yearlyDimension,
155+
//The bubble chart expects the groups are reduced to multiple values which are used
156+
//to generate x, y, and radius for each key (bubble) in the group
157+
group: yearlyPerformanceGroup,
158+
// `.valueAccessor` - the `Y` value will be passed to the `.y()` scale to determine pixel location
159+
valueAccessor: p => p.value.percentageGain,
160+
});
161+
162+
const gainOrLossDataProvider = new CFDataCapHelper({
163+
dimension: gainOrLoss,
164+
group: gainOrLossGroup,
165+
});
166+
167+
const quarterDataProvider = new CFSimpleAdapter({
168+
dimension: quarter,
169+
group: quarterGroup,
170+
});
171+
172+
const dayOfWeekDataProvider = new CFSimpleAdapter({
173+
group: dayOfWeekGroup,
174+
dimension: dayOfWeek,
175+
});
176+
177+
const fluctuationDataProvider = new CFMultiAdapter({
178+
dimension: fluctuation,
179+
layers: [{ group: fluctuationGroup }],
180+
});
181+
182+
const moveDataProvider = new CFMultiAdapter({
183+
dimension: moveMonths,
184+
// Stack layers
185+
layers: [
186+
{
187+
name: 'Monthly Index Average',
188+
group: indexAvgByMonthGroup,
189+
valueAccessor: d => d.value.avg,
190+
},
191+
{
192+
name: 'Monthly Index Move',
193+
group: monthlyMoveGroup,
194+
valueAccessor: d => d.value,
195+
},
196+
],
197+
});
198+
199+
const volumeDataProvider = new CFMultiAdapter({
200+
dimension: moveMonths,
201+
layers: [
202+
{
203+
group: volumeByMonthGroup,
204+
},
205+
],
206+
});
207+
208+
const filterStorage = new FilterStorage();
209+
210+
const dataProviders = [
211+
{
212+
chartId: 'yearly-bubble-chart',
213+
dataProvider: yearlyBubbleDataProvider,
214+
},
215+
{
216+
chartId: 'gain-loss-chart',
217+
dataProvider: gainOrLossDataProvider,
218+
},
219+
{
220+
chartId: 'quarter-chart',
221+
dataProvider: quarterDataProvider,
222+
},
223+
{
224+
chartId: 'day-of-week-chart',
225+
dataProvider: dayOfWeekDataProvider,
226+
},
227+
{
228+
chartId: 'fluctuation-chart',
229+
dataProvider: fluctuationDataProvider,
230+
},
231+
{
232+
chartId: 'monthly-move-chart',
233+
dataProvider: moveDataProvider,
234+
},
235+
{
236+
chartId: 'monthly-volume-chart',
237+
dataProvider: volumeDataProvider,
238+
},
239+
];
240+
241+
dataProviders.forEach(e => {
242+
e.dataProvider.configure({
243+
chartId: e.chartId,
244+
filterStorage,
245+
});
246+
});
247+
248+
const adaptor = {
249+
cf: ndx,
250+
groupAll: all,
251+
dataProviders,
252+
filterStorage,
253+
computeChartData: () => {
254+
const chartData = adaptor.dataProviders.map(e => ({
255+
chartId: e.chartId,
256+
values: e.dataProvider.data(),
257+
}));
258+
259+
return {
260+
selectedRecords: adaptor.groupAll.value(),
261+
totalRecords: adaptor.cf.size(),
262+
chartData,
263+
};
264+
},
265+
};
266+
267+
return adaptor;
268+
}

0 commit comments

Comments
 (0)