Question

I have a WPF bar chart that I am updating once every 200ms. It updates with new data fine but there is a ghosting effect. So when a bar changes from 1 to 0.5 there is a faded bar where the original bar was. I'm not sure why this is happening but I would to remove it so the bars move cleanly.

MainWindow.xml

<charting:Chart Grid.Column="0" HorizontalAlignment="Stretch" Margin="6,6,6,6" Name="motorPowerChart" VerticalAlignment="Stretch" Title="Motor Power">
                    <charting:ColumnSeries IndependentValueBinding="{Binding Key}"
                                           DependentValueBinding="{Binding Value}"
                                           ItemsSource="{Binding}">
                    </charting:ColumnSeries>
                    <charting:Chart.Axes>
                        <charting:LinearAxis Orientation="Y" Minimum="0" Maximum="1" Title="PWM Frequency"/>
                    </charting:Chart.Axes>
                    <charting:Chart.LegendStyle>
                        <Style TargetType="Control">
                            <Setter Property="Width" Value="0"/>
                            <Setter Property="Height" Value="0"/>
                        </Style>
                    </charting:Chart.LegendStyle>
                </charting:Chart>

MainWindow.xml.cs

public partial class MainWindow : Window
        {
        DispatcherTimer _statusUpdateTimer;
        public ObservableCollection<KeyValuePair<string, double>> MotorPowerGraphData = new ObservableCollection<KeyValuePair<string, double>>();
        public ObservableCollection<KeyValuePair<string, double>> PIDGraphData = new ObservableCollection<KeyValuePair<string, double>>();

        public MainWindow()
        {
              InitializeComponent();

              _statusUpdateTimer = new DispatcherTimer();
              _statusUpdateTimer.Interval = new TimeSpan(0, 0, 0, 0, 200);
              _statusUpdateTimer.Tick += new EventHandler(updateStatus);

              motorPowerChart.DataContext = MotorPowerGraphData;
              pidChart.DataContext = PIDGraphData;
        }

        private void updateStatus(Object myObject, EventArgs myEventArgs)
        {

              double[] status = SerialCommunications.GetStatus();

              MotorPowerGraphData.Clear();
              MotorPowerGraphData.Add(new KeyValuePair<String, double>("Motor1", status[0]));
              MotorPowerGraphData.Add(new KeyValuePair<String, double>("Motor2", status[1]));
              MotorPowerGraphData.Add(new KeyValuePair<String, double>("Motor3", status[2]));
              MotorPowerGraphData.Add(new KeyValuePair<String, double>("Motor4", status[3]));

              PIDGraphData.Clear();
              PIDGraphData.Add(new KeyValuePair<String, double>("Yaw", status[8]));
              PIDGraphData.Add(new KeyValuePair<String, double>("Pitch", status[9]));
              PIDGraphData.Add(new KeyValuePair<String, double>("Roll", status[10]));

        }

Can anyone help me with this?

Thanks

EDIT

I have slowed the timer down so it ticks once per second and have tried inputting dummy data from within the update status function and I am still seeing the same behaviour. It looks worse at 10Hz because the bars dont have time to fade before the next one renders over the top.

Here is my new update status function

int i = 0;
        private void updateStatus(Object myObject, EventArgs myEventArgs)
        {

              /*double[] status = SerialCommunications.GetStatus();

              MotorPowerGraphData.Clear();
              MotorPowerGraphData.Add(new KeyValuePair<String, double>("M1 " + Math.Round(status[0], 3), status[0]));
              MotorPowerGraphData.Add(new KeyValuePair<String, double>("M2 " + Math.Round(status[1], 3), status[1]));
              MotorPowerGraphData.Add(new KeyValuePair<String, double>("M3 " + Math.Round(status[2], 3), status[2]));
              MotorPowerGraphData.Add(new KeyValuePair<String, double>("M4 " + Math.Round(status[3], 3), status[3]));

              PIDGraphData.Clear();
              PIDGraphData.Add(new KeyValuePair<String, double>("Yaw " + Math.Round(status[8], 0), status[8]));
              PIDGraphData.Add(new KeyValuePair<String, double>("Pitch " + Math.Round(status[9], 0), status[9]));
              PIDGraphData.Add(new KeyValuePair<String, double>("Roll " + Math.Round(status[10], 0), status[10]));

              IMUGraphData.Clear();
              IMUGraphData.Add(new KeyValuePair<String, double>("Yaw " + Math.Round(status[4],0), status[4]));
              IMUGraphData.Add(new KeyValuePair<String, double>("Pitch " + Math.Round(status[5], 0), status[5]));
              IMUGraphData.Add(new KeyValuePair<String, double>("Roll " + Math.Round(status[6], 0), status[6]));

              if (status[7] == 1) batteryStatusLabel.Content = "Battery Level: Charged";
              else batteryStatusLabel.Content = "Battery Level: Empty";*/

              if (i == 5) i = 0;
              else
              {
                    IMUGraphData.Clear();
                    IMUGraphData.Add(new KeyValuePair<String, double>("Yaw " + i*30, i*30));
                    IMUGraphData.Add(new KeyValuePair<String, double>("Pitch " + i*-30, i*-30));
                    IMUGraphData.Add(new KeyValuePair<String, double>("Roll " + i * 15, i * 15));
                    i++;
              }

        }

I have uploaded a video to youtube so you can see what the chart looks like as it updates

EDIT

I have tried this in Win forms using the chart control and it works perfectly, however I would like to use WPF because I want to render a cube that rotates in space depending on yaw, pitch and roll and from what I have read this is very easy to do in WPF

EDIT

If no one has an idea why this doesn't work, can anyone recommend some other way of displaying my data?

SOLUTION

I edited the values in place rather than removing and the adding them to the observable collection.

 public partial class MainWindow : Window
        {

        DispatcherTimer _statusUpdateTimer;
        public ObservableCollection<KeyValuePair<string, double>> _motorPowerGraphData = new ObservableCollection<KeyValuePair<string, double>>();
        public ObservableCollection<KeyValuePair<string, double>> _ratePIDGraphData = new ObservableCollection<KeyValuePair<string, double>>();
        public ObservableCollection<KeyValuePair<string, double>> _stabPIDGraphData = new ObservableCollection<KeyValuePair<string, double>>();
        public ObservableCollection<KeyValuePair<string, double>> _imuGraphData = new ObservableCollection<KeyValuePair<string, double>>();

        public MainWindow()
        {
              InitializeComponent();

              _motorPowerGraphData.Clear();
              _motorPowerGraphData.Add(new KeyValuePair<string, double>("M1 0", 0));
              _motorPowerGraphData.Add(new KeyValuePair<string, double>("M2 0", 0));
              _motorPowerGraphData.Add(new KeyValuePair<string, double>("M3 0", 0));
              _motorPowerGraphData.Add(new KeyValuePair<string, double>("M4 0", 0));

              _ratePIDGraphData.Clear();
              _ratePIDGraphData.Add(new KeyValuePair<String, double>("Yaw 0", 0));
              _ratePIDGraphData.Add(new KeyValuePair<String, double>("Pitch 0", 0));
              _ratePIDGraphData.Add(new KeyValuePair<String, double>("Roll 0", 0));

              _stabPIDGraphData.Clear();
              _stabPIDGraphData.Add(new KeyValuePair<String, double>("Yaw 0", 0));
              _stabPIDGraphData.Add(new KeyValuePair<String, double>("Pitch 0", 0));
              _stabPIDGraphData.Add(new KeyValuePair<String, double>("Roll 0", 0));

              _imuGraphData.Clear();
              _imuGraphData.Add(new KeyValuePair<String, double>("Yaw 0", 0));
              _imuGraphData.Add(new KeyValuePair<String, double>("Pitch 0",  0));
              _imuGraphData.Add(new KeyValuePair<String, double>("Roll 0",  0));

              _statusUpdateTimer = new DispatcherTimer();
              _statusUpdateTimer.Interval = new TimeSpan(0, 0, 0, 0, 200);
              _statusUpdateTimer.Tick += new EventHandler(updateStatus);

              motorPowerChart.DataContext = _motorPowerGraphData;
              ratePIDChart.DataContext = _ratePIDGraphData;
              stabPIDChart.DataContext = _stabPIDGraphData;
              imuChart.DataContext = _imuGraphData;
        }

        private void updateStatus(Object myObject, EventArgs myEventArgs)
        {

              double[] status = SerialCommunications.GetStatus();

              _motorPowerGraphData[0] = new KeyValuePair<String, double>("M1 " + Math.Round(status[0], 3), status[0]);
              _motorPowerGraphData[1] = new KeyValuePair<String, double>("M2 " + Math.Round(status[1], 3), status[1]);
              _motorPowerGraphData[2] = new KeyValuePair<String, double>("M3 " + Math.Round(status[2], 3), status[2]);
              _motorPowerGraphData[3] = new KeyValuePair<String, double>("M4 " + Math.Round(status[3], 3), status[3]);

              _ratePIDGraphData[0] = new KeyValuePair<String, double>("Yaw " + Math.Round(status[8], 0), status[8]);
              _ratePIDGraphData[1] = new KeyValuePair<String, double>("Pitch " + Math.Round(status[9], 0), status[9]);
              _ratePIDGraphData[2] = new KeyValuePair<String, double>("Roll " + Math.Round(status[10], 0), status[10]);

              _stabPIDGraphData[0] = new KeyValuePair<String, double>("Yaw " + Math.Round(status[8], 0), status[11]);
              _stabPIDGraphData[1] = new KeyValuePair<String, double>("Pitch " + Math.Round(status[9], 0), status[12]);
              _stabPIDGraphData[2] = new KeyValuePair<String, double>("Roll " + Math.Round(status[10], 0), status[13]);

              _imuGraphData[0] = new KeyValuePair<String, double>("Yaw " + Math.Round(status[4],0), status[4]);
              _imuGraphData[1] = new KeyValuePair<String, double>("Pitch " + Math.Round(status[5], 0), status[5]);
              _imuGraphData[2] = new KeyValuePair<String, double>("Roll " + Math.Round(status[6], 0), status[6]);

              if (status[7] == 1) batteryStatusLabel.Content = "Battery Level: Charged";
              else batteryStatusLabel.Content = "Battery Level: Empty";

              double armed = status[14];
              if (armed == 1)
              {
                    armButton.Content = "Disarm";
                    armedLabel.Content = "Armed: True";
              }
              else
              {
                    armButton.Content = "Arm";
                    armedLabel.Content = "Armed: False";
              }

              double rateMode = status[16];
              double stabMode = status[17];
              if (rateMode == 1 && stabMode == 0)
              {
                    modeLabel.Content = "Mode: Rate";
                    modeButton.Content = "Set Mode To Stability";
              }
              else
              {
                    modeLabel.Content = "Mode: Stability";
                    modeButton.Content = "Set Mode To Rate";
              }

              double initialised = status[15];
              if (initialised == 1) initialisedLabel.Content = "Initialised: True";
              else initialisedLabel.Content = "Initialised: False";
        }
Was it helpful?

Solution

Looking at the source code in the WPFToolkit it looks like BarDataPoint has a "Reveal" VisualState that animates the opacity from 0 -> 1 over 0.5s. My guess is that since you're adding and removing the KeyValuePairs for the bars that the toolkit is 'revealing' the new BarDataPoints as if they were new data.

<VisualStateGroup x:Name="RevealStates">
  <VisualStateGroup.Transitions>
    <VisualTransition GeneratedDuration="0:0:0.5" />
  </VisualStateGroup.Transitions>
  <VisualState x:Name="Shown">
    <Storyboard>
      <DoubleAnimation Storyboard.TargetName="Root" Storyboard.TargetProperty="Opacity" To="1" Duration="0" />
    </Storyboard>
  </VisualState>
  <VisualState x:Name="Hidden">
    <Storyboard>
      <DoubleAnimation Storyboard.TargetName="Root" Storyboard.TargetProperty="Opacity" To="0" Duration="0" />
    </Storyboard>
  </VisualState>
</VisualStateGroup>

I can imagine two potential solutions:

  1. Modify the bound data directly. Instead of adding/removing your KeyValuePairs, update them in place. Getting the bindings to update might be a little tricky depending on how your binding Source is set.
  2. Override the DefaultStyle of the BarDataPoints. Alternatively, directly modify the source code of the WPFToolkit (assuming you're building it yourself) to try removing the problematic "RevealStates".
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top