Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/pages/Demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import React, { ChangeEvent } from 'react'
import { OutputState } from '../sonification/OutputConstants'
import { ImportView } from '../views/ImportView'
import { DataView } from '../views/DataView'
import { ChartView } from '../views/ChartView'

import { FormControl, InputLabel, Grid, NativeSelect } from '@mui/material'

import { DataManager } from '../DataManager'


import { IDemoView } from '../views/demos/IDemoView'
import { DemoSimple } from '../views/demos/DemoSimple'
import { DemoHighlightRegion } from '../views/demos/DemoHighlightRegion'
Expand All @@ -23,6 +25,7 @@ const DEMO_VIEW_MAP = {
}

let demoViewRef: React.RefObject<DemoSimple<DemoProps, DemoState> | DemoHighlightRegion> = React.createRef()
let chartViewRef: React.RefObject<ChartView> = React.createRef()
export interface DemoState {
dataSummary: any
columnList: string[]
Expand Down Expand Up @@ -63,6 +66,10 @@ export class Demo extends React.Component<DemoProps, DemoState> {
<DataView />
</div>

<div>
<ChartView ref={chartViewRef}/>
</div>

<div style={{ marginTop: '20px' }}>
<Grid container spacing={2}>
<Grid item xs={8} sm={4} md={4}>
Expand Down Expand Up @@ -189,6 +196,11 @@ export class Demo extends React.Component<DemoProps, DemoState> {
}
}
}
// Need to to create one handler for visual
const sinks = outputEngineInstance.getSinks();
sinks.forEach(sink => {
if (chartViewRef.current) chartViewRef.current.handleSinkUpdate(sink);
})
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/sonification/Datum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,20 @@ import * as d3 from 'd3'
export class Datum {
value: number
adjustedValue: number
id: number
sinkId: number
time: number

constructor(sinkId: number, value: number, time?: number) {
constructor(sinkId: number, value: number, id: number, time?: number) {
this.value = value
this.adjustedValue = value
this.id = id
this.sinkId = sinkId
if (time) this.time = time
else this.time = d3.now()
}

public toString(): string {
return `(raw: ${this.value}; adjusted: ${this.adjustedValue}, ${this.time})`
return `(raw: ${this.value}; adjusted: ${this.adjustedValue}; id: ${this.id}, ${this.time})`
}
}
10 changes: 9 additions & 1 deletion src/sonification/OutputEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ export class OutputEngine {
return true
}

/**
*
* @returns Array of active DataSinks
*/
public getSinks(): DataSink[] {
return [...this.sinks.values()]
}

/**
* Get a sink given an Id. Throws an error of sinkId doesn't exist.
* @param sinkId
Expand Down Expand Up @@ -205,7 +213,7 @@ export class OutputEngine {
let sink = this.sinks.get(sinkId)
if (!sink) throw new Error(`no sink associated with ${sinkId}`)
if (DEBUG) console.log(`Sink ${sink}`)
let datum = new Datum(sinkId, point)
let datum = new Datum(sinkId, point, point)
switch (this.outputState) {
case OutputState.Stopped: // ignore the point
if (DEBUG) console.log(`playback: Stopped`)
Expand Down
224 changes: 224 additions & 0 deletions src/views/ChartView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import React from 'react'

import { DataManager } from '../DataManager'
import * as d3 from 'd3'
import { DataSink } from '../sonification/DataSink'
import { Datum } from '../sonification/Datum'
import { DataHandler } from '../sonification/handler/DataHandler'
import { DatumOutput } from '../sonification/output/DatumOutput'
import { ThreeDRotationSharp } from '@mui/icons-material'
// import { Sonifier } from '../sonification/Sonifier'

export interface ChartViewState {
rows: any[]
valuesX: number[]
domainX: number[]
domainY: number[]
margin: any
width: number
height: number
fieldY: string[]
fieldX: string
highlight: any[]
}

export interface ChartViewProps {}

export class ChartView extends React.Component<ChartViewProps, ChartViewState> {
constructor(props: ChartViewProps) {
super(props)
this.state = {
rows: [],
valuesX: [],
domainX: [-10, 10],
domainY: [-10, 10],
margin: { l: 20, r: 20, b: 20, t: 20 },
width: 1340,
height: 500,
fieldX: 'Time',
fieldY: ['Value'],
highlight: [],
}

// Sonifier.getSonifierInstance().addPlaybackListener(this.handleDatumUpdate)

DataManager.getInstance().addListener(this.handleDataUpdate)
}

// Private parameters
private scaleX = d3.scaleLinear()
private scaleY = d3.scaleLinear()

private dataHandler: ChartDataHandler | undefined

private chartContainer: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
private pointHighlight: React.RefObject<SVGGElement> = React.createRef<SVGGElement>()

public render() {
const { rows, margin, fieldX, fieldY } = this.state

const ticksMajorY = this.scaleY.ticks().map((t, i) => {
const y0 = this.scaleY(t)
return (
<g key={i}>
<path
d={`M0,${y0}h${this.scaleX.range()[1]}`}
style={{ stroke: '#999', shapeRendering: 'crispEdges' }}
/>
<text x={this.scaleX(0)} y={y0} dy="0.3em" style={{ textAnchor: 'middle' }}>
{t}
</text>
</g>
)
})

const ticksMajorX = this.scaleX.ticks().map((t, i) => {
const x0 = this.scaleX(t)
return (
<g key={i}>
<path
d={`M${x0},0v${this.scaleY.range()[0]}`}
style={{ stroke: '#999', shapeRendering: 'crispEdges' }}
/>
<text x={x0} y={this.scaleY(0)} dy="0.7em" style={{ textAnchor: 'middle' }}>
{t}
</text>
</g>
)
})

const paths = fieldY.map((f, i) => {
const line = d3
.line()
.x((d) => this.scaleX(d[fieldX]))
.y((d) => this.scaleY(d[f]))
return <path d={line(rows)} style={{ stroke: '#2d70b3', strokeWidth: 2, fill: 'none' }} />
})

return (
<div ref={this.chartContainer} style={{ height: 500, width: '100%' }}>
<svg style={{ width: '100%', height: '100%' }} onMouseMove={this.handleMouseMove}>
<g id="chart" transform={`translate(${margin.l},${margin.t})`}>
<g className="axis-group">
<g className="axis axis-x">{ticksMajorX}</g>
<g className="axis axis-y">{ticksMajorY}</g>
</g>
<g className="mark-group">{paths}</g>
<g className="annotation-group">
<g className="highlight-point" ref={this.pointHighlight}>
<circle r="4" style={{ fill: '#2d70b3', strokeWidth: 0.5, stroke: '#fff' }} />
</g>
</g>
</g>
</svg>
</div>
)
}

private handleMouseMove = (e: React.MouseEvent<SVGSVGElement>): void => {
const { rows, valuesX, fieldX, fieldY } = this.state

if (valuesX.length > 0) {
const mx = d3.pointer(e)[0],
i = d3.bisectCenter(valuesX, this.scaleX.invert(mx)),
tx = this.scaleX(valuesX[i]),
ty = this.scaleY(rows[i][fieldY[0]])

if (this.pointHighlight.current) {
d3.select(this.pointHighlight.current).attr('transform', `translate(${tx},${ty})`)
}
}
}

public componentDidMount() {
this.handleResize()
window.addEventListener('resize', this.handleResize)
}

public handleResize = (): void => {
if (this.chartContainer.current) {
const dimensions = this.chartContainer.current.getBoundingClientRect(),
{ width, height } = dimensions

this.scaleX.range([0, width - this.state.margin.l - this.state.margin.r])
this.scaleY.range([height - this.state.margin.t - this.state.margin.b, 0])

this.setState({ width, height })
}
}

public handleSinkUpdate = (sink: DataSink): void => {
if (!this.dataHandler) {
this.dataHandler = new ChartDataHandler(sink, undefined, this.handleDatumUpdate)
sink.addDataHandler(this.dataHandler)
}
}

public handleDataUpdate = (table: any): void => {
const { fieldX } = this.state

const rows = table.objects().map((o, i) => Object.assign({ id: i }, o)),
valuesX = rows.map((d) => d[fieldX]),
domainX = d3.extent(rows, (d) => d[fieldX]),
domainY = d3.extent(rows, (d) => d['Value'])

this.scaleX.domain(domainX)
this.scaleY.domain(domainY)

this.setState({ rows, valuesX, domainX, domainY })
}

/**
* Stores relevant information. Value is derived from point.scaledValue.
* @param datum The raw datum
*/
public handleDatumUpdate = (datum?: Datum): void => {
if (datum) {
const { rows, valuesX, fieldX, fieldY } = this.state

const i = datum.id

const tx = this.scaleX(valuesX[i]),
ty = this.scaleY(rows[i][fieldY[0]])

if (this.pointHighlight.current) {
d3.select(this.pointHighlight.current).attr('transform', `translate(${tx},${ty})`)
}
}
}
}

// Does it make sense to create a subclass for highlighting a point? That way there could be multiple?
class ChartDataHandler extends DataHandler {
private handleDatumCallback?: (datum?: Datum) => void

/**
*
* @param sink. DataSink that is providing data to this Handler.
* @param output An optional way to output the data
*/
constructor(sink?: DataSink, output?: DatumOutput, handleDatumCallback?: (datum?: Datum) => void) {
super(sink, output)
this.handleDatumCallback = handleDatumCallback
}

/**
* Adjusts the value for datum by scaling it to the range [min, max]
* Alternatively, if datum is empty, no need to adjust since the stream is empty
* at this point in time.
*
* @param datum
* @returns Always returns true
*/
handleDatum(datum?: Datum): boolean {
if (!datum) return true

if (this.handleDatumCallback) this.handleDatumCallback(datum)

return super.handleDatum(datum)
}

public toString(): string {
return `ChartDataHandler`
}
}
6 changes: 6 additions & 0 deletions src/views/ImportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@ export class ImportView extends React.Component<ImportViewProps, ImportViewState
}
}

public componentDidMount() {
// Update data based on first example
let url = `./data/${EXAMPLE_LIST[0].fileName}`
DataManager.getInstance().loadDataFromUrl(url)
}

// https://raw.githubusercontent.com/vega/vega-datasets/next/data/stocks.csv

private _handleFileChange = (event: React.FormEvent<HTMLElement>) => {
Expand Down
2 changes: 1 addition & 1 deletion src/views/demos/DemoHighlightRegion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class DemoHighlightRegion
maxValue: this.props.dataSummary.max,
}
}

public render() {
const { minValue, maxValue } = this.state

Expand Down
2 changes: 1 addition & 1 deletion src/views/demos/DemoSimple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class DemoSimple<DemoSimpleProps, DemoSimpleState>

let id = this.sink ? this.sink.id : 0
let source = timer(0, 200).pipe(
map((val) => new Datum(id, data[val])),
map((val) => new Datum(id, data[val], val)),
take(data.length),
)
console.log('setStream in demo')
Expand Down
2 changes: 1 addition & 1 deletion src/views/demos/ExperimentalDemoHighlightRegion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export class ExperimentalDemoHighlightRegion<ExperimentalDemoHighlightRegionProp

let id = this.sink ? this.sink.id : 0
let sink = timer(0, 200).pipe(
map((val) => new Datum(id, data[val])),
map((val) => new Datum(id, data[val], val)),
take(data.length),
)
console.log('setStream in demo')
Expand Down