record lux_fast and lux _slow
This commit is contained in:
@@ -9,6 +9,9 @@ import android.hardware.SensorEventListener;
|
|||||||
import android.hardware.SensorManager;
|
import android.hardware.SensorManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
@@ -18,6 +21,7 @@ import androidx.annotation.Nullable;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import io.flutter.Log;
|
||||||
import io.flutter.embedding.android.FlutterActivity;
|
import io.flutter.embedding.android.FlutterActivity;
|
||||||
import io.flutter.embedding.engine.FlutterEngine;
|
import io.flutter.embedding.engine.FlutterEngine;
|
||||||
import io.flutter.plugin.common.EventChannel;
|
import io.flutter.plugin.common.EventChannel;
|
||||||
@@ -28,11 +32,23 @@ public class MainActivity extends FlutterActivity implements EventChannel.Stream
|
|||||||
private Sensor mLightSensor;
|
private Sensor mLightSensor;
|
||||||
private SensorManager mSensorManager;
|
private SensorManager mSensorManager;
|
||||||
private EventChannel.EventSink mEventSink;
|
private EventChannel.EventSink mEventSink;
|
||||||
|
private AmbientLightRingBuffer mAmbientLightRingBuffer;
|
||||||
|
private long mAmbientLightHorizonLong = 4000;
|
||||||
|
private int mRecentLightSamples = 0;
|
||||||
|
private float mLastObservedLux;
|
||||||
|
private long mLastObservedLuxTime;
|
||||||
|
private Handler mHandler;
|
||||||
|
private long mAmbientLightHorizonShort = 2000;
|
||||||
|
private float mSlowAmbientLux;
|
||||||
|
private float mFastAmbientLux;
|
||||||
|
private long AMBIENT_LIGHT_PREDICTION_TIME_MILLIS = 100;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
|
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
|
||||||
mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
|
mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
|
||||||
|
mHandler = new AutomaticBrightnessHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -49,6 +65,7 @@ public class MainActivity extends FlutterActivity implements EventChannel.Stream
|
|||||||
super.onResume();
|
super.onResume();
|
||||||
mSensorManager.registerListener(this, mLightSensor, SensorManager.SENSOR_DELAY_FASTEST);
|
mSensorManager.registerListener(this, mLightSensor, SensorManager.SENSOR_DELAY_FASTEST);
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
mAmbientLightRingBuffer = new AmbientLightRingBuffer(250, 4000, new RealClock(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -69,12 +86,14 @@ public class MainActivity extends FlutterActivity implements EventChannel.Stream
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSensorChanged(SensorEvent sensorEvent) {
|
public void onSensorChanged(SensorEvent sensorEvent) {
|
||||||
if (mEventSink != null) {
|
final long time = SystemClock.uptimeMillis();
|
||||||
final Map data = new HashMap();
|
final float lux = sensorEvent.values[0];
|
||||||
data.put("lux", sensorEvent.values[0]);
|
Message msg = mHandler.obtainMessage();
|
||||||
data.put("timestamp", sensorEvent.timestamp);
|
Bundle data = new Bundle();
|
||||||
mEventSink.success(data);
|
data.putFloat("lux", lux);
|
||||||
}
|
data.putLong("time", time);
|
||||||
|
msg.setData(data);
|
||||||
|
msg.sendToTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -99,4 +118,297 @@ public class MainActivity extends FlutterActivity implements EventChannel.Stream
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* A ring buffer of ambient light measurements sorted by time.
|
||||||
|
*
|
||||||
|
* Each entry consists of a timestamp and a lux measurement, and the overall buffer is sorted
|
||||||
|
* from oldest to newest.
|
||||||
|
*/
|
||||||
|
private static final class AmbientLightRingBuffer {
|
||||||
|
// Proportional extra capacity of the buffer beyond the expected number of light samples
|
||||||
|
// in the horizon
|
||||||
|
private static final float BUFFER_SLACK = 1.5f;
|
||||||
|
private float[] mRingLux;
|
||||||
|
private long[] mRingTime;
|
||||||
|
private int mCapacity;
|
||||||
|
|
||||||
|
// The first valid element and the next open slot.
|
||||||
|
// Note that if mCount is zero then there are no valid elements.
|
||||||
|
private int mStart;
|
||||||
|
private int mEnd;
|
||||||
|
private int mCount;
|
||||||
|
Clock mClock;
|
||||||
|
|
||||||
|
public AmbientLightRingBuffer(long lightSensorRate, int ambientLightHorizon, Clock clock) {
|
||||||
|
if (lightSensorRate <= 0) {
|
||||||
|
throw new IllegalArgumentException("lightSensorRate must be above 0");
|
||||||
|
}
|
||||||
|
mCapacity = (int) Math.ceil(ambientLightHorizon * BUFFER_SLACK / lightSensorRate);
|
||||||
|
mRingLux = new float[mCapacity];
|
||||||
|
mRingTime = new long[mCapacity];
|
||||||
|
mClock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getLux(int index) {
|
||||||
|
return mRingLux[offsetOf(index)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public float[] getAllLuxValues() {
|
||||||
|
float[] values = new float[mCount];
|
||||||
|
if (mCount == 0) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mStart < mEnd) {
|
||||||
|
System.arraycopy(mRingLux, mStart, values, 0, mCount);
|
||||||
|
} else {
|
||||||
|
System.arraycopy(mRingLux, mStart, values, 0, mCapacity - mStart);
|
||||||
|
System.arraycopy(mRingLux, 0, values, mCapacity - mStart, mEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTime(int index) {
|
||||||
|
return mRingTime[offsetOf(index)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public long[] getAllTimestamps() {
|
||||||
|
long[] values = new long[mCount];
|
||||||
|
if (mCount == 0) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mStart < mEnd) {
|
||||||
|
System.arraycopy(mRingTime, mStart, values, 0, mCount);
|
||||||
|
} else {
|
||||||
|
System.arraycopy(mRingTime, mStart, values, 0, mCapacity - mStart);
|
||||||
|
System.arraycopy(mRingTime, 0, values, mCapacity - mStart, mEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void push(long time, float lux) {
|
||||||
|
int next = mEnd;
|
||||||
|
if (mCount == mCapacity) {
|
||||||
|
int newSize = mCapacity * 2;
|
||||||
|
|
||||||
|
float[] newRingLux = new float[newSize];
|
||||||
|
long[] newRingTime = new long[newSize];
|
||||||
|
int length = mCapacity - mStart;
|
||||||
|
System.arraycopy(mRingLux, mStart, newRingLux, 0, length);
|
||||||
|
System.arraycopy(mRingTime, mStart, newRingTime, 0, length);
|
||||||
|
if (mStart != 0) {
|
||||||
|
System.arraycopy(mRingLux, 0, newRingLux, length, mStart);
|
||||||
|
System.arraycopy(mRingTime, 0, newRingTime, length, mStart);
|
||||||
|
}
|
||||||
|
mRingLux = newRingLux;
|
||||||
|
mRingTime = newRingTime;
|
||||||
|
|
||||||
|
next = mCapacity;
|
||||||
|
mCapacity = newSize;
|
||||||
|
mStart = 0;
|
||||||
|
}
|
||||||
|
mRingTime[next] = time;
|
||||||
|
mRingLux[next] = lux;
|
||||||
|
mEnd = next + 1;
|
||||||
|
if (mEnd == mCapacity) {
|
||||||
|
mEnd = 0;
|
||||||
|
}
|
||||||
|
mCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void prune(long horizon) {
|
||||||
|
if (mCount == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (mCount > 1) {
|
||||||
|
int next = mStart + 1;
|
||||||
|
if (next >= mCapacity) {
|
||||||
|
next -= mCapacity;
|
||||||
|
}
|
||||||
|
if (mRingTime[next] > horizon) {
|
||||||
|
// Some light sensors only produce data upon a change in the ambient light
|
||||||
|
// levels, so we need to consider the previous measurement as the ambient light
|
||||||
|
// level for all points in time up until we receive a new measurement. Thus, we
|
||||||
|
// always want to keep the youngest element that would be removed from the
|
||||||
|
// buffer and just set its measurement time to the horizon time since at that
|
||||||
|
// point it is the ambient light level, and to remove it would be to drop a
|
||||||
|
// valid data point within our horizon.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mStart = next;
|
||||||
|
mCount -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mRingTime[mStart] < horizon) {
|
||||||
|
mRingTime[mStart] = horizon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return mCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
mStart = 0;
|
||||||
|
mEnd = 0;
|
||||||
|
mCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
buf.append('[');
|
||||||
|
for (int i = 0; i < mCount; i++) {
|
||||||
|
final long next = i + 1 < mCount ? getTime(i + 1)
|
||||||
|
: mClock.getSensorEventScaleTime();
|
||||||
|
if (i != 0) {
|
||||||
|
buf.append(", ");
|
||||||
|
}
|
||||||
|
buf.append(getLux(i));
|
||||||
|
buf.append(" / ");
|
||||||
|
buf.append(next - getTime(i));
|
||||||
|
buf.append("ms");
|
||||||
|
}
|
||||||
|
buf.append(']');
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int offsetOf(int index) {
|
||||||
|
if (index >= mCount || index < 0) {
|
||||||
|
throw new ArrayIndexOutOfBoundsException(index);
|
||||||
|
}
|
||||||
|
index += mStart;
|
||||||
|
if (index >= mCapacity) {
|
||||||
|
index -= mCapacity;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static class RealClock implements Clock {
|
||||||
|
private final boolean mOffloadControlsDozeBrightness;
|
||||||
|
|
||||||
|
RealClock(boolean offloadControlsDozeBrightness) {
|
||||||
|
mOffloadControlsDozeBrightness = offloadControlsDozeBrightness;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long uptimeMillis() {
|
||||||
|
return SystemClock.uptimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSensorEventScaleTime() {
|
||||||
|
return (mOffloadControlsDozeBrightness)
|
||||||
|
? SystemClock.elapsedRealtime() : uptimeMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface Clock {
|
||||||
|
/**
|
||||||
|
* Returns current time in milliseconds since boot, not counting time spent in deep sleep.
|
||||||
|
*/
|
||||||
|
long uptimeMillis();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the time on either the elapsedTime or the uptime scale, depending on how we
|
||||||
|
* processing the events from the sensor
|
||||||
|
*/
|
||||||
|
long getSensorEventScaleTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AutomaticBrightnessHandler extends Handler {
|
||||||
|
@Override
|
||||||
|
public void handleMessage(@NonNull Message msg) {
|
||||||
|
switch (msg.what) {
|
||||||
|
default:
|
||||||
|
final long time = msg.getData().getLong("time");
|
||||||
|
final float lux = msg.getData().getFloat("lux");
|
||||||
|
handleLightSensorEvent(time, lux);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleLightSensorEvent(long time, float lux) {
|
||||||
|
mRecentLightSamples++;
|
||||||
|
mAmbientLightRingBuffer.prune(time - mAmbientLightHorizonLong);
|
||||||
|
mAmbientLightRingBuffer.push(time, lux);
|
||||||
|
// Remember this sample value.
|
||||||
|
mLastObservedLux = lux;
|
||||||
|
mLastObservedLuxTime = time;
|
||||||
|
//updateAmbientLux(time);
|
||||||
|
mFastAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonShort);
|
||||||
|
mSlowAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonLong);
|
||||||
|
// Log.i("moon", "mFastAmbientLux: " + mFastAmbientLux);
|
||||||
|
// Log.i("moon", "mSlowAmbientLux: " + mSlowAmbientLux);
|
||||||
|
if (mEventSink != null) {
|
||||||
|
final Map data = new HashMap();
|
||||||
|
data.put("lux", lux);
|
||||||
|
data.put("lux_fast", mFastAmbientLux);
|
||||||
|
data.put("lux_slow", mSlowAmbientLux);
|
||||||
|
data.put("timestamp", time);
|
||||||
|
mEventSink.success(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float calculateAmbientLux(long now, long horizon) {
|
||||||
|
final int N = mAmbientLightRingBuffer.size();
|
||||||
|
if (N == 0) {
|
||||||
|
// Slog.e(TAG, "calculateAmbientLux: No ambient light readings available");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the first measurement that is just outside of the horizon.
|
||||||
|
int endIndex = 0;
|
||||||
|
final long horizonStartTime = now - horizon;
|
||||||
|
for (int i = 0; i < N-1; i++) {
|
||||||
|
if (mAmbientLightRingBuffer.getTime(i + 1) <= horizonStartTime) {
|
||||||
|
endIndex++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if (mLoggingEnabled) {
|
||||||
|
// Slog.d(TAG, "calculateAmbientLux: selected endIndex=" + endIndex + ", point=(" +
|
||||||
|
// mAmbientLightRingBuffer.getTime(endIndex) + ", " +
|
||||||
|
// mAmbientLightRingBuffer.getLux(endIndex) + ")");
|
||||||
|
// }
|
||||||
|
float sum = 0;
|
||||||
|
float totalWeight = 0;
|
||||||
|
long endTime = AMBIENT_LIGHT_PREDICTION_TIME_MILLIS;
|
||||||
|
for (int i = N - 1; i >= endIndex; i--) {
|
||||||
|
long eventTime = mAmbientLightRingBuffer.getTime(i);
|
||||||
|
if (i == endIndex && eventTime < horizonStartTime) {
|
||||||
|
// If we're at the final value, make sure we only consider the part of the sample
|
||||||
|
// within our desired horizon.
|
||||||
|
eventTime = horizonStartTime;
|
||||||
|
}
|
||||||
|
final long startTime = eventTime - now;
|
||||||
|
float weight = calculateWeight(startTime, endTime);
|
||||||
|
float lux = mAmbientLightRingBuffer.getLux(i);
|
||||||
|
// if (mLoggingEnabled) {
|
||||||
|
// Log.i("moon", "calculateAmbientLux: [" + startTime + ", " + endTime + "]: " +
|
||||||
|
// "lux=" + lux + ", " +
|
||||||
|
// "weight=" + weight);
|
||||||
|
// }
|
||||||
|
totalWeight += weight;
|
||||||
|
sum += lux * weight;
|
||||||
|
endTime = startTime;
|
||||||
|
}
|
||||||
|
// if (mLoggingEnabled) {
|
||||||
|
// Log.i("moon", "calculateAmbientLux: " +
|
||||||
|
// "totalWeight=" + totalWeight + ", " +
|
||||||
|
// "newAmbientLux=" + (sum / totalWeight));
|
||||||
|
// }
|
||||||
|
return sum / totalWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float calculateWeight(long startDelta, long endDelta) {
|
||||||
|
return weightIntegral(endDelta) - weightIntegral(startDelta);
|
||||||
|
}
|
||||||
|
private float weightIntegral(long x) {
|
||||||
|
return x * (x * 0.5f + 4000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import 'dart:ffi';
|
|||||||
class LightSensorEvent {
|
class LightSensorEvent {
|
||||||
final double lux;
|
final double lux;
|
||||||
final int timestamp;
|
final int timestamp;
|
||||||
LightSensorEvent(this.lux, this.timestamp);
|
final double lux_fast;
|
||||||
|
final double lux_slow;
|
||||||
|
LightSensorEvent(this.lux, this.timestamp, this.lux_fast, this.lux_slow);
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'lux': lux,
|
|
||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
|
'lux': lux,
|
||||||
|
'lux_fast': lux_fast,
|
||||||
|
'lux_slow': lux_slow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,8 +174,8 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
|
|
||||||
Stream<LightSensorEvent> streamSensorEventFromNative() {
|
Stream<LightSensorEvent> streamSensorEventFromNative() {
|
||||||
const eventChannel = EventChannel('tw.moonjuice.light_sensor.stream');
|
const eventChannel = EventChannel('tw.moonjuice.light_sensor.stream');
|
||||||
return eventChannel
|
return eventChannel.receiveBroadcastStream().map((event) =>
|
||||||
.receiveBroadcastStream()
|
LightSensorEvent(event['lux'], event['timestamp'], event['lux_fast'],
|
||||||
.map((event) => LightSensorEvent(event['lux'], event['timestamp']));
|
event['lux_slow']));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
test/Sensor_datas/2024-12-11_16-54-17.txt
Normal file
1
test/Sensor_datas/2024-12-11_16-54-17.txt
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 MiB |
@@ -5,21 +5,15 @@
|
|||||||
<title>D3.js Multiple Line Chart</title>
|
<title>D3.js Multiple Line Chart</title>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
|
.line {
|
||||||
.line { fill: none; stroke-width: 2px; }
|
fill: none;
|
||||||
.axis { font-size: 12px; }
|
stroke-width: 2px;
|
||||||
.legend { font-size: 12px; }
|
}
|
||||||
.tooltip {
|
.axis-label {
|
||||||
position: absolute;
|
font-size: 16px;
|
||||||
text-align: center;
|
}
|
||||||
width: 120px;
|
.legend {
|
||||||
height: 40px;
|
font-size: 16px;
|
||||||
padding: 2px;
|
|
||||||
font: 12px sans-serif;
|
|
||||||
background: lightsteelblue;
|
|
||||||
border: 0px;
|
|
||||||
border-radius: 8px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -28,207 +22,140 @@
|
|||||||
<div id="chart"></div>
|
<div id="chart"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Sample multiple dataset
|
// Data
|
||||||
const data = [
|
const sample_data = [
|
||||||
{"lux":10.0,"timestamp":442470841810000},
|
{"lux":1263.0,"timestamp":18188401,"lux_fast":1000.0,"lux_slow":900.0},
|
||||||
{"lux":20.0,"timestamp":442471010509000},
|
{"lux":1000.0,"timestamp":18188571,"lux_fast":1263.0,"lux_slow":1000.0},
|
||||||
{"lux":30.0,"timestamp":442471182576000},
|
{"lux":900.0,"timestamp":18188741,"lux_fast":1262.0,"lux_slow":1263.0}
|
||||||
{"lux":40.0,"timestamp":442471360117000},
|
|
||||||
{"lux":100.0,"timestamp":442471522264000}
|
|
||||||
];
|
];
|
||||||
const colorMap = new Map();
|
|
||||||
colorMap.set('original', 'steelblue');
|
|
||||||
colorMap.set('fastLuxData', 'green');
|
|
||||||
colorMap.set('slowLuxData', 'red');
|
|
||||||
|
|
||||||
function weightIntegral(x) {
|
function createChart(data) {
|
||||||
return x * (x * 0.5 + 4000000000);
|
d3.select("#chart").select("svg").remove();
|
||||||
}
|
// Set the dimensions and margins of the graph
|
||||||
|
const margin = {top: 20, right: 80, bottom: 50, left: 60};
|
||||||
function calculateLux(datas, now) {
|
const width = 600 - margin.left - margin.right;
|
||||||
var sum = 0, totalWeight = 0, endDelta = 100000000;
|
|
||||||
for (var i = datas.length - 1; i >= 0; i--) {
|
|
||||||
var eventTime = datas[i].timestamp;
|
|
||||||
var startDelta = eventTime - now;
|
|
||||||
var weight = weightIntegral(endDelta) - weightIntegral(startDelta);
|
|
||||||
var lux = datas[i].lux;
|
|
||||||
totalWeight += weight;
|
|
||||||
sum += lux * weight;
|
|
||||||
endDelta = startDelta;
|
|
||||||
}
|
|
||||||
return sum / totalWeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render multiple line chart
|
|
||||||
function renderMultiLineChart(data) {
|
|
||||||
var timestampNow;
|
|
||||||
var fastLuxData = [];
|
|
||||||
var slowLuxData = [];
|
|
||||||
var datasets = [];
|
|
||||||
for (timestampNow=data[0].timestamp; timestampNow<=data[data.length-1].timestamp; timestampNow+=200000000) {
|
|
||||||
var fastRingBuffer = data.filter((value) => value.timestamp >= timestampNow-2000000000 && value.timestamp <= timestampNow);
|
|
||||||
var slowRingBuffer = data.filter((value) => value.timestamp >= timestampNow-4000000000 && value.timestamp <= timestampNow);
|
|
||||||
if (fastRingBuffer.length <= 1) {
|
|
||||||
fastLuxData.push(data[0]);
|
|
||||||
} else {
|
|
||||||
fastLuxData.push({timestamp:timestampNow, lux:calculateLux(fastRingBuffer, timestampNow)});
|
|
||||||
}
|
|
||||||
if (slowRingBuffer.length <= 1) {
|
|
||||||
slowLuxData.push(data[0]);
|
|
||||||
} else {
|
|
||||||
slowLuxData.push({timestamp:timestampNow, lux:calculateLux(slowRingBuffer, timestampNow)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var dataObj = new Object();
|
|
||||||
dataObj.name = "original";
|
|
||||||
dataObj.values = data;
|
|
||||||
datasets.push(dataObj);
|
|
||||||
var fastLuxDataObj = new Object();
|
|
||||||
fastLuxDataObj.name = "fastLuxData";
|
|
||||||
fastLuxDataObj.values = fastLuxData;
|
|
||||||
datasets.push(fastLuxDataObj);
|
|
||||||
var slowLuxDataObj = new Object();
|
|
||||||
slowLuxDataObj.name = "slowLuxData";
|
|
||||||
slowLuxDataObj.values = slowLuxData;
|
|
||||||
datasets.push(slowLuxDataObj);
|
|
||||||
console.log(datasets.length);
|
|
||||||
console.log(datasets[0]);
|
|
||||||
|
|
||||||
// Clear previous chart
|
|
||||||
d3.select("#chart").html("");
|
|
||||||
|
|
||||||
// Chart dimensions
|
|
||||||
const margin = {top: 50, right: 50, bottom: 50, left: 50};
|
|
||||||
const width = 800 - margin.left - margin.right;
|
|
||||||
const height = 400 - margin.top - margin.bottom;
|
const height = 400 - margin.top - margin.bottom;
|
||||||
|
|
||||||
// Tooltip
|
// Append the svg object to the body of the page
|
||||||
const tooltip = d3.select("body")
|
|
||||||
.append("div")
|
|
||||||
.attr("class", "tooltip")
|
|
||||||
.style("opacity", 0);
|
|
||||||
|
|
||||||
// Create SVG
|
|
||||||
const svg = d3.select("#chart")
|
const svg = d3.select("#chart")
|
||||||
.append("svg")
|
.append("svg")
|
||||||
.attr("width", width + margin.left + margin.right)
|
.attr("width", width + margin.left + margin.right)
|
||||||
.attr("height", height + margin.top + margin.bottom)
|
.attr("height", height + margin.top + margin.bottom)
|
||||||
.append("g")
|
.append("g")
|
||||||
.attr("transform", `translate(${margin.left},${margin.top})`);
|
.attr("transform", `translate(${margin.left},${margin.top})`);
|
||||||
|
|
||||||
// Parse dates
|
// X scale
|
||||||
const parseTime = d3.timeParse("%Y-%m-%d");
|
|
||||||
const formatTime = d3.timeFormat("%Y-%m-%d");
|
|
||||||
/*datasets.forEach(series => {
|
|
||||||
series.values.forEach(d => {
|
|
||||||
d.timestamp = parseTime(d.timestamp);
|
|
||||||
});
|
|
||||||
});*/
|
|
||||||
|
|
||||||
// Combine all values for scale
|
|
||||||
const allValues = datasets.flatMap(series => series.values.map(d => d.lux));
|
|
||||||
|
|
||||||
// Scales
|
|
||||||
const x = d3.scaleLinear()
|
const x = d3.scaleLinear()
|
||||||
.domain(d3.extent(datasets[0].values, d => d.timestamp))
|
.domain(d3.extent(data, d => d.timestamp))
|
||||||
.range([0, width]);
|
.range([0, width]);
|
||||||
|
|
||||||
|
// Y scale
|
||||||
const y = d3.scaleLinear()
|
const y = d3.scaleLinear()
|
||||||
.domain([
|
.domain([d3.min(data, d => Math.min(d.lux, d.lux_fast, d.lux_slow)) - 0.5,
|
||||||
d3.min(allValues) * 0.9,
|
d3.max(data, d => Math.max(d.lux, d.lux_fast, d.lux_slow)) + 0.5])
|
||||||
d3.max(allValues) * 1.1
|
|
||||||
])
|
|
||||||
.range([height, 0]);
|
.range([height, 0]);
|
||||||
|
|
||||||
|
// Add X axis
|
||||||
|
svg.append("g")
|
||||||
|
.attr("transform", `translate(0,${height})`)
|
||||||
|
.call(d3.axisBottom(x));
|
||||||
|
|
||||||
|
// Add Y axis
|
||||||
|
svg.append("g")
|
||||||
|
.call(d3.axisLeft(y));
|
||||||
|
|
||||||
|
// X axis label
|
||||||
|
svg.append("text")
|
||||||
|
.attr("class", "axis-label")
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("x", width / 2)
|
||||||
|
.attr("y", height + margin.bottom - 10)
|
||||||
|
.text("Timestamp");
|
||||||
|
|
||||||
|
// Y axis label
|
||||||
|
svg.append("text")
|
||||||
|
.attr("class", "axis-label")
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("transform", "rotate(-90)")
|
||||||
|
.attr("y", -margin.left + 20)
|
||||||
|
.attr("x", -height / 2)
|
||||||
|
.text("Lux Value");
|
||||||
|
|
||||||
// Line generator
|
// Line generator
|
||||||
const line = d3.line()
|
const line = d3.line()
|
||||||
.x(d => x(d.timestamp))
|
.x(d => x(d.timestamp))
|
||||||
.y(d => y(d.lux));
|
.y(d => y(d.lux));
|
||||||
|
|
||||||
// X-axis
|
// Add the lux line
|
||||||
svg.append("g")
|
svg.append("path")
|
||||||
.attr("class", "axis")
|
.datum(data)
|
||||||
.attr("transform", `translate(0,${height})`)
|
.attr("class", "line")
|
||||||
.call(d3.axisBottom(x));
|
.attr("d", line)
|
||||||
|
.attr("stroke", "blue");
|
||||||
|
|
||||||
// Y-axis
|
// Add the lux_fast
|
||||||
svg.append("g")
|
svg.append("path")
|
||||||
.attr("class", "axis")
|
.datum(data)
|
||||||
.call(d3.axisLeft(y));
|
.attr("class", "line")
|
||||||
|
.attr("d", d3.line()
|
||||||
|
.x(d => x(d.timestamp))
|
||||||
|
.y(d => y(d.lux_fast)))
|
||||||
|
.attr("stroke", "red");
|
||||||
|
|
||||||
// Lines and data points for each dataset
|
// Add the lux_slow line
|
||||||
datasets.forEach(series => {
|
svg.append("path")
|
||||||
// Line
|
.datum(data)
|
||||||
svg.append("path")
|
.attr("class", "line")
|
||||||
.datum(series.values)
|
.attr("d", d3.line()
|
||||||
.attr("class", "line")
|
.x(d => x(d.timestamp))
|
||||||
.attr("fill", "none")
|
.y(d => y(d.lux_slow)))
|
||||||
.attr("stroke", colorMap.get(series.name))
|
.attr("stroke", "green");
|
||||||
.attr("stroke-width", 2)
|
|
||||||
.attr("d", line);
|
|
||||||
|
|
||||||
// Data points
|
// Add legend
|
||||||
svg.selectAll(`.dot-${series.name}`)
|
const legend = svg.append("g")
|
||||||
.data(series.values)
|
.attr("class", "legend")
|
||||||
.enter().append("circle")
|
.attr("transform", `translate(${width + 10}, 0)`);
|
||||||
.attr("class", `dot dot-${series.name}`)
|
|
||||||
.attr("cx", d => x(d.timestamp))
|
const legendItems = [
|
||||||
.attr("cy", d => y(d.lux))
|
{ label: "Lux", color: "blue" },
|
||||||
.attr("r", 5)
|
{ label: "Lux Fast", color: "red" },
|
||||||
.attr("fill", colorMap.get(series.name))
|
{ label: "Lux Slow", color: "green" }
|
||||||
.on("mouseover", function(event, d) {
|
];
|
||||||
tooltip.transition()
|
|
||||||
.duration(200)
|
legendItems.forEach((item, index) => {
|
||||||
.style("opacity", .9);
|
const legendItem = legend.append("g")
|
||||||
tooltip.html(`${series.name}<br/>timestamp: ${d.timestamp}<br/>lux: ${d.lux}`)
|
.attr("transform", `translate(0, ${index * 20})`);
|
||||||
.style("left", (event.pageX + 5) + "px")
|
|
||||||
.style("top", (event.pageY - 28) + "px");
|
legendItem.append("rect")
|
||||||
})
|
.attr("width", 10)
|
||||||
.on("mouseout", function(d) {
|
.attr("height", 10)
|
||||||
tooltip.transition()
|
.attr("fill", item.color);
|
||||||
.duration(500)
|
|
||||||
.style("opacity", 0);
|
legendItem.append("text")
|
||||||
});
|
.attr("x", 15)
|
||||||
|
.attr("y", 10)
|
||||||
|
.text(item.label);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Legend
|
|
||||||
const legend = svg.selectAll(".legend")
|
|
||||||
.data(datasets)
|
|
||||||
.enter().append("g")
|
|
||||||
.attr("class", "legend")
|
|
||||||
.attr("transform", (d, i) => `translate(${width + 10},${i * 20})`);
|
|
||||||
|
|
||||||
legend.append("rect")
|
|
||||||
.attr("x", 0)
|
|
||||||
.attr("width", 10)
|
|
||||||
.attr("height", 10)
|
|
||||||
.style("fill", d => colorMap.get(d.name));
|
|
||||||
|
|
||||||
legend.append("text")
|
|
||||||
.attr("x", 20)
|
|
||||||
.attr("y", 9)
|
|
||||||
.text(d => d.name)
|
|
||||||
.style("text-anchor", "start");
|
|
||||||
}
|
}
|
||||||
|
// File input event listener
|
||||||
// Render chart
|
|
||||||
renderMultiLineChart(data);
|
|
||||||
// File input handler
|
|
||||||
document.getElementById('fileInput').addEventListener('change', function(e) {
|
document.getElementById('fileInput').addEventListener('change', function(e) {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
const reader = new FileReader();
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
reader.onload = function(event) {
|
reader.onload = function(e) {
|
||||||
try {
|
const contents = e.target.result;
|
||||||
const jsonData = JSON.parse(event.target.result);
|
try {
|
||||||
renderMultiLineChart(jsonData);
|
const data = JSON.parse(contents);
|
||||||
} catch (error) {
|
createChart(data);
|
||||||
alert('Error parsing JSON file: ' + error);
|
} catch (error) {
|
||||||
}
|
console.error("Error parsing JSON:", error);
|
||||||
};
|
alert("Error parsing JSON file. Please make sure it's a valid JSON.");
|
||||||
|
}
|
||||||
reader.readAsText(file);
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
createChart(sample_data);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user