// issue - global URL const GDAX_URL = "https://api.pro.coinbase.com/", SOCKET_URL = "wss://ws-feed.pro.coinbase.com"; const insertIf = (condition, ...elements) => (condition ? elements : []); const scatterConfig = { // received orders are the only ones with size filter: [["type", "==", "received"], ["side", "==", "buy"]], row_pivot: ["product_id"], column_pivot: [], aggregate: [ { op: "avg", column: "price" }, { op: "avg", column: "size" }, { op: "count", column: "order_id" } ], sort: [] }; (async () => { let initialised = false, table, scatterView, suppress = false; const res = await fetch(`${GDAX_URL}products`); const products = await res.json(); const color = d3 .scaleOrdinal(d3.schemeCategory10) .domain(products.map(d => d.base_currency)); let buffer = []; setInterval(async () => { // to do - raise buffer.length bug if (initialised && buffer.length > 0 && !suppress) { try { table.update(buffer); render(); } catch (e) { console.error(e); } buffer = []; } }, 50); const render = async () => { const scatterCols = await scatterView.to_columns(); const series = scatterCols.__ROW_PATH__ .map((p, i) => ({ price: scatterCols.price[i], size: scatterCols.size[i], count: scatterCols.order_id[i], instrument: p[0] })) .slice(1); const labelPadding = 2; const textLabel = fc .layoutTextLabel() .padding(labelPadding) .value(d => d.instrument); const labels = fc .layoutLabel(fc.layoutGreedy()) .key(d => d.instrument) .size((d, i, g) => { const textSize = g[i].getElementsByTagName("text")[0].getBBox(); return [ textSize.width + labelPadding * 2, textSize.height + labelPadding * 2 ]; }) .position(d => [d.size, d.price]) .component(textLabel); const size = d3 .scaleLinear() .range([20, 3600]) .domain(fc.extentLinear().accessors([d => d.count])(series)); const pointSeries = fc .seriesSvgPoint() .key(d => d.instrument) .crossValue(d => d.size) .mainValue(d => d.price) .size(d => size(d.count)) .decorate(sel => { sel.attr("fill", d => color(d.instrument.split("-")[0])); }); const multi = fc .seriesSvgMulti() .series([pointSeries, ...insertIf(suppress, labels)]); const chart = fc .chartCartesian(d3.scaleLog(), d3.scaleLog()) .xDomain([0.01, 10000]) .yDomain([0.001, 10000]) .xTickFormat(d3.format(",")) .yTickFormat(d3.format(",")) .xLabel("size") .yLabel("price") .xTickValues([0.1, 1, 10, 100, 1000]) .yTickValues([0.001, 0.1, 1, 10, 100, 1000]) .svgPlotArea(multi); d3.select("#chart") .datum(series) .transition() .duration(200) .call(chart); d3.select("#chart") .on("mouseover", () => { suppress = true; render(); }) .on("mouseout", () => { suppress = false; render(); }); }; const ws = new WebSocket(SOCKET_URL); ws.onopen = () => { ws.send( JSON.stringify({ type: "subscribe", product_ids: products.map(p => p.id) }) ); }; ws.onmessage = msg => { if (document.hidden) { return; } const data = JSON.parse(msg.data); buffer.push(data); if (!initialised) { if (buffer.length > 200) { table = perspective.worker().table(buffer, { limit: 5000 }); initialised = true; scatterView = table.view(scatterConfig); render(); } } }; ws.onerror = console.log; ws.onclose = e => console.log(e.code, e.reason); })();