private void performDrag(MotionEvent event) {

    mLastGesture = ChartGesture.DRAG;

    mMatrix.set(mSavedMatrix);

    OnChartGestureListener l = mChart.getOnChartGestureListener();

    float dX, dY;

    // check if axis is inverted
    if (mChart.isAnyAxisInverted()
        && mClosestDataSetToTouch != null
        && mChart.getAxis(mClosestDataSetToTouch.getAxisDependency()).isInverted()) {

      // if there is an inverted horizontalbarchart
      if (mChart instanceof HorizontalBarChart) {
        dX = -(event.getX() - mTouchStartPoint.x);
        dY = event.getY() - mTouchStartPoint.y;
      } else {
        dX = event.getX() - mTouchStartPoint.x;
        dY = -(event.getY() - mTouchStartPoint.y);
      }
    } else {
      dX = event.getX() - mTouchStartPoint.x;
      dY = event.getY() - mTouchStartPoint.y;
    }

    mMatrix.postTranslate(dX, dY);

    if (l != null) l.onChartTranslate(event, dX, dY);
  }
  public List<SelectionDetail> getSelectionDetailsAtIndex(int xIndex) {

    List<SelectionDetail> vals = new ArrayList<>();

    for (int i = 0; i < mData.getDataSetCount(); i++) {

      DataSet<?> dataSet = mData.getDataSetByIndex(i);

      // extract all y-values from all DataSets at the given x-index
      final float yVal = dataSet.getYValForXIndex(xIndex);
      if (yVal == Float.NaN) continue;

      vals.add(new SelectionDetail(yVal, i, dataSet));
    }

    return vals;
  }
  private Label makeClassLabel(int classId) {

    float[] labels = tempLabels[classId];
    for (int i = 0; i < labels.length; i++) {
      if (trainData.getLabel(i) == classId) labels[i] = 1.0F;
      labels[i] -= 1 / classCount;
    }
    return new Label(labels, null);
  }
  public PointF getTrans(float x, float y) {

    ViewPortHandler vph = mChart.getViewPortHandler();

    float xTrans = x - vph.offsetLeft();
    float yTrans = 0f;

    // check if axis is inverted
    if (mChart.isAnyAxisInverted()
        && mClosestDataSetToTouch != null
        && mChart.isInverted(mClosestDataSetToTouch.getAxisDependency())) {
      yTrans = -(y - vph.offsetTop());
    } else {
      yTrans = -(mChart.getMeasuredHeight() - y - vph.offsetBottom());
    }

    return new PointF(xTrans, yTrans);
  }
  @Override
  public void initialize(DataSet d) {

    trainData = d;
    indices = new TIntArrayList(RandomUtils.getIndexes(d.getInstanceLength()));
    classCount = trainData.getLabels().getClassIndexMap().size();
    scoreCache = new float[trainData.getInstanceLength()][classCount];
    roundData = new DataSet[classCount];
    roundIndices = new int[(int) (trainData.getInstanceLength() * SAMPLE_RATE)];
    tempLabels = new float[classCount][trainData.getInstanceLength()];

    indices.shuffle(new Random());
    for (int j = 0; j < roundIndices.length; j++) roundIndices[j] = indices.get(j);

    for (int j = 0; j < classCount; j++) {
      roundData[j] = new DataSet(trainData.getFeatureMatrix(), makeClassLabel(j));
    }

    log.info(
        "initialize finished, tempLabels + ScoreCache MEM use ~ {} GB",
        2.0 * scoreCache.length * scoreCache[0].length * 4 / 1024 / 1024 / 1024);
  }
  @Override
  public void train() {

    for (int i = 0; i < boosters.length; i++) {

      long t1 = System.currentTimeMillis();

      final int ROUND = i;

      service = Executors.newFixedThreadPool(MAX_THREADS);
      countDownLatch = new CountDownLatch(classCount);
      IntStream.range(0, classCount)
          .forEach(
              j ->
                  service.submit(
                      () -> {
                        long tic = System.currentTimeMillis();

                        try {
                          boosters[ROUND][j].boostInitialize(roundData[j], roundIndices);
                          boosters[ROUND][j].boost();
                        } catch (Throwable t) {
                          log.error(t.getMessage(), t);
                        }

                        long toc = System.currentTimeMillis();
                        log.debug(
                            "round {}, task: {}/{} finished, elapsed {} ms",
                            ROUND,
                            j,
                            classCount,
                            toc - tic);
                        countDownLatch.countDown();
                      }));
      try {
        TimeUnit.SECONDS.sleep(10);
        countDownLatch.await();
      } catch (Throwable t) {
        log.error(t.getMessage(), t);
      }
      service.shutdown();

      roundIndicator[ROUND] = true;

      long t2 = System.currentTimeMillis();

      float[] probs = new float[classCount];
      for (int j = 0; j < trainData.getInstanceLength(); j++) {

        double[] x = trainData.getInstance(j);
        double y = trainData.getLabel(j);
        float[] ys = new float[classCount];
        ys[(int) y] = 1;

        for (int k = 0; k < classCount; k++) {
          scoreCache[j][k] += LEARNING_RATE * boosters[ROUND][k].boostPredict(x);
          probs[k] = (float) Math.exp(scoreCache[j][k]);
        }

        ArraySumUtil.normalize(probs);

        roundKL[ROUND] += (float) ArrayUtil.KLDivergence(ys, probs);

        for (int k = 0; k < classCount; k++) tempLabels[k][j] = ys[k] - probs[k];
      }

      roundKL[ROUND] /= trainData.getInstanceLength();

      for (int j = 0; j < classCount; j++)
        roundData[j] = new DataSet(trainData.getFeatureMatrix(), new Label(tempLabels[j], null));

      indices.shuffle(new Random());
      for (int j = 0; j < roundIndices.length; j++) roundIndices[j] = indices.get(j);

      long t3 = System.currentTimeMillis();

      if (NEED_REPORT) {
        statisticReport(ROUND);
      }

      long t4 = System.currentTimeMillis();

      log.info("round {}, KL {}", ROUND, roundKL[ROUND]);
      log.info(
          "boost {} |update gradient {} |report {}| total {}", t2 - t1, t3 - t2, t4 - t3, t4 - t1);
    }

    log.info("GradientBoostClassification training finished ...");

    if (NEED_REPORT) {
      printRoundReport();
    }
  }